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.
- package/.claude-plugin/README.md +4 -4
- 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 +2 -2
- package/adapters/amp/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
- package/adapters/chatgpt/openapi.yaml +2 -2
- package/adapters/claude/.mcp.json +3 -3
- package/adapters/codex/config.toml +4 -4
- package/adapters/gemini/function-declarations.json +1 -1
- package/adapters/mcp/server-stdio.js +66 -6
- package/adapters/opencode/opencode.json +4 -2
- package/bin/cli.js +188 -39
- 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 +33 -6
- package/config/skill-packs/react-testing.json +1 -1
- package/config/tessl-tiles.json +3 -3
- package/openapi/openapi.yaml +2 -2
- package/package.json +23 -9
- package/plugins/amp-skill/INSTALL.md +3 -2
- 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 +5 -3
- package/plugins/claude-codex-bridge/README.md +1 -1
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +1 -1
- package/plugins/claude-skill/INSTALL.md +4 -3
- package/plugins/claude-skill/SKILL.md +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +5 -3
- package/plugins/codex-profile/INSTALL.md +2 -2
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +5 -5
- package/plugins/cursor-marketplace/mcp.json +4 -2
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +1 -1
- package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
- package/plugins/gemini-extension/INSTALL.md +4 -4
- package/plugins/opencode-profile/INSTALL.md +5 -5
- package/public/dashboard.html +15 -8
- package/public/index.html +134 -375
- 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 +17 -5
- package/scripts/agent-readiness.js +3 -1
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/auto-promote-gates.js +8 -0
- package/scripts/auto-wire-hooks.js +105 -21
- package/scripts/billing.js +111 -7
- package/scripts/build-metadata.js +14 -0
- package/scripts/check-congruence.js +1 -1
- package/scripts/context-engine.js +2 -1
- package/scripts/daemon-manager.js +2 -2
- package/scripts/dashboard.js +2 -2
- package/scripts/data-governance.js +1 -1
- package/scripts/deploy-gcp.sh +1 -1
- package/scripts/deploy-policy.js +22 -4
- package/scripts/dispatch-brief.js +1 -1
- package/scripts/ensure-repo-bootstrap.js +1 -1
- package/scripts/feedback-attribution.js +22 -10
- package/scripts/feedback-fallback.js +3 -2
- package/scripts/feedback-inbox-read.js +1 -1
- package/scripts/feedback-loop.js +41 -3
- package/scripts/feedback-paths.js +8 -8
- package/scripts/feedback-schema.js +1 -1
- package/scripts/feedback-to-memory.js +2 -2
- package/scripts/filesystem-search.js +2 -2
- package/scripts/gates-engine.js +765 -34
- package/scripts/generate-paperbanana-diagrams.sh +3 -3
- package/scripts/github-about.js +1 -1
- package/scripts/gtm-revenue-loop.js +20 -1
- package/scripts/hook-runtime.js +89 -0
- package/scripts/hook-stop-self-score.sh +3 -3
- package/scripts/hook-thumbgate-cache-updater.js +98 -37
- package/scripts/hosted-config.js +12 -10
- package/scripts/hybrid-feedback-context.js +54 -13
- package/scripts/install-mcp.js +14 -1
- package/scripts/intent-router.js +1 -1
- package/scripts/internal-agent-bootstrap.js +1 -1
- package/scripts/lesson-inference.js +6 -1
- package/scripts/license.js +54 -16
- package/scripts/mcp-config.js +69 -7
- package/scripts/memory-migration.js +1 -1
- package/scripts/money-watcher.js +166 -16
- package/scripts/operational-integrity.js +480 -0
- package/scripts/optimize-context.js +1 -1
- package/scripts/perplexity-marketing.js +1 -1
- package/scripts/post-everywhere.js +7 -12
- package/scripts/post-to-x.js +1 -1
- package/scripts/pr-manager.js +14 -11
- package/scripts/problem-detail.js +10 -10
- package/scripts/profile-router.js +2 -0
- package/scripts/prompt-dlp.js +1 -0
- package/scripts/prove-adapters.js +6 -6
- package/scripts/prove-automation.js +1 -1
- package/scripts/prove-autoresearch.js +1 -1
- package/scripts/prove-claim-verification.js +3 -3
- package/scripts/prove-data-pipeline.js +5 -5
- package/scripts/prove-data-quality.js +1 -1
- package/scripts/prove-evolution.js +7 -7
- package/scripts/prove-harnesses.js +2 -2
- package/scripts/prove-lancedb.js +2 -2
- package/scripts/prove-local-intelligence.js +1 -1
- package/scripts/prove-loop-closure.js +1 -1
- package/scripts/prove-predictive-insights.js +2 -2
- package/scripts/prove-runtime.js +6 -6
- package/scripts/prove-seo-gsd.js +1 -1
- package/scripts/prove-settings.js +4 -4
- package/scripts/prove-subway-upgrades.js +1 -1
- package/scripts/prove-tessl.js +2 -2
- package/scripts/prove-xmemory.js +2 -2
- package/scripts/publish-decision.js +10 -0
- package/scripts/published-cli.js +34 -0
- package/scripts/rate-limiter.js +2 -2
- package/scripts/reddit-monitor-cron.sh +2 -2
- package/scripts/reminder-engine.js +1 -1
- package/scripts/schedule-manager.js +3 -3
- package/scripts/self-healing-check.js +1 -1
- package/scripts/shieldcortex-memory-firewall-runner.mjs +1 -1
- package/scripts/skill-quality-tracker.js +1 -1
- 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/generate-instagram-card.js +1 -1
- package/scripts/social-analytics/instagram-thumbgate-post.js +5 -1
- package/scripts/social-analytics/install-growth-automation.js +114 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +8 -2
- package/scripts/social-analytics/publish-thumbgate-launch.js +1 -1
- package/scripts/social-analytics/publishers/reddit.js +7 -12
- package/scripts/social-analytics/publishers/zernio.js +19 -0
- 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-pipeline.js +2 -2
- package/scripts/social-post-hourly.js +185 -0
- package/scripts/social-quality-gate.js +119 -3
- package/scripts/social-reply-monitor.js +150 -34
- 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 +24 -12
- package/scripts/telemetry-analytics.js +4 -4
- package/scripts/tessl-export.js +1 -1
- package/scripts/test-coverage.js +20 -13
- package/scripts/thumbgate-search.js +2 -2
- package/scripts/tool-registry.js +98 -1
- package/scripts/train_from_feedback.py +1 -1
- package/scripts/user-profile.js +4 -4
- package/scripts/validate-feedback.js +1 -1
- package/scripts/vector-store.js +1 -1
- package/scripts/verification-loop.js +1 -1
- package/scripts/verify-run.js +1 -1
- package/scripts/weekly-auto-post.js +1 -1
- package/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
- package/src/api/server.js +291 -41
- package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
- 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, '.
|
|
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
|
-
|
|
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, '.
|
|
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
|
-
|
|
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
|
|
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
|
|
261
|
-
*
|
|
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
|
|
264
|
-
* @param {string} [utmContent] - Optional UTM content tag
|
|
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
|
-
'
|
|
270
|
+
'Happy to answer questions about the implementation.',
|
|
272
271
|
'',
|
|
273
|
-
'
|
|
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
|
+
};
|