thumbgate 0.9.10 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.claude-plugin/README.md +2 -2
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +1 -1
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +4 -4
  9. package/adapters/mcp/server-stdio.js +61 -1
  10. package/adapters/opencode/opencode.json +4 -2
  11. package/bin/cli.js +156 -8
  12. package/bin/memory.sh +3 -3
  13. package/config/e2e-critical-flows.json +4 -0
  14. package/config/gates/default.json +74 -2
  15. package/config/github-about.json +1 -1
  16. package/config/mcp-allowlists.json +27 -0
  17. package/package.json +22 -5
  18. package/plugins/amp-skill/INSTALL.md +1 -0
  19. package/plugins/amp-skill/SKILL.md +1 -0
  20. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  21. package/plugins/claude-codex-bridge/.mcp.json +4 -2
  22. package/plugins/claude-skill/INSTALL.md +1 -0
  23. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  24. package/plugins/codex-profile/.mcp.json +4 -2
  25. package/plugins/codex-profile/INSTALL.md +1 -1
  26. package/plugins/codex-profile/README.md +1 -1
  27. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  28. package/plugins/cursor-marketplace/README.md +3 -3
  29. package/plugins/cursor-marketplace/mcp.json +3 -1
  30. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  31. package/plugins/gemini-extension/INSTALL.md +3 -3
  32. package/plugins/opencode-profile/INSTALL.md +1 -1
  33. package/public/dashboard.html +15 -8
  34. package/public/index.html +125 -185
  35. package/public/js/buyer-intent.js +252 -0
  36. package/public/pro.html +1085 -0
  37. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  38. package/scripts/adk-consolidator.js +14 -2
  39. package/scripts/agent-readiness.js +3 -1
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/auto-promote-gates.js +2 -0
  42. package/scripts/auto-wire-hooks.js +105 -17
  43. package/scripts/behavioral-extraction.js +2 -6
  44. package/scripts/billing.js +107 -3
  45. package/scripts/budget-guard.js +2 -2
  46. package/scripts/build-metadata.js +14 -0
  47. package/scripts/context-engine.js +1 -0
  48. package/scripts/deploy-policy.js +3 -17
  49. package/scripts/dpo-optimizer.js +3 -6
  50. package/scripts/ensure-repo-bootstrap.js +129 -0
  51. package/scripts/export-dpo-pairs.js +2 -3
  52. package/scripts/export-kto-pairs.js +3 -4
  53. package/scripts/export-training.js +8 -6
  54. package/scripts/feedback-attribution.js +23 -11
  55. package/scripts/feedback-loop.js +40 -2
  56. package/scripts/feedback-to-rules.js +2 -1
  57. package/scripts/filesystem-search.js +3 -2
  58. package/scripts/gates-engine.js +760 -29
  59. package/scripts/generate-pretool-hook.sh +0 -0
  60. package/scripts/gtm-revenue-loop.js +20 -1
  61. package/scripts/hook-auto-capture.sh +8 -3
  62. package/scripts/hook-runtime.js +81 -0
  63. package/scripts/hook-stop-self-score.sh +3 -3
  64. package/scripts/hook-thumbgate-cache-updater.js +99 -38
  65. package/scripts/hosted-config.js +4 -16
  66. package/scripts/hybrid-feedback-context.js +54 -14
  67. package/scripts/install-mcp.js +13 -3
  68. package/scripts/intent-router.js +2 -2
  69. package/scripts/license.js +52 -14
  70. package/scripts/local-model-profile.js +3 -2
  71. package/scripts/mcp-config.js +62 -7
  72. package/scripts/meta-policy.js +4 -8
  73. package/scripts/money-watcher.js +166 -16
  74. package/scripts/obsidian-export.js +1 -0
  75. package/scripts/operational-integrity.js +480 -0
  76. package/scripts/post-everywhere.js +35 -12
  77. package/scripts/pr-manager.js +14 -11
  78. package/scripts/profile-router.js +2 -0
  79. package/scripts/prompt-dlp.js +1 -0
  80. package/scripts/publish-decision.js +10 -0
  81. package/scripts/published-cli.js +61 -0
  82. package/scripts/risk-scorer.js +3 -2
  83. package/scripts/rlhf_session_start.sh +32 -0
  84. package/scripts/skill-quality-tracker.js +3 -5
  85. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  86. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  87. package/scripts/social-analytics/engagement-audit.js +202 -0
  88. package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
  89. package/scripts/social-analytics/install-growth-automation.js +114 -0
  90. package/scripts/social-analytics/load-env.js +46 -0
  91. package/scripts/social-analytics/poll-all.js +23 -23
  92. package/scripts/social-analytics/pollers/plausible.js +2 -4
  93. package/scripts/social-analytics/pollers/zernio.js +3 -0
  94. package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
  95. package/scripts/social-analytics/publish-thumbgate-launch.js +322 -0
  96. package/scripts/social-analytics/publishers/reddit.js +7 -12
  97. package/scripts/social-analytics/publishers/zernio.js +301 -22
  98. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  99. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  100. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  101. package/scripts/social-post-hourly.js +185 -0
  102. package/scripts/social-quality-gate.js +119 -3
  103. package/scripts/social-reply-monitor.js +184 -37
  104. package/scripts/statusline-cache-path.js +27 -0
  105. package/scripts/statusline-local-stats.js +16 -0
  106. package/scripts/statusline-meta.js +22 -0
  107. package/scripts/statusline.sh +40 -33
  108. package/scripts/sync-version.js +24 -3
  109. package/scripts/test-coverage.js +21 -13
  110. package/scripts/tool-registry.js +97 -0
  111. package/scripts/train_from_feedback.py +32 -9
  112. package/scripts/validate-feedback.js +3 -2
  113. package/scripts/vector-store.js +2 -3
  114. package/scripts/verify-obsidian-setup.sh +3 -3
  115. package/src/api/server.js +281 -33
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { execFileSync } = require('child_process');
7
+
8
+ function shellQuote(value) {
9
+ return JSON.stringify(String(value));
10
+ }
11
+
12
+ function runtimePrefixDir(prefixDir) {
13
+ return prefixDir || path.join(os.homedir(), '.thumbgate', 'runtime');
14
+ }
15
+
16
+ function publishedCliArgs(pkgVersion, commandArgs = [], options = {}) {
17
+ return [
18
+ 'exec',
19
+ '--prefix',
20
+ runtimePrefixDir(options.prefixDir),
21
+ '--yes',
22
+ '--package',
23
+ `thumbgate@${pkgVersion}`,
24
+ '--',
25
+ 'thumbgate',
26
+ ...commandArgs,
27
+ ];
28
+ }
29
+
30
+ function publishedCliShellCommand(pkgVersion, commandArgs = [], options = {}) {
31
+ const prefixDir = runtimePrefixDir(options.prefixDir);
32
+ return `mkdir -p ${shellQuote(prefixDir)} && exec npm ${publishedCliArgs(pkgVersion, commandArgs, { prefixDir }).map(shellQuote).join(' ')}`;
33
+ }
34
+
35
+ function runPublishedCli(pkgVersion, commandArgs = [], options = {}) {
36
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-published-cli-'));
37
+ const prefixDir = path.join(tmpDir, 'runtime');
38
+ try {
39
+ fs.mkdirSync(prefixDir, { recursive: true });
40
+ return execFileSync('npm', publishedCliArgs(pkgVersion, commandArgs, { prefixDir }), {
41
+ encoding: 'utf8',
42
+ stdio: ['ignore', 'pipe', 'ignore'],
43
+ timeout: options.timeout || 8000,
44
+ cwd: tmpDir,
45
+ });
46
+ } finally {
47
+ fs.rmSync(tmpDir, { recursive: true, force: true });
48
+ }
49
+ }
50
+
51
+ function runPublishedCliHelp(pkgVersion, options = {}) {
52
+ return runPublishedCli(pkgVersion, ['help'], options);
53
+ }
54
+
55
+ module.exports = {
56
+ publishedCliArgs,
57
+ publishedCliShellCommand,
58
+ runtimePrefixDir,
59
+ runPublishedCli,
60
+ runPublishedCliHelp,
61
+ };
@@ -3,9 +3,10 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
6
7
 
