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,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Best-effort Claude SessionStart hook that bootstraps ThumbGate/Codex support
|
|
3
|
+
# for repos under ~/workspace/git without surfacing noisy hook errors.
|
|
4
|
+
|
|
5
|
+
set -u
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
HOOK_INPUT="$(cat 2>/dev/null || true)"
|
|
9
|
+
TARGET_DIR="${CLAUDE_PROJECT_DIR:-}"
|
|
10
|
+
|
|
11
|
+
if [ -z "${TARGET_DIR}" ] && [ -n "${HOOK_INPUT}" ]; then
|
|
12
|
+
TARGET_DIR="$(printf '%s' "${HOOK_INPUT}" | /usr/bin/python3 -c 'import json,sys; raw=sys.stdin.read().strip(); print(json.loads(raw).get("cwd","")) if raw else print("")' 2>/dev/null || true)"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [ -z "${TARGET_DIR}" ]; then
|
|
16
|
+
TARGET_DIR="${PWD:-}"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [ -z "${TARGET_DIR}" ]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
REPO_ROOT="$(git -C "${TARGET_DIR}" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
24
|
+
WORKSPACE_ROOT="${HOME:-}/workspace/git"
|
|
25
|
+
|
|
26
|
+
case "${REPO_ROOT}" in
|
|
27
|
+
"${WORKSPACE_ROOT}"/*) ;;
|
|
28
|
+
*) exit 0 ;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
31
|
+
node "${SCRIPT_DIR}/ensure-repo-bootstrap.js" "${REPO_ROOT}" >/dev/null 2>&1 || true
|
|
32
|
+
exit 0
|
|
@@ -15,15 +15,13 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const readline = require('readline');
|
|
17
17
|
const path = require('path');
|
|
18
|
-
|
|
19
|
-
const FEEDBACK_DIR = process.env.THUMBGATE_FEEDBACK_DIR
|
|
20
|
-
|| path.join(__dirname, '..', '.claude', 'memory', 'feedback');
|
|
18
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
21
19
|
|
|
22
20
|
const METRICS_PATH = process.env.METRICS_PATH
|
|
23
|
-
|| path.join(
|
|
21
|
+
|| path.join(resolveFeedbackDir(), 'tool-metrics.jsonl');
|
|
24
22
|
|
|
25
23
|
const FEEDBACK_PATH = process.env.FEEDBACK_PATH
|
|
26
|
-
|| path.join(
|
|
24
|
+
|| path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
|
|
27
25
|
|
|
28
26
|
// Correlation window: feedback within 60 seconds of a tool call is considered correlated
|
|
29
27
|
const CORRELATION_WINDOW_MS = 60_000;
|
|
Binary file
|
|
File without changes
|
|
@@ -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
|
+
};
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const {
|
|
14
14
|
publishPost,
|
|
15
|
-
|
|
15
|
+
schedulePost,
|
|
16
16
|
getConnectedAccounts,
|
|
17
|
+
uploadLocalMedia,
|
|
17
18
|
} = require('./publishers/zernio');
|
|
18
19
|
|
|
19
20
|
const THUMBGATE_CAPTION = `Your AI coding agent has amnesia. It forgets everything between sessions.
|
|
@@ -24,9 +25,14 @@ One command: npx thumbgate init
|
|
|
24
25
|
|
|
25
26
|
Works with Claude Code, Cursor, Codex, Gemini.
|
|
26
27
|
|
|
27
|
-
#AIAgents #DeveloperTools #
|
|
28
|
+
#AIAgents #DeveloperTools #ClaudeCode #ThumbGate`;
|
|
29
|
+
|
|
30
|
+
async function postThumbGateToInstagram(options = {}) {
|
|
31
|
+
const caption = String(options.caption || THUMBGATE_CAPTION).trim();
|
|
32
|
+
const imagePath = options.imagePath ? path.resolve(options.imagePath) : '';
|
|
33
|
+
const schedule = String(options.schedule || '').trim();
|
|
34
|
+
const timezone = String(options.timezone || 'America/New_York').trim() || 'America/New_York';
|
|
28
35
|
|
|
29
|
-
async function postThumbGateToInstagram() {
|
|
30
36
|
try {
|
|
31
37
|
console.log('[instagram] Fetching Zernio connected accounts...');
|
|
32
38
|
const accounts = await getConnectedAccounts();
|
|
@@ -46,10 +52,38 @@ async function postThumbGateToInstagram() {
|
|
|
46
52
|
},
|
|
47
53
|
];
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
if (!imagePath) {
|
|
56
|
+
throw new Error('Instagram posts require an imagePath because Zernio requires media content for Instagram posts.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`[instagram] Uploading Instagram media from ${imagePath}...`);
|
|
60
|
+
const mediaItem = await uploadLocalMedia(imagePath);
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
const publishOptions = {
|
|
63
|
+
mediaItems: [mediaItem],
|
|
64
|
+
utm: options.utm,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let result;
|
|
68
|
+
if (schedule) {
|
|
69
|
+
console.log(`[instagram] Scheduling Instagram post for ${schedule} (${timezone})...`);
|
|
70
|
+
result = await schedulePost(caption, platforms, schedule, timezone, publishOptions);
|
|
71
|
+
} else {
|
|
72
|
+
console.log('[instagram] Publishing ThumbGate caption to Instagram...');
|
|
73
|
+
result = await publishPost(caption, platforms, publishOptions);
|
|
74
|
+
}
|
|
75
|
+
if (result && result.blocked) {
|
|
76
|
+
const reasons = Array.isArray(result.reasons)
|
|
77
|
+
? result.reasons.map((reason) => reason.reason || reason.id || String(reason)).join(', ')
|
|
78
|
+
: 'quality gate blocked the caption';
|
|
79
|
+
throw new Error(`Instagram post blocked: ${reasons}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (schedule) {
|
|
83
|
+
console.log('✅ Instagram post scheduled successfully!');
|
|
84
|
+
} else {
|
|
85
|
+
console.log('✅ Post published successfully!');
|
|
86
|
+
}
|
|
53
87
|
console.log(`Post ID: ${result.id || result.data?.id || 'unknown'}`);
|
|
54
88
|
return result;
|
|
55
89
|
} catch (err) {
|
|
@@ -60,9 +94,13 @@ async function postThumbGateToInstagram() {
|
|
|
60
94
|
|
|
61
95
|
// CLI execution
|
|
62
96
|
if (require.main === module) {
|
|
97
|
+
const args = process.argv.slice(2);
|
|
98
|
+
const imageArg = args.find((arg) => arg.startsWith('--image-path='));
|
|
99
|
+
const imagePath = imageArg ? imageArg.slice('--image-path='.length) : '';
|
|
100
|
+
|
|
63
101
|
(async () => {
|
|
64
102
|
try {
|
|
65
|
-
await postThumbGateToInstagram();
|
|
103
|
+
await postThumbGateToInstagram({ imagePath });
|
|
66
104
|
process.exit(0);
|
|
67
105
|
} catch (err) {
|
|
68
106
|
process.exit(1);
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const dotenv = require('dotenv');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
|
|
8
|
+
|
|
9
|
+
function resolveEnvPath(envPath = DEFAULT_ENV_PATH) {
|
|
10
|
+
return path.isAbsolute(envPath) ? envPath : path.resolve(process.cwd(), envPath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadLocalEnv(options = {}) {
|
|
14
|
+
const resolvedPath = resolveEnvPath(options.envPath);
|
|
15
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
16
|
+
return {
|
|
17
|
+
exists: false,
|
|
18
|
+
loadedKeys: [],
|
|
19
|
+
path: resolvedPath,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parsed = dotenv.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
24
|
+
const loadedKeys = [];
|
|
25
|
+
const override = options.override === true;
|
|
26
|
+
|
|
27
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
28
|
+
if (!override && process.env[key] !== undefined) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
process.env[key] = value;
|
|
32
|
+
loadedKeys.push(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
exists: true,
|
|
37
|
+
loadedKeys,
|
|
38
|
+
path: resolvedPath,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
DEFAULT_ENV_PATH,
|
|
44
|
+
loadLocalEnv,
|
|
45
|
+
resolveEnvPath,
|
|
46
|
+
};
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
require('dotenv').config({ path: require('node:path').resolve(__dirname, '..', '..', '.env') });
|
|
4
3
|
const path = require('node:path');
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const envPath = path.resolve(__dirname, '..', '..', '.env');
|
|
9
|
-
if (fs.existsSync(envPath)) {
|
|
10
|
-
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
11
|
-
for (const line of envContent.split('\n')) {
|
|
12
|
-
const trimmed = line.trim();
|
|
13
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
14
|
-
const eqIdx = trimmed.indexOf('=');
|
|
15
|
-
if (eqIdx > 0) {
|
|
16
|
-
const key = trimmed.slice(0, eqIdx);
|
|
17
|
-
const value = trimmed.slice(eqIdx + 1);
|
|
18
|
-
if (!process.env[key]) process.env[key] = value;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
4
|
+
const { loadLocalEnv } = require('./load-env');
|
|
5
|
+
|
|
6
|
+
loadLocalEnv({ envPath: path.resolve(__dirname, '..', '..', '.env') });
|
|
22
7
|
|
|
23
8
|
const { initDb } = require('./store');
|
|
24
9
|
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const { normalizeZernioMetric } = require('../normalizer');
|
|
14
|
+
const { loadLocalEnv } = require('../load-env');
|
|
14
15
|
const { upsertMetric, initDb } = require('../store');
|
|
15
16
|
const { getConnectedAccounts } = require('../publishers/zernio');
|
|
16
17
|
|
|
17
18
|
const ZERNIO_BASE = 'https://zernio.com/api/v1';
|
|
18
19
|
|
|
20
|
+
loadLocalEnv();
|
|
21
|
+
|
|
19
22
|
function requireApiKey() {
|
|
20
23
|
const key = process.env.ZERNIO_API_KEY;
|
|
21
24
|
if (!key) {
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Options:
|
|
12
12
|
* --image-only Generate image only, don't post
|
|
13
|
-
* --post-only Post
|
|
13
|
+
* --post-only Post an existing image without regenerating it
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const fs = require('node:fs');
|
|
17
18
|
const { generateInstagramCard } = require('./generate-instagram-card');
|
|
18
19
|
const { postThumbGateToInstagram, THUMBGATE_CAPTION } = require('./instagram-thumbgate-post');
|
|
19
20
|
|
|
@@ -22,9 +23,13 @@ const IMAGE_PATH = path.join(REPO_ROOT, '.thumbgate', 'instagram-card.png');
|
|
|
22
23
|
|
|
23
24
|
async function publishInstagramThumbGate(options = {}) {
|
|
24
25
|
const {
|
|
26
|
+
caption = THUMBGATE_CAPTION,
|
|
25
27
|
imageOnly = false,
|
|
26
28
|
postOnly = false,
|
|
27
29
|
imagePath = IMAGE_PATH,
|
|
30
|
+
schedule = '',
|
|
31
|
+
timezone = 'America/New_York',
|
|
32
|
+
utm,
|
|
28
33
|
} = options;
|
|
29
34
|
|
|
30
35
|
try {
|
|
@@ -38,18 +43,32 @@ async function publishInstagramThumbGate(options = {}) {
|
|
|
38
43
|
console.log('[workflow] Image-only mode. Stopping here.');
|
|
39
44
|
return { imagePath: generatedPath };
|
|
40
45
|
}
|
|
46
|
+
} else if (!fs.existsSync(imagePath)) {
|
|
47
|
+
throw new Error(`Image file is required for --post-only mode: ${imagePath}`);
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// Step 2: Post to Instagram (unless --image-only)
|
|
44
51
|
if (!imageOnly) {
|
|
45
52
|
console.log('[workflow] Step 2: Publishing to Instagram via Zernio...');
|
|
46
|
-
const postResult = await postThumbGateToInstagram(
|
|
47
|
-
|
|
53
|
+
const postResult = await postThumbGateToInstagram({
|
|
54
|
+
caption,
|
|
55
|
+
imagePath,
|
|
56
|
+
schedule,
|
|
57
|
+
timezone,
|
|
58
|
+
utm,
|
|
59
|
+
});
|
|
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
|
+
}
|
|
48
65
|
|
|
49
66
|
return {
|
|
50
67
|
success: true,
|
|
51
68
|
imagePath: postOnly ? undefined : imagePath,
|
|
52
69
|
postId: postResult.id || postResult.data?.id,
|
|
70
|
+
scheduled: Boolean(schedule),
|
|
71
|
+
scheduledFor: schedule || undefined,
|
|
53
72
|
};
|
|
54
73
|
}
|
|
55
74
|
} catch (err) {
|