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
@@ -12,7 +12,6 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
- const { GoogleGenAI } = require('@google/genai');
16
15
 
17
16
  const PROJECT_ROOT = path.join(__dirname, '..');
18
17
  const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
@@ -91,7 +90,20 @@ async function consolidateMemory() {
91
90
  return;
92
91
  }
93
92
 
94
- const ai = useFakeConsolidation ? null : new GoogleGenAI({ apiKey });
93
+ let ai = null;
94
+ if (!useFakeConsolidation) {
95
+ let GoogleGenAI;
96
+ try {
97
+ ({ GoogleGenAI } = require('@google/genai'));
98
+ } catch (error) {
99
+ if (error && error.code === 'MODULE_NOT_FOUND') {
100
+ console.warn('[ADK Consolidator] @google/genai is not installed. Skipping active consolidation.');
101
+ return;
102
+ }
103
+ throw error;
104
+ }
105
+ ai = new GoogleGenAI({ apiKey });
106
+ }
95
107
  const paths = getFeedbackPaths();
96
108
  const state = loadState();
97
109
 
@@ -20,6 +20,8 @@ const WRITE_CAPABLE_TOOLS = new Set([
20
20
  'evaluate_context_pack',
21
21
  'generate_skill',
22
22
  'satisfy_gate',
23
+ 'set_task_scope',
24
+ 'approve_protected_action',
23
25
  'track_action',
24
26
  'register_claim_gate',
25
27
  ]);
@@ -29,7 +31,7 @@ const BOOTSTRAP_FILES = [
29
31
  { id: 'claude', path: 'CLAUDE.md', required: true },
30
32
  { id: 'gemini', path: 'GEMINI.md', required: true },
31
33
  { id: 'mcp', path: '.mcp.json', required: true },
32
- { id: 'rlhfConfig', path: '.thumbgate/config.json', required: false },
34
+ { id: 'thumbgateConfig', path: '.thumbgate/config.json', required: false },
33
35
  ];
34
36
 