7
8
  const PROJECT_ROOT = path.join(__dirname, '..');
8
- const DEFAULT_FEEDBACK_DIR = path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
9
+ const DEFAULT_FEEDBACK_DIR = resolveSharedFeedbackDir();
9
10
  const DEFAULT_MODEL_PATH = path.join(DEFAULT_FEEDBACK_DIR, 'risk-model.json');
10
11
  const DEFAULT_SEQUENCE_PATH = path.join(DEFAULT_FEEDBACK_DIR, 'feedback-sequences.jsonl');
11
12
 
@@ -29,7 +30,7 @@ const SAFETY_WORD_RE = /\b(budget|path|guardrail|safe|security|risk)\b/i;
29
30
  const SUCCESS_WORD_RE = /\b(pass|worked|fixed|success|verified)\b/i;
30
31
 
31
32
  function resolveFeedbackDir(feedbackDir) {
32
- return feedbackDir || process.env.THUMBGATE_FEEDBACK_DIR || DEFAULT_FEEDBACK_DIR;
33
+ return resolveSharedFeedbackDir({ feedbackDir });
33
34
  }
34
35
 
35
36
  function readJSONL(filePath) {
@@ -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(FEEDBACK_DIR, 'tool-metrics.jsonl');
21
+ || path.join(resolveFeedbackDir(), 'tool-metrics.jsonl');
24
22
 
25
23
  const FEEDBACK_PATH = process.env.FEEDBACK_PATH
26
- || path.join(FEEDBACK_DIR, 'feedback-log.jsonl');
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;
@@ -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
- publishToAllPlatforms,
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 #CodingAgents #MCP #OpenSource #ClaudeCode #ThumbGate`;
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
- console.log('[instagram] Publishing ThumbGate caption to Instagram...');
50
- const result = await publishPost(THUMBGATE_CAPTION, platforms);
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
- console.log('✅ Post published successfully!');
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,29 +1,17 @@
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 fs = require('node:fs');
6
-
7
- // Load .env if available
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
 
25
10
  const POLLERS = [
26
11
  { name: 'github', module: './pollers/github', envRequired: ['GITHUB_TOKEN'] },
12
+ // Direct Instagram Graph API poller. Requires INSTAGRAM_ACCESS_TOKEN + INSTAGRAM_USER_ID.
13
+ // When those are absent, Instagram engagement data is still captured via the Zernio poller
14
+ // below (getConnectedAccounts returns Instagram accounts when Zernio is connected to IG).
27
15
  { name: 'instagram', module: './pollers/instagram', envRequired: ['INSTAGRAM_ACCESS_TOKEN', 'INSTAGRAM_USER_ID'] },
28
16
  { name: 'tiktok', module: './pollers/tiktok', envRequired: ['TIKTOK_ACCESS_TOKEN'] },
29
17
  { name: 'linkedin', module: './pollers/linkedin', envRequired: ['LINKEDIN_ACCESS_TOKEN', 'LINKEDIN_PERSON_URN'] },
@@ -31,7 +19,10 @@ const POLLERS = [
31
19
  { name: 'reddit', module: './pollers/reddit', envRequired: ['REDDIT_CLIENT_ID', 'REDDIT_CLIENT_SECRET', 'REDDIT_USERNAME', 'REDDIT_PASSWORD'] },
32
20
  { name: 'threads', module: './pollers/threads', envRequired: ['THREADS_ACCESS_TOKEN', 'THREADS_USER_ID'] },
33
21
  { name: 'youtube', module: './pollers/youtube', envRequired: ['YOUTUBE_API_KEY', 'YOUTUBE_CHANNEL_ID'] },
34
- { name: 'plausible', module: './pollers/plausible', envRequired: ['PLAUSIBLE_API_KEY', 'PLAUSIBLE_SITE_ID'] },
22
+ // PLAUSIBLE_SITE_ID defaults to thumbgate-production.up.railway.app if not set.
23
+ { name: 'plausible', module: './pollers/plausible', envRequired: ['PLAUSIBLE_API_KEY'] },
24
+ // Zernio covers all connected social accounts (including Instagram) via its unified API.
25
+ // Instagram posts published via Zernio will have their engagement metrics captured here.
35
26
  { name: 'zernio', module: './pollers/zernio', envRequired: ['ZERNIO_API_KEY'] },
36
27
  ];
37
28
 
@@ -52,10 +43,19 @@ async function pollAll(options = {}) {
52
43
 
53
44
  try {
54
45
  const mod = require(poller.module);
55
- const fn = mod[`poll${poller.name.charAt(0).toUpperCase()}${poller.name.slice(1)}`]
56
- || mod.pollGitHub || mod.pollInstagram || mod.pollTikTok
57
- || mod.pollLinkedIn || mod.pollX || mod.pollReddit
58
- || mod.pollThreads || mod.pollPlausible || mod.pollZernio;
46
+ // Resolve the poll function by trying the simple title-case name first, then
47
+ // known capitalization variants (pollGitHub, pollTikTok, pollLinkedIn, pollYouTube),
48
+ // and finally any exported function whose name starts with "poll" as a last resort.
49
+ const baseName = poller.name.charAt(0).toUpperCase() + poller.name.slice(1);
50
+ const KNOWN_VARIANTS = {
51
+ github: 'pollGitHub',
52
+ tiktok: 'pollTikTok',
53
+ linkedin: 'pollLinkedIn',
54
+ youtube: 'pollYouTube',
55
+ };
56
+ const fn = mod[`poll${baseName}`]
57
+ || (KNOWN_VARIANTS[poller.name] && mod[KNOWN_VARIANTS[poller.name]])
58
+ || Object.values(mod).find((v) => typeof v === 'function' && v.name && v.name.startsWith('poll'));
59
59
 
60
60
  if (!fn) {
61
61
  console.log(`⚠ ${poller.name}: no poll function found in module`);
@@ -23,10 +23,9 @@ const PLAUSIBLE_BASE = 'https://plausible.io/api/v1';
23
23
  */
24
24
  async function plausibleQuery(endpoint, params = {}) {
25
25
  const apiKey = process.env.PLAUSIBLE_API_KEY;
26
- const siteId = process.env.PLAUSIBLE_SITE_ID;
26
+ const siteId = process.env.PLAUSIBLE_SITE_ID || 'thumbgate-production.up.railway.app';
27
27
 
28
28
  if (!apiKey) throw new Error('PLAUSIBLE_API_KEY is not set');
29
- if (!siteId) throw new Error('PLAUSIBLE_SITE_ID is not set');
30
29
 
31
30
  const qs = new URLSearchParams({ site_id: siteId, ...params });
32
31
  const url = `${PLAUSIBLE_BASE}${endpoint}?${qs.toString()}`;
@@ -153,9 +152,8 @@ async function getFunnelMetrics(period = '7d') {
153
152
  * @returns {Promise<object>} Summary of stored results.
154
153
  */
155
154
  async function pollPlausible(db) {
156
- const siteId = process.env.PLAUSIBLE_SITE_ID;
155
+ const siteId = process.env.PLAUSIBLE_SITE_ID || 'thumbgate-production.up.railway.app';
157
156
  if (!process.env.PLAUSIBLE_API_KEY) throw new Error('PLAUSIBLE_API_KEY is not set');
158
- if (!siteId) throw new Error('PLAUSIBLE_SITE_ID is not set');
159
157
 
160
158
  const period = '7d';
161
159
  const fetchedAt = new Date().toISOString();