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.
Files changed (113) 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 +89 -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 -0
  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 +68 -6
  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 +7 -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 +34 -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 +3 -18
  92. package/scripts/social-analytics/pollers/zernio.js +3 -0
  93. package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
  94. package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
  95. package/scripts/social-analytics/publishers/reddit.js +7 -12
  96. package/scripts/social-analytics/publishers/zernio.js +210 -22
  97. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  98. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  99. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  100. package/scripts/social-post-hourly.js +185 -0
  101. package/scripts/social-quality-gate.js +119 -3
  102. package/scripts/social-reply-monitor.js +148 -32
  103. package/scripts/statusline-cache-path.js +27 -0
  104. package/scripts/statusline-meta.js +22 -0
  105. package/scripts/statusline.sh +24 -32
  106. package/scripts/sync-version.js +11 -3
  107. package/scripts/test-coverage.js +20 -13
  108. package/scripts/tool-registry.js +97 -0
  109. package/scripts/train_from_feedback.py +32 -9
  110. package/scripts/validate-feedback.js +3 -2
  111. package/scripts/vector-store.js +2 -3
  112. package/scripts/verify-obsidian-setup.sh +3 -3
  113. 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(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,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 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
 
@@ -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 caption only, don't generate image (caption can work without image on Zernio)
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
- console.log(`[workflow] ✅ Post published: ${postResult.id || postResult.data?.id}`);
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) {