35
37
  const MCP_PROFILE_TIERS = {
@@ -84,10 +84,10 @@ function getCredentialAudit({ periodHours = 24 } = {}) {
84
84
 
85
85
  // MCP profile tool allowlists (loaded from config or defaults)
86
86
  const PROFILE_ALLOWLISTS = {
87
- essential: new Set(['capture_feedback', 'recall', 'search_lessons', 'search_thumbgate', 'prevention_rules', 'enforcement_matrix', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard']),
88
- readonly: new Set(['recall', 'feedback_summary', 'search_lessons', 'verify_claim', 'gate_stats', 'search_thumbgate', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard']),
89
- locked: new Set(['feedback_summary', 'search_lessons', 'diagnose_failure', 'list_intents', 'plan_intent', 'list_harnesses', 'verify_claim']),
90
- commerce: new Set(['capture_feedback', 'recall', 'search_thumbgate', 'commerce_recall', 'track_action', 'verify_claim', 'feedback_stats']),
87
+ essential: new Set(['capture_feedback', 'recall', 'search_lessons', 'search_thumbgate', 'prevention_rules', 'enforcement_matrix', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard', 'set_task_scope', 'get_scope_state', 'set_branch_governance', 'get_branch_governance', 'approve_protected_action', 'check_operational_integrity']),
88
+ readonly: new Set(['recall', 'feedback_summary', 'search_lessons', 'verify_claim', 'gate_stats', 'search_thumbgate', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard', 'get_scope_state', 'get_branch_governance', 'check_operational_integrity']),
89
+ locked: new Set(['feedback_summary', 'search_lessons', 'diagnose_failure', 'list_intents', 'plan_intent', 'list_harnesses', 'verify_claim', 'get_scope_state', 'get_branch_governance', 'check_operational_integrity']),
90
+ commerce: new Set(['capture_feedback', 'recall', 'search_thumbgate', 'commerce_recall', 'track_action', 'verify_claim', 'feedback_stats', 'set_task_scope', 'get_scope_state', 'set_branch_governance', 'get_branch_governance', 'approve_protected_action', 'check_operational_integrity']),
91
91
  };
92
92
 
93
93
  /**
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { resolveFeedbackDir } = require('./feedback-paths');
6
7
 
7
8
  const MAX_AUTO_GATES = 10;
8
9
  const WARN_THRESHOLD = 3; // 3+ repeated failures surface a warning gate
@@ -20,6 +21,7 @@ function getFeedbackLogPath() {
20
21
  if (fs.existsSync(localFallback)) return localFallback;
21
22
  if (fs.existsSync(localClaude)) return localClaude;
22
23
  return localFallback; // default even if doesn't exist
24
+ return path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
23
25
  }
24
26
 
25
27
  function getAutoGatesPath() {
@@ -15,35 +15,31 @@
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
-
19
- const PKG_ROOT = path.join(__dirname, '..');
18
+ const {
19
+ cacheUpdateHookCommand,
20
+ preToolHookCommand,
21
+ sessionStartHookCommand,
22
+ statuslineCommand,
23
+ userPromptHookCommand,
24
+ } = require('./hook-runtime');
20
25
 
21
26
  function getHome() {
22
27
  return process.env.HOME || process.env.USERPROFILE || '';
23
28
  }
24
29
 
25
30
  // --- Hook definitions ---
26
-
27
- function preToolHookCommand() {
28
- return 'bash scripts/generate-pretool-hook.sh';
29
- }
30
-
31
- function userPromptHookCommand() {
32
- return 'bash scripts/hook-auto-capture.sh';
33
- }
34
-
35
- function sessionStartHookCommand() {
36
- return 'bash scripts/thumbgate_session_start.sh';
37
- }
38
-
39
31
  const CLAUDE_HOOKS = {
40
32
  PreToolUse: {
41
- matcher: 'Bash',
33
+ matcher: 'Bash|Edit|Write|MultiEdit',
42
34
  hooks: [{ type: 'command', command: preToolHookCommand() }],
43
35
  },
44
36
  UserPromptSubmit: {
45
37
  hooks: [{ type: 'command', command: userPromptHookCommand() }],
46
38
  },
39
+ PostToolUse: {
40
+ matcher: 'mcp__thumbgate__feedback_stats|mcp__thumbgate__dashboard',
41
+ hooks: [{ type: 'command', command: cacheUpdateHookCommand() }],
42
+ },
47
43
  SessionStart: {
48
44
  hooks: [{ type: 'command', command: sessionStartHookCommand() }],
49
45
  },
@@ -74,6 +70,10 @@ function claudeSettingsPath() {
74
70
  return path.join(getHome(), '.claude', 'settings.local.json');
75
71
  }
76
72
 
73
+ function claudeSharedSettingsPath() {
74
+ return path.join(getHome(), '.claude', 'settings.json');
75
+ }
76
+
77
77
  function loadJsonFile(filePath) {
78
78
  if (!fs.existsSync(filePath)) return null;
79
79
  try {
@@ -92,17 +92,67 @@ function hookAlreadyPresent(hookArray, command) {
92
92
  );
93
93
  }
94
94
 
95
+ function pruneLegacyHookEntries(hookArray, expectedCommand, legacyPattern) {
96
+ if (!Array.isArray(hookArray)) {
97
+ return { hooks: [], removed: false };
98
+ }
99
+
100
+ let removed = false;
101
+ const hooks = hookArray.filter((entry) => {
102
+ const entryHooks = Array.isArray(entry && entry.hooks) ? entry.hooks : [];
103
+ const shouldRemove = entryHooks.some((hook) => {
104
+ const command = hook && typeof hook.command === 'string' ? hook.command : '';
105
+ return command !== expectedCommand && legacyPattern.test(command);
106
+ });
107
+ if (shouldRemove) {
108
+ removed = true;
109
+ return false;
110
+ }
111
+ return true;
112
+ });
113
+
114
+ return { hooks, removed };
115
+ }
116
+
117
+ function syncClaudeStatusLine(settingsPath, desiredStatusLine, dryRun) {
118
+ const settings = loadJsonFile(settingsPath) || {};
119
+ if (settings.statusLine && settings.statusLine.command === desiredStatusLine) {
120
+ return false;
121
+ }
122
+
123
+ settings.statusLine = { type: 'command', command: desiredStatusLine };
124
+ if (!dryRun) {
125
+ const dir = path.dirname(settingsPath);
126
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
127
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
128
+ }
129
+ return true;
130
+ }
131
+
95
132
  function wireClaudeHooks(options) {
96
133
  const settingsPath = options.settingsPath || claudeSettingsPath();
134
+ const sharedSettingsPath = options.sharedSettingsPath || claudeSharedSettingsPath();
97
135
  const dryRun = options.dryRun || false;
136
+ const desiredStatusLine = statuslineCommand();
98
137
 
99
138
  let settings = loadJsonFile(settingsPath) || {};
100
139
  settings.hooks = settings.hooks || {};
101
140
 
102
141
  const added = [];
142
+ const legacyPatterns = {
143
+ PreToolUse: /(generate-pretool-hook\.sh|\bgate-check\b)/,
144
+ UserPromptSubmit: /(hook-auto-capture\.sh|hook-auto-capture\b)/,
145
+ PostToolUse: /(hook-thumbgate-cache-updater|cache-update\b)/,
146
+ SessionStart: /(thumbgate_session_start\.sh|session-start\b)/,
147
+ };
103
148
 
104
149
  for (const [lifecycle, hookDef] of Object.entries(CLAUDE_HOOKS)) {
105
150
  const hookCommand = hookDef.hooks[0].command;
151
+ const pruned = pruneLegacyHookEntries(settings.hooks[lifecycle], hookCommand, legacyPatterns[lifecycle]);
152
+ settings.hooks[lifecycle] = pruned.hooks;
153
+ if (pruned.removed) {
154
+ added.push({ lifecycle, command: `${hookCommand} (replaced legacy ThumbGate hook)` });
155
+ }
106
156
 
107
157
  if (hookAlreadyPresent(settings.hooks[lifecycle], hookCommand)) {
108
158
  continue;
@@ -118,15 +168,41 @@ function wireClaudeHooks(options) {
118
168
  }
119
169
 
120
170
  if (added.length === 0) {
121
- return { changed: false, settingsPath, added: [] };
171
+ if (!settings.statusLine || settings.statusLine.command !== desiredStatusLine) {
172
+ if (!dryRun) {
173
+ const dir = path.dirname(settingsPath);
174
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
175
+ }
176
+ settings.statusLine = { type: 'command', command: desiredStatusLine };
177
+ if (!dryRun) {
178
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
179
+ }
180
+ const addedEntries = [{ lifecycle: 'statusLine', command: desiredStatusLine }];
181
+ if (syncClaudeStatusLine(sharedSettingsPath, desiredStatusLine, dryRun)) {
182
+ addedEntries.push({ lifecycle: 'statusLine', command: `${desiredStatusLine} (synced ~/.claude/settings.json)` });
183
+ }
184
+ return { changed: true, settingsPath, added: addedEntries };
185
+ }
186
+ const sharedStatusChanged = syncClaudeStatusLine(sharedSettingsPath, desiredStatusLine, dryRun);
187
+ return {
188
+ changed: sharedStatusChanged,
189
+ settingsPath,
190
+ added: sharedStatusChanged ? [{ lifecycle: 'statusLine', command: `${desiredStatusLine} (synced ~/.claude/settings.json)` }] : [],
191
+ };
122
192
  }
123
193
 
194
+ settings.statusLine = { type: 'command', command: desiredStatusLine };
195
+
124
196
  if (!dryRun) {
125
197
  const dir = path.dirname(settingsPath);
126
198
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
127
199
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
128
200
  }
129
201
 
202
+ if (syncClaudeStatusLine(sharedSettingsPath, desiredStatusLine, dryRun)) {
203
+ added.push({ lifecycle: 'statusLine', command: `${desiredStatusLine} (synced ~/.claude/settings.json)` });
204
+ }
205
+
130
206
  return { changed: true, settingsPath, added };
131
207
  }
132
208
 
@@ -147,6 +223,11 @@ function wireCodexHooks(options) {
147
223
  const preToolCmd = preToolHookCommand();
148
224
  const userPromptCmd = userPromptHookCommand();
149
225
 
226
+ const preToolPruned = pruneLegacyHookEntries(config.hooks.PreToolUse, preToolCmd, /(generate-pretool-hook\.sh|\bgate-check\b)/);
227
+ config.hooks.PreToolUse = preToolPruned.hooks;
228
+ const userPromptPruned = pruneLegacyHookEntries(config.hooks.UserPromptSubmit, userPromptCmd, /(hook-auto-capture\.sh|hook-auto-capture\b)/);
229
+ config.hooks.UserPromptSubmit = userPromptPruned.hooks;
230
+
150
231
  if (!hookAlreadyPresent(config.hooks.PreToolUse, preToolCmd)) {
151
232
  config.hooks.PreToolUse = config.hooks.PreToolUse || [];
152
233
  config.hooks.PreToolUse.push({
@@ -194,6 +275,11 @@ function wireGeminiHooks(options) {
194
275
  const preToolCmd = preToolHookCommand();
195
276
  const userPromptCmd = userPromptHookCommand();
196
277
 
278
+ const preToolPruned = pruneLegacyHookEntries(settings.hooks.PreToolUse, preToolCmd, /(generate-pretool-hook\.sh|\bgate-check\b)/);
279
+ settings.hooks.PreToolUse = preToolPruned.hooks;
280
+ const userPromptPruned = pruneLegacyHookEntries(settings.hooks.UserPromptSubmit, userPromptCmd, /(hook-auto-capture\.sh|hook-auto-capture\b)/);
281
+ settings.hooks.UserPromptSubmit = userPromptPruned.hooks;
282
+
197
283
  if (!hookAlreadyPresent(settings.hooks.PreToolUse, preToolCmd)) {
198
284
  settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
199
285
  settings.hooks.PreToolUse.push({
@@ -282,8 +368,10 @@ module.exports = {
282
368
  loadJsonFile,
283
369
  parseFlags,
284
370
  claudeSettingsPath,
371
+ claudeSharedSettingsPath,
285
372
  codexConfigPath,
286
373
  geminiSettingsPath,
374
+ syncClaudeStatusLine,
287
375
  CLAUDE_HOOKS,
288
376
  preToolHookCommand,
289
377
  userPromptHookCommand,
@@ -8,13 +8,9 @@
8
8
 
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
+ const { resolveFeedbackDir } = require('./feedback-paths');
11
12
 
12
- const HOME = process.env.HOME || process.env.USERPROFILE || '';
13
- const envDir = process.env.THUMBGATE_FEEDBACK_DIR;
14
- const localFallback = path.join(process.cwd(), '.thumbgate');
15
- const localClaude = path.join(process.cwd(), '.claude', 'memory', 'feedback');
16
- const baseDir = envDir || (fs.existsSync(localFallback) ? localFallback : localClaude);
17
-
13
+ const baseDir = resolveFeedbackDir();
18
14
  const FEEDBACK_LOG_PATH = path.join(baseDir, 'feedback-log.jsonl');
19
15
  const TRAITS_PATH = path.join(baseDir, 'behavioral-traits.json');
20
16
 
@@ -16,7 +16,6 @@ function withTimeout(promise, ms = STRIPE_TIMEOUT_MS) {
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const crypto = require('crypto');
19
- const Stripe = require('stripe');
20
19
  const { createTraceId } = require('./hosted-config');
21
20
  const {
22
21
  getFeedbackPaths,
@@ -71,6 +70,9 @@ const CONFIG = {
71
70
  get LOCAL_CHECKOUT_SESSIONS_PATH() {
72
71
  return process.env._TEST_LOCAL_CHECKOUT_SESSIONS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'local-checkout-sessions.json');
73
72
  },
73
+ get NEWSLETTER_SUBSCRIBERS_PATH() {
74
+ return process.env._TEST_NEWSLETTER_SUBSCRIBERS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'newsletter-subscribers.jsonl');
75
+ },
74
76
  CREDIT_PACKS: {
75
77
  'mistake-free-starter': {
76
78
  id: 'mistake-free-starter',
@@ -89,11 +91,27 @@ function resolveLegacyBillingPath(fileName) {
89
91
  }
90
92
 
91
93
  let _stripeClient = null;
94
+ let _stripeCtor = null;
95
+
96
+ function getStripeConstructor() {
97
+ if (_stripeCtor) return _stripeCtor;
98
+ try {
99
+ _stripeCtor = require('stripe');
100
+ return _stripeCtor;
101
+ } catch (error) {
102
+ if (error && error.code === 'MODULE_NOT_FOUND') {
103
+ throw new Error('stripe package is not installed. Live billing features are unavailable.');
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+
92
109
  function getStripeClient() {
93
110
  if (!_stripeClient) {
94
111
  if (!CONFIG.STRIPE_SECRET_KEY) {
95
112
  throw new Error('STRIPE_SECRET_KEY is missing. Stripe client cannot be initialized.');
96
113
  }
114
+ const Stripe = getStripeConstructor();
97
115
  _stripeClient = new Stripe(CONFIG.STRIPE_SECRET_KEY);
98
116
  }
99
117
  return _stripeClient;
@@ -297,6 +315,11 @@ function buildBillingSourceDiagnostics(feedbackDir) {
297
315
  legacyPath: resolveLegacyBillingPath('local-checkout-sessions.json'),
298
316
  mode: 'fallback',
299
317
  });
318
+ const newsletterSubscribers = describeDataFile({
319
+ primaryPath: CONFIG.NEWSLETTER_SUBSCRIBERS_PATH,
320
+ legacyPath: resolveLegacyBillingPath('newsletter-subscribers.jsonl'),
321
+ mode: 'fallback',
322
+ });
300
323
  const telemetry = getTelemetrySourceDiagnostics(feedbackDir);
301
324
  const warnings = [
302
325
  ...telemetry.warnings,
@@ -338,7 +361,7 @@ function buildBillingSourceDiagnostics(feedbackDir) {
338
361
  ));
339
362
  }
340
363
 
341
- const mixedRoots = [keyStore, funnelLedger, revenueLedger, checkoutSessions, telemetry]
364
+ const mixedRoots = [keyStore, funnelLedger, revenueLedger, checkoutSessions, newsletterSubscribers, telemetry]
342
365
  .some((descriptor) => descriptor.mixedRoots || descriptor.activeMode === 'legacy_fallback');
343
366
  if (mixedRoots) {
344
367
  warnings.push(buildSourceWarning(
@@ -357,6 +380,7 @@ function buildBillingSourceDiagnostics(feedbackDir) {
357
380
  funnelLedger,
358
381
  revenueLedger,
359
382
  checkoutSessions,
383
+ newsletterSubscribers,
360
384
  telemetry,
361
385
  },
362
386
  warnings,
@@ -622,6 +646,13 @@ function loadRevenueLedger() {
622
646
  );
623
647
  }
624
648
 
649
+ function loadNewsletterSubscribers() {
650
+ return loadJsonlRecords(
651
+ CONFIG.NEWSLETTER_SUBSCRIBERS_PATH,
652
+ resolveLegacyBillingPath('newsletter-subscribers.jsonl')
653
+ );
654
+ }
655
+
625
656
  function resolveRevenueLedgerFilePath() {
626
657
  const primary = CONFIG.REVENUE_LEDGER_PATH;
627
658
  const legacy = resolveLegacyBillingPath('revenue-events.jsonl');
@@ -1331,6 +1362,11 @@ function getBusinessAnalytics(options = {}) {
1331
1362
  analyticsWindow,
1332
1363
  (entry) => entry && entry.submittedAt
1333
1364
  );
1365
+ const newsletterSubscribers = filterEntriesForWindow(
1366
+ loadNewsletterSubscribers(),
1367
+ analyticsWindow,
1368
+ (entry) => entry && entry.subscribedAt
1369
+ );
1334
1370
  const funnel = getFunnelAnalytics({ ...analyticsWindow, extraRevenueEvents });
1335
1371
  const acquisitionEvents = events.filter((entry) => entry && entry.stage === 'acquisition');
1336
1372
  const paidEvents = events.filter((entry) => entry && entry.stage === 'paid');
@@ -1544,6 +1580,17 @@ function getBusinessAnalytics(options = {}) {
1544
1580
  let workflowSprintLeadLatestAt = null;
1545
1581
  let workflowSprintLeadContactable = 0;
1546
1582
  let qualifiedWorkflowSprintLeadCount = 0;
1583
+ const newsletterBySource = {};
1584
+ const newsletterByCampaign = {};
1585
+ const newsletterByCreator = {};
1586
+ const newsletterByCommunity = {};
1587
+ const newsletterByPostId = {};
1588
+ const newsletterByCommentId = {};
1589
+ const newsletterByCampaignVariant = {};
1590
+ const newsletterByOfferCode = {};
1591
+ const newsletterSubscriberKeys = new Set();
1592
+ let newsletterLatest = null;
1593
+ let newsletterLatestAt = null;
1547
1594
 
1548
1595
  for (const entry of workflowSprintLeads) {
1549
1596
  if (!entry || typeof entry !== 'object') continue;
@@ -1584,6 +1631,46 @@ function getBusinessAnalytics(options = {}) {
1584
1631
  }
1585
1632
  }
1586
1633
 
1634
+ for (const entry of newsletterSubscribers) {
1635
+ if (!entry || typeof entry !== 'object') continue;
1636
+ const attribution = extractAttribution({
1637
+ ...sanitizeMetadata(entry.attribution || {}),
1638
+ ...sanitizeMetadata(entry),
1639
+ });
1640
+ incrementCounter(newsletterBySource, resolveAttributionSource(attribution, entry.source || 'newsletter'));
1641
+ incrementCounter(newsletterByCampaign, resolveAttributionCampaign(attribution));
1642
+ incrementCounter(newsletterByCreator, attribution.creator);
1643
+ incrementCounter(newsletterByCommunity, attribution.community);
1644
+ incrementCounter(newsletterByPostId, attribution.postId);
1645
+ incrementCounter(newsletterByCommentId, attribution.commentId);
1646
+ incrementCounter(newsletterByCampaignVariant, attribution.campaignVariant);
1647
+ incrementCounter(newsletterByOfferCode, attribution.offerCode);
1648
+
1649
+ newsletterSubscriberKeys.add(
1650
+ pickFirstText(
1651
+ entry.email,
1652
+ entry.acquisitionId,
1653
+ entry.visitorId,
1654
+ entry.sessionId,
1655
+ entry.subscribedAt
1656
+ )
1657
+ );
1658
+
1659
+ if (!newsletterLatestAt || String(entry.subscribedAt || '') > newsletterLatestAt) {
1660
+ newsletterLatestAt = entry.subscribedAt || null;
1661
+ newsletterLatest = {
1662
+ email: entry.email || null,
1663
+ subscribedAt: entry.subscribedAt || null,
1664
+ source: resolveAttributionSource(attribution, entry.source || 'newsletter'),
1665
+ campaign: resolveAttributionCampaign(attribution),
1666
+ creator: attribution.creator || null,
1667
+ community: attribution.community || null,
1668
+ landingPath: pickFirstText(entry.landingPath, attribution.landingPath),
1669
+ referrerHost: entry.referrerHost || null,
1670
+ };
1671
+ }
1672
+ }
1673
+
1587
1674
  const unreconciledPaidEvents = paidEvents.filter((entry) => {
1588
1675
  const eventKey = resolvePaidProviderEventKey(entry);
1589
1676
  if (!eventKey) return true;
@@ -1603,6 +1690,7 @@ function getBusinessAnalytics(options = {}) {
1603
1690
  checkoutLookupFailures: telemetry.ctas ? telemetry.ctas.lookupFailures || 0 : 0,
1604
1691
  buyerLossFeedback: telemetry.buyerLoss ? telemetry.buyerLoss.totalSignals || 0 : 0,
1605
1692
  seoLandingViews: telemetry.seo ? telemetry.seo.landingViews || 0 : 0,
1693
+ newsletterSignups: newsletterSubscribers.length,
1606
1694
  };
1607
1695
 
1608
1696
  const operatorGeneratedAcquisition = {
@@ -1632,6 +1720,7 @@ function getBusinessAnalytics(options = {}) {
1632
1720
  tracksInvoices: false,
1633
1721
  tracksAttribution: true,
1634
1722
  tracksWorkflowSprintLeads: true,
1723
+ tracksNewsletterSubscribers: true,
1635
1724
  providerCoverage: {
1636
1725
  stripe: processorReconciledOrders > 0 ? 'booked_revenue+processor_reconciled' : 'booked_revenue',
1637
1726
  githubMarketplace: 'webhook_or_configured_plan_prices',
@@ -1701,6 +1790,20 @@ function getBusinessAnalytics(options = {}) {
1701
1790
  byCreator: qualifiedWorkflowSprintLeadByCreator,
1702
1791
  },
1703
1792
  },
1793
+ newsletter: {
1794
+ total: newsletterSubscribers.length,
1795
+ uniqueSubscribers: newsletterSubscriberKeys.size,
1796
+ bySource: newsletterBySource,
1797
+ byCampaign: newsletterByCampaign,
1798
+ byCreator: newsletterByCreator,
1799
+ byCommunity: newsletterByCommunity,
1800
+ byPostId: newsletterByPostId,
1801
+ byCommentId: newsletterByCommentId,
1802
+ byCampaignVariant: newsletterByCampaignVariant,
1803
+ byOfferCode: newsletterByOfferCode,
1804
+ latestSubscribedAt: newsletterLatestAt,
1805
+ latestSubscriber: newsletterLatest,
1806
+ },
1704
1807
  attribution: {
1705
1808
  acquisitionBySource: signupsBySource,
1706
1809
  acquisitionByCampaign: signupsByCampaign,
@@ -1822,6 +1925,7 @@ function getBillingSummary(options = {}) {
1822
1925
  signups: business.signups,
1823
1926
  revenue: business.revenue,
1824
1927
  pipeline: business.pipeline,
1928
+ newsletter: business.newsletter,
1825
1929
  attribution: business.attribution,
1826
1930
  trafficMetrics: business.trafficMetrics,
1827
1931
  operatorGeneratedAcquisition: business.operatorGeneratedAcquisition,
@@ -2426,7 +2530,7 @@ function handleGithubWebhook(event) {
2426
2530
  }
2427
2531
 
2428
2532
  module.exports = {
2429
- CONFIG, createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, disableCustomerKeys, handleWebhook, verifyWebhookSignature, verifyGithubWebhookSignature, handleGithubWebhook, loadKeyStore, appendFunnelEvent, appendRevenueEvent, loadFunnelLedger, loadRevenueLedger, loadResolvedRevenueEvents, getFunnelAnalytics, getBusinessAnalytics, getBillingSummary, getBillingSummaryLive, listStripeReconciledRevenueEvents, repairGithubMarketplaceRevenueLedger,
2533
+ CONFIG, createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, disableCustomerKeys, handleWebhook, verifyWebhookSignature, verifyGithubWebhookSignature, handleGithubWebhook, loadKeyStore, appendFunnelEvent, appendRevenueEvent, loadFunnelLedger, loadRevenueLedger, loadNewsletterSubscribers, loadResolvedRevenueEvents, getFunnelAnalytics, getBusinessAnalytics, getBillingSummary, getBillingSummaryLive, listStripeReconciledRevenueEvents, repairGithubMarketplaceRevenueLedger,
2430
2534
  _buildCheckoutSessionPayload: buildCheckoutSessionPayload,
2431
2535
  _resolveSubscriptionCheckoutSelection: resolveSubscriptionCheckoutSelection,
2432
2536
  _API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const { resolveFeedbackDir } = require('./feedback-paths');
4
5
 
5
- const PROJECT_ROOT = path.join(__dirname, '..');
6
- const FEEDBACK_DIR = process.env.THUMBGATE_FEEDBACK_DIR || path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
6
+ const FEEDBACK_DIR = resolveFeedbackDir();
7
7
  const LEDGER_PATH = path.join(FEEDBACK_DIR, 'budget-ledger.json');
8
8
  const LOCK_PATH = `${LEDGER_PATH}.lock`;
9
9
 
@@ -3,6 +3,8 @@ const path = require('path');
3
3
 
4
4
  const PROJECT_ROOT = path.resolve(__dirname, '..');
5
5
  const DEFAULT_BUILD_METADATA_PATH = path.join(PROJECT_ROOT, 'config', 'build-metadata.json');
6
+ const BUILD_SHA_ENV_KEY = 'THUMBGATE_BUILD_SHA';
7
+ const BUILD_GENERATED_AT_ENV_KEY = 'THUMBGATE_BUILD_GENERATED_AT';
6
8
 
7
9
  function normalizeNullableText(value) {
8
10
  if (typeof value !== 'string') {
@@ -18,6 +20,16 @@ function resolveBuildMetadata({ env = process.env, filePath } = {}) {
18
20
  normalizeNullableText(filePath) ||
19
21
  normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
20
22
  DEFAULT_BUILD_METADATA_PATH;
23
+ const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
24
+ const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
25
+
26
+ if (envBuildSha || envGeneratedAt) {
27
+ return {
28
+ path: resolvedPath,
29
+ buildSha: envBuildSha,
30
+ generatedAt: envGeneratedAt,
31
+ };
32
+ }
21
33
 
22
34
  try {
23
35
  const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
@@ -91,6 +103,8 @@ if (require.main === module) {
91
103
  }
92
104
 
93
105
  module.exports = {
106
+ BUILD_GENERATED_AT_ENV_KEY,
107
+ BUILD_SHA_ENV_KEY,
94
108
  DEFAULT_BUILD_METADATA_PATH,
95
109
  resolveBuildMetadata,
96
110
  writeBuildMetadataFile,
@@ -10,6 +10,7 @@
10
10
  * MCP tool calls and context window consumption.
11
11
  *
12
12
  * Ported from Subway_RN_Demo/scripts/context-engine.js for ThumbGate.
13
+ * Ported from Subway_RN_Demo/scripts/context-engine.js for thumbgate.
13
14
  * PATH: PROJECT_ROOT = path.join(__dirname, '..') — 1 level up from scripts/
14
15
  */
15
16
 
@@ -39,24 +39,10 @@ const PROFILE_DEFS = {
39
39
  },
40
40
  };
41
41
 
42
- const ENV_ALIASES = {
43
- THUMBGATE_API_KEY: ['THUMBGATE_API_KEY', 'RLHF_API_KEY'],
44
- THUMBGATE_API_KEY_ROTATED_AT: ['THUMBGATE_API_KEY_ROTATED_AT', 'RLHF_API_KEY_ROTATED_AT'],
45
- THUMBGATE_PUBLIC_APP_ORIGIN: ['THUMBGATE_PUBLIC_APP_ORIGIN', 'RLHF_PUBLIC_APP_ORIGIN'],
46
- THUMBGATE_BILLING_API_BASE_URL: [
47
- 'THUMBGATE_BILLING_API_BASE_URL',
48
- 'RLHF_BILLING_API_BASE_URL',
49
- 'THUMBGATE_CANONICAL_API_BASE_URL',
50
- ],
51
- };
52
-
53
42
  function resolveEnvValue(name, env = process.env) {
54
- const aliases = ENV_ALIASES[name] || [name];
55
- for (const alias of aliases) {
56
- const value = String(env[alias] || '').trim();
57
- if (value) {
58
- return value;
59
- }
43
+ const value = String(env[name] || '').trim();
44
+ if (value) {
45
+ return value;
60
46
  }
61
47
 
62
48
  if (name === 'THUMBGATE_PUBLIC_APP_ORIGIN') {
@@ -13,8 +13,8 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const os = require('os');
17
16
  const { getEffectiveSetting } = require('./evolution-state');
17
+ const { resolveFeedbackDir } = require('./feedback-paths');
18
18
 
19
19
  const DPO_BETA = 0.1;
20
20
 
@@ -154,11 +154,8 @@ function applyDpoAdjustments(modelPath, pairs) {
154
154
  */
155
155
  function run(opts) {
156
156
  const options = opts || {};
157
- const feedbackDir = options.feedbackDir ||
158
- process.env.THUMBGATE_FEEDBACK_DIR ||
159
- path.join(os.homedir(), '.claude', 'memory', 'feedback');
160
- const modelPath = options.modelPath ||
161
- path.join(process.cwd(), '.claude', 'memory', 'feedback', 'feedback_model.json');
157
+ const feedbackDir = options.feedbackDir || resolveFeedbackDir();
158
+ const modelPath = options.modelPath || path.join(feedbackDir, 'feedback_model.json');
162
159
 
163
160
  const pairs = buildPreferencePairs(feedbackDir);
164
161