thumbgate 0.9.9 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  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 +2 -2
  7. package/adapters/amp/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +2 -2
  9. package/adapters/claude/.mcp.json +3 -3
  10. package/adapters/codex/config.toml +4 -4
  11. package/adapters/gemini/function-declarations.json +1 -1
  12. package/adapters/mcp/server-stdio.js +66 -6
  13. package/adapters/opencode/opencode.json +4 -2
  14. package/bin/cli.js +188 -39
  15. package/config/e2e-critical-flows.json +4 -0
  16. package/config/gates/default.json +74 -2
  17. package/config/github-about.json +1 -1
  18. package/config/mcp-allowlists.json +33 -6
  19. package/config/skill-packs/react-testing.json +1 -1
  20. package/config/tessl-tiles.json +3 -3
  21. package/openapi/openapi.yaml +2 -2
  22. package/package.json +23 -9
  23. package/plugins/amp-skill/INSTALL.md +3 -2
  24. package/plugins/amp-skill/SKILL.md +1 -0
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +5 -3
  27. package/plugins/claude-codex-bridge/README.md +1 -1
  28. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +1 -1
  29. package/plugins/claude-skill/INSTALL.md +4 -3
  30. package/plugins/claude-skill/SKILL.md +1 -1
  31. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  32. package/plugins/codex-profile/.mcp.json +5 -3
  33. package/plugins/codex-profile/INSTALL.md +2 -2
  34. package/plugins/codex-profile/README.md +1 -1
  35. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  36. package/plugins/cursor-marketplace/README.md +5 -5
  37. package/plugins/cursor-marketplace/mcp.json +4 -2
  38. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +1 -1
  39. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  40. package/plugins/gemini-extension/INSTALL.md +4 -4
  41. package/plugins/opencode-profile/INSTALL.md +5 -5
  42. package/public/dashboard.html +15 -8
  43. package/public/index.html +134 -375
  44. package/public/js/buyer-intent.js +252 -0
  45. package/public/pro.html +1085 -0
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/adk-consolidator.js +17 -5
  48. package/scripts/agent-readiness.js +3 -1
  49. package/scripts/agent-security-hardening.js +4 -4
  50. package/scripts/auto-promote-gates.js +8 -0
  51. package/scripts/auto-wire-hooks.js +105 -21
  52. package/scripts/billing.js +111 -7
  53. package/scripts/build-metadata.js +14 -0
  54. package/scripts/check-congruence.js +1 -1
  55. package/scripts/context-engine.js +2 -1
  56. package/scripts/daemon-manager.js +2 -2
  57. package/scripts/dashboard.js +2 -2
  58. package/scripts/data-governance.js +1 -1
  59. package/scripts/deploy-gcp.sh +1 -1
  60. package/scripts/deploy-policy.js +22 -4
  61. package/scripts/dispatch-brief.js +1 -1
  62. package/scripts/ensure-repo-bootstrap.js +1 -1
  63. package/scripts/feedback-attribution.js +22 -10
  64. package/scripts/feedback-fallback.js +3 -2
  65. package/scripts/feedback-inbox-read.js +1 -1
  66. package/scripts/feedback-loop.js +41 -3
  67. package/scripts/feedback-paths.js +8 -8
  68. package/scripts/feedback-schema.js +1 -1
  69. package/scripts/feedback-to-memory.js +2 -2
  70. package/scripts/filesystem-search.js +2 -2
  71. package/scripts/gates-engine.js +765 -34
  72. package/scripts/generate-paperbanana-diagrams.sh +3 -3
  73. package/scripts/github-about.js +1 -1
  74. package/scripts/gtm-revenue-loop.js +20 -1
  75. package/scripts/hook-runtime.js +89 -0
  76. package/scripts/hook-stop-self-score.sh +3 -3
  77. package/scripts/hook-thumbgate-cache-updater.js +98 -37
  78. package/scripts/hosted-config.js +12 -10
  79. package/scripts/hybrid-feedback-context.js +54 -13
  80. package/scripts/install-mcp.js +14 -1
  81. package/scripts/intent-router.js +1 -1
  82. package/scripts/internal-agent-bootstrap.js +1 -1
  83. package/scripts/lesson-inference.js +6 -1
  84. package/scripts/license.js +54 -16
  85. package/scripts/mcp-config.js +69 -7
  86. package/scripts/memory-migration.js +1 -1
  87. package/scripts/money-watcher.js +166 -16
  88. package/scripts/operational-integrity.js +480 -0
  89. package/scripts/optimize-context.js +1 -1
  90. package/scripts/perplexity-marketing.js +1 -1
  91. package/scripts/post-everywhere.js +7 -12
  92. package/scripts/post-to-x.js +1 -1
  93. package/scripts/pr-manager.js +14 -11
  94. package/scripts/problem-detail.js +10 -10
  95. package/scripts/profile-router.js +2 -0
  96. package/scripts/prompt-dlp.js +1 -0
  97. package/scripts/prove-adapters.js +6 -6
  98. package/scripts/prove-automation.js +1 -1
  99. package/scripts/prove-autoresearch.js +1 -1
  100. package/scripts/prove-claim-verification.js +3 -3
  101. package/scripts/prove-data-pipeline.js +5 -5
  102. package/scripts/prove-data-quality.js +1 -1
  103. package/scripts/prove-evolution.js +7 -7
  104. package/scripts/prove-harnesses.js +2 -2
  105. package/scripts/prove-lancedb.js +2 -2
  106. package/scripts/prove-local-intelligence.js +1 -1
  107. package/scripts/prove-loop-closure.js +1 -1
  108. package/scripts/prove-predictive-insights.js +2 -2
  109. package/scripts/prove-runtime.js +6 -6
  110. package/scripts/prove-seo-gsd.js +1 -1
  111. package/scripts/prove-settings.js +4 -4
  112. package/scripts/prove-subway-upgrades.js +1 -1
  113. package/scripts/prove-tessl.js +2 -2
  114. package/scripts/prove-xmemory.js +2 -2
  115. package/scripts/publish-decision.js +10 -0
  116. package/scripts/published-cli.js +34 -0
  117. package/scripts/rate-limiter.js +2 -2
  118. package/scripts/reddit-monitor-cron.sh +2 -2
  119. package/scripts/reminder-engine.js +1 -1
  120. package/scripts/schedule-manager.js +3 -3
  121. package/scripts/self-healing-check.js +1 -1
  122. package/scripts/shieldcortex-memory-firewall-runner.mjs +1 -1
  123. package/scripts/skill-quality-tracker.js +1 -1
  124. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  125. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  126. package/scripts/social-analytics/engagement-audit.js +202 -0
  127. package/scripts/social-analytics/generate-instagram-card.js +1 -1
  128. package/scripts/social-analytics/instagram-thumbgate-post.js +5 -1
  129. package/scripts/social-analytics/install-growth-automation.js +114 -0
  130. package/scripts/social-analytics/publish-instagram-thumbgate.js +8 -2
  131. package/scripts/social-analytics/publish-thumbgate-launch.js +1 -1
  132. package/scripts/social-analytics/publishers/reddit.js +7 -12
  133. package/scripts/social-analytics/publishers/zernio.js +19 -0
  134. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  135. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  136. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  137. package/scripts/social-pipeline.js +2 -2
  138. package/scripts/social-post-hourly.js +185 -0
  139. package/scripts/social-quality-gate.js +119 -3
  140. package/scripts/social-reply-monitor.js +150 -34
  141. package/scripts/statusline-cache-path.js +27 -0
  142. package/scripts/statusline-meta.js +22 -0
  143. package/scripts/statusline.sh +24 -32
  144. package/scripts/sync-version.js +24 -12
  145. package/scripts/telemetry-analytics.js +4 -4
  146. package/scripts/tessl-export.js +1 -1
  147. package/scripts/test-coverage.js +20 -13
  148. package/scripts/thumbgate-search.js +2 -2
  149. package/scripts/tool-registry.js +98 -1
  150. package/scripts/train_from_feedback.py +1 -1
  151. package/scripts/user-profile.js +4 -4
  152. package/scripts/validate-feedback.js +1 -1
  153. package/scripts/vector-store.js +1 -1
  154. package/scripts/verification-loop.js +1 -1
  155. package/scripts/verify-run.js +1 -1
  156. package/scripts/weekly-auto-post.js +1 -1
  157. package/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  158. package/src/api/server.js +291 -41
  159. package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
  160. package/scripts/social-analytics/db/social-analytics.db +0 -0
@@ -3,42 +3,71 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const crypto = require('crypto');
5
5
 
6
- const LICENSE_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thumbgate', 'license.json');
7
- const VALID_PREFIXES = ['rlhf_', 'tg_pro_', 'tg_'];
6
+ const VALID_PREFIXES = ['tg_pro_', 'tg_'];
7
+ const LEGACY_COMPATIBLE_KEY = /^[a-z]{4,16}_[a-f0-9]{24,}$/i;
8
+
9
+ function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || '.') {
10
+ return path.join(homeDir, '.thumbgate', 'license.json');
11
+ }
12
+
13
+ const LICENSE_PATH = getLicensePath();
8
14
 
9
15
  function isValidKey(key) {
10
- return key && VALID_PREFIXES.some((p) => key.startsWith(p));
16
+ return Boolean(
17
+ key
18
+ && (
19
+ VALID_PREFIXES.some((p) => key.startsWith(p))
20
+ || LEGACY_COMPATIBLE_KEY.test(key)
21
+ )
22
+ );
11
23
  }
12
24
 
13
- function verifyLicense() {
14
- const envKey = process.env.THUMBGATE_API_KEY || process.env.THUMBGATE_PRO_KEY;
25
+ function verifyLicense(options = {}) {
26
+ const envKey = [
27
+ process.env.THUMBGATE_API_KEY,
28
+ process.env.THUMBGATE_PRO_KEY,
29
+ ...Object.entries(process.env)
30
+ .filter(([name]) => /(?:_API_KEY|_PRO_KEY)$/.test(name))
31
+ .map(([, value]) => value),
32
+ ].find((value) => isValidKey(value));
15
33
  if (isValidKey(envKey)) {
16
34
  return { valid: true, source: 'env', key: envKey };
17
35
  }
36
+
37
+ const licensePath = getLicensePath(options.homeDir);
18
38
  try {
19
- if (fs.existsSync(LICENSE_PATH)) {
20
- const data = JSON.parse(fs.readFileSync(LICENSE_PATH, 'utf8'));
39
+ if (fs.existsSync(licensePath)) {
40
+ const data = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
21
41
  if (isValidKey(data.key)) {
22
- return { valid: true, source: 'file', key: data.key, activatedAt: data.activatedAt };
42
+ return {
43
+ valid: true,
44
+ source: 'file',
45
+ key: data.key,
46
+ activatedAt: data.activatedAt,
47
+ path: licensePath,
48
+ };
23
49
  }
24
50
  }
25
51
  } catch (_) {}
52
+
26
53
  return { valid: false, source: null };
27
54
  }
28
55
 
29
- function isProLicensed() {
30
- return verifyLicense().valid;
56
+ function isProLicensed(options) {
57
+ return verifyLicense(options).valid;
31
58
  }
32
59
 
33
- function activateLicense(key) {
60
+ function activateLicense(key, options = {}) {
34
61
  if (!isValidKey(key)) {
35
- return { success: false, error: 'Invalid key format. Expected rlhf_... or tg_pro_...' };
62
+ return { success: false, error: 'Invalid key format. Expected tg_... or tg_pro_...' };
36
63
  }
37
- const dir = path.dirname(LICENSE_PATH);
64
+
65
+ const licensePath = getLicensePath(options.homeDir);
66
+ const dir = path.dirname(licensePath);
38
67
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
39
68
  const data = { key, activatedAt: new Date().toISOString(), version: require('../package.json').version };
40
- fs.writeFileSync(LICENSE_PATH, JSON.stringify(data, null, 2));
41
- return { success: true, path: LICENSE_PATH };
69
+ fs.writeFileSync(licensePath, JSON.stringify(data, null, 2));
70
+ return { success: true, path: licensePath };
42
71
  }
43
72
 
44
73
  function generateLicenseKey(email) {
@@ -47,4 +76,13 @@ function generateLicenseKey(email) {
47
76
  return `tg_pro_${hash}`;
48
77
  }
49
78
 
50
- module.exports = { verifyLicense, isProLicensed, activateLicense, generateLicenseKey, isValidKey, VALID_PREFIXES, LICENSE_PATH };
79
+ module.exports = {
80
+ verifyLicense,
81
+ isProLicensed,
82
+ activateLicense,
83
+ generateLicenseKey,
84
+ isValidKey,
85
+ VALID_PREFIXES,
86
+ LICENSE_PATH,
87
+ getLicensePath,
88
+ };
@@ -3,6 +3,9 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { execFileSync } = require('child_process');
6
+ const { publishedCliArgs, runPublishedCliHelp } = require('./published-cli');
7
+ const DEFAULT_PKG_ROOT = path.join(__dirname, '..');
8
+ const cliAvailabilityCache = new Map();
6
9
 
7
10
  function isSourceCheckout(pkgRoot) {
8
11
  return fs.existsSync(path.join(pkgRoot, '.git'));
@@ -17,17 +20,40 @@ function parseWorktreePaths(raw) {
17
20
  }
18
21
 
19
22
  function resolveStableSourceRoot(pkgRoot) {
20
- if (!isSourceCheckout(pkgRoot)) {
23
+ const effectivePkgRoot =
24
+ typeof pkgRoot === 'string' && pkgRoot.trim() ? pkgRoot : DEFAULT_PKG_ROOT;
25
+
26
+ if (!isSourceCheckout(effectivePkgRoot)) {
21
27
  return null;
22
28
  }
23
29
 
30
+ let preferredBasenames = [];
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(path.join(effectivePkgRoot, 'package.json'), 'utf8'));
33
+ const packageName = String(pkg && pkg.name || '').trim().toLowerCase();
34
+ if (packageName) {
35
+ preferredBasenames.push(packageName);
36
+ preferredBasenames.push(packageName.replace(/[^a-z0-9]+/g, ''));
37
+ }
38
+ } catch (_) {
39
+ preferredBasenames = [];
40
+ }
41
+
24
42
  try {
25
- const output = execFileSync('git', ['-C', pkgRoot, 'worktree', 'list', '--porcelain'], {
43
+ const output = execFileSync('git', ['-C', effectivePkgRoot, 'worktree', 'list', '--porcelain'], {
26
44
  encoding: 'utf8',
27
45
  stdio: ['ignore', 'pipe', 'ignore'],
28
46
  });
29
47
  const worktreePaths = parseWorktreePaths(output);
30
48
 
49
+ for (const worktreePath of worktreePaths) {
50
+ const baseName = path.basename(worktreePath).toLowerCase();
51
+ const normalizedBaseName = baseName.replace(/[^a-z0-9]+/g, '');
52
+ if (preferredBasenames.includes(baseName) || preferredBasenames.includes(normalizedBaseName)) {
53
+ return worktreePath;
54
+ }
55
+ }
56
+
31
57
  for (const worktreePath of worktreePaths) {
32
58
  const gitPath = path.join(worktreePath, '.git');
33
59
  if (!fs.existsSync(gitPath)) {
@@ -38,10 +64,10 @@ function resolveStableSourceRoot(pkgRoot) {
38
64
  }
39
65
  }
40
66
  } catch (_) {
41
- return pkgRoot;
67
+ return effectivePkgRoot;
42
68
  }
43
69
 
44
- return pkgRoot;
70
+ return effectivePkgRoot;
45
71
  }
46
72
 
47
73
  function resolveGitCommonDir(dirPath) {
@@ -76,7 +102,7 @@ function resolveLocalServerPath(pkgRoot, scope = 'project') {
76
102
  function portableMcpEntry(pkgVersion) {
77
103
  return {
78
104
  command: 'npx',
79
- args: ['-y', `thumbgate@${pkgVersion}`, 'serve'],
105
+ args: publishedCliArgs(pkgVersion, ['serve']),
80
106
  };
81
107
  }
82
108
 
@@ -90,7 +116,7 @@ function localMcpEntry(pkgRoot, scope = 'project') {
90
116
  const publicationCache = new Map();
91
117
 
92
118
  function publishedVersionOverride() {
93
- const override = String(process.env.MCP_MEMORY_GATEWAY_PUBLISH_STATE || '').trim().toLowerCase();
119
+ const override = String(process.env.THUMBGATE_PUBLISH_STATE || '').trim().toLowerCase();
94
120
  if (override === 'published') {
95
121
  return true;
96
122
  }
@@ -125,17 +151,53 @@ function isVersionPublished(pkgVersion) {
125
151
  return published;
126
152
  }
127
153
 
154
+ function publishedCliOverride() {
155
+ const override = String(process.env.THUMBGATE_PUBLISHED_CLI_STATE || '').trim().toLowerCase();
156
+ if (override === 'available') {
157
+ return true;
158
+ }
159
+ if (override === 'unavailable') {
160
+ return false;
161
+ }
162
+ return null;
163
+ }
164
+
165
+ function publishedCliAvailable(pkgVersion) {
166
+ if (!isVersionPublished(pkgVersion)) {
167
+ return false;
168
+ }
169
+ const override = publishedCliOverride();
170
+ if (override !== null) {
171
+ return override;
172
+ }
173
+ if (cliAvailabilityCache.has(pkgVersion)) {
174
+ return cliAvailabilityCache.get(pkgVersion);
175
+ }
176
+
177
+ let available = false;
178
+ try {
179
+ runPublishedCliHelp(pkgVersion, { timeout: 8000 });
180
+ available = true;
181
+ } catch (_) {
182
+ available = false;
183
+ }
184
+
185
+ cliAvailabilityCache.set(pkgVersion, available);
186
+ return available;
187
+ }
188
+
128
189
  function resolveMcpEntry({ pkgRoot, pkgVersion, scope = 'project', targetDir = pkgRoot }) {
129
190
  if (!isSourceCheckout(pkgRoot)) {
130
191
  return portableMcpEntry(pkgVersion);
131
192
  }
132
- if (scope === 'project' && !isSameCheckoutFamily(pkgRoot, targetDir) && isVersionPublished(pkgVersion)) {
193
+ if (scope === 'project' && !isSameCheckoutFamily(pkgRoot, targetDir) && publishedCliAvailable(pkgVersion)) {
133
194
  return portableMcpEntry(pkgVersion);
134
195
  }
135
196
  return localMcpEntry(pkgRoot, scope);
136
197
  }
137
198
 
138
199
  module.exports = {
200
+ publishedCliAvailable,
139
201
  isVersionPublished,
140
202
  isSourceCheckout,
141
203
  isSameCheckoutFamily,
@@ -253,7 +253,7 @@ function migrateAllMemory() {
253
253
  */
254
254
  function generateComparisonData() {
255
255
  const health = checkMemoryHealth();
256
- const feedbackDir = process.env.THUMBGATE_FEEDBACK_DIR || path.join(process.cwd(), '.rlhf');
256
+ const feedbackDir = process.env.THUMBGATE_FEEDBACK_DIR || path.join(process.cwd(), '.thumbgate');
257
257
  let lessonCount = 0;
258
258
  const lessonsPath = path.join(feedbackDir, 'lessons-index.jsonl');
259
259
  if (fs.existsSync(lessonsPath)) {
@@ -6,10 +6,19 @@
6
6
 
7
7
  'use strict';
8
8
 
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
9
11
  const { getOperationalBillingSummary } = require('./operational-summary');
10
12
 
11
- function getCommercialRevenueSnapshot(summary) {
12
- const revenue = summary.revenue || {};
13
+ const DEFAULT_STATE_PATH = path.resolve(__dirname, '..', '.thumbgate', 'commercial-watch-state.json');
14
+ const DEFAULT_ALERT_LOG_PATH = path.resolve(__dirname, '..', '.thumbgate', 'commercial-alerts.jsonl');
15
+
16
+ function ensureParentDir(filePath) {
17
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
18
+ }
19
+
20
+ function getCommercialRevenueSnapshot(summary = {}) {
21
+ const revenue = summary && typeof summary === 'object' ? summary.revenue || {} : {};
13
22
  return {
14
23
  paidOrders: revenue.paidOrders || 0,
15
24
  bookedRevenueCents: revenue.bookedRevenueCents || 0,
@@ -18,10 +27,86 @@ function getCommercialRevenueSnapshot(summary) {
18
27
  };
19
28
  }
20
29
 
21
- async function watchMoney(intervalMs = 10000) {
30
+ function readSnapshotState(statePath = DEFAULT_STATE_PATH) {
31
+ try {
32
+ if (!fs.existsSync(statePath)) {
33
+ return null;
34
+ }
35
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function writeSnapshotState(snapshot, statePath = DEFAULT_STATE_PATH) {
42
+ ensureParentDir(statePath);
43
+ fs.writeFileSync(statePath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
44
+ return statePath;
45
+ }
46
+
47
+ function buildCommercialAlert(previousSnapshot = {}, currentSnapshot = {}, meta = {}) {
48
+ const newPaidOrders = (currentSnapshot.paidOrders || 0) - (previousSnapshot.paidOrders || 0);
49
+ const newBookedRevenueCents = (currentSnapshot.bookedRevenueCents || 0) - (previousSnapshot.bookedRevenueCents || 0);
50
+
51
+ if (newPaidOrders <= 0 && newBookedRevenueCents <= 0) {
52
+ return null;
53
+ }
54
+
55
+ return {
56
+ detectedAt: new Date().toISOString(),
57
+ source: meta.source || null,
58
+ fallbackReason: meta.fallbackReason || null,
59
+ newPaidOrders,
60
+ newBookedRevenueCents,
61
+ latestPaidAt: currentSnapshot.latestPaidAt || null,
62
+ latestPaidOrder: currentSnapshot.latestPaidOrder || null,
63
+ paidOrders: currentSnapshot.paidOrders || 0,
64
+ bookedRevenueCents: currentSnapshot.bookedRevenueCents || 0,
65
+ };
66
+ }
67
+
68
+ function recordCommercialAlert(alert, alertLogPath = DEFAULT_ALERT_LOG_PATH) {
69
+ ensureParentDir(alertLogPath);
70
+ fs.appendFileSync(alertLogPath, `${JSON.stringify(alert)}\n`, 'utf8');
71
+ return alertLogPath;
72
+ }
73
+
74
+ async function checkForCommercialChange(options = {}) {
75
+ const statePath = options.statePath || DEFAULT_STATE_PATH;
76
+ const alertLogPath = options.alertLogPath || DEFAULT_ALERT_LOG_PATH;
77
+ const previousSnapshot = options.previousSnapshot || readSnapshotState(statePath) || getCommercialRevenueSnapshot();
78
+ const summaryResolver = options.getSummary || getOperationalBillingSummary;
79
+ const { source, summary, fallbackReason } = await summaryResolver();
80
+ const currentSnapshot = getCommercialRevenueSnapshot(summary);
81
+ const alert = buildCommercialAlert(previousSnapshot, currentSnapshot, {
82
+ source,
83
+ fallbackReason,
84
+ });
85
+
86
+ writeSnapshotState(currentSnapshot, statePath);
87
+ if (alert) {
88
+ recordCommercialAlert(alert, alertLogPath);
89
+ }
90
+
91
+ return {
92
+ changed: Boolean(alert),
93
+ alert,
94
+ previousSnapshot,
95
+ currentSnapshot,
96
+ source,
97
+ fallbackReason: fallbackReason || null,
98
+ statePath,
99
+ alertLogPath,
100
+ };
101
+ }
102
+
103
+ async function watchMoney(intervalMs = 10000, options = {}) {
22
104
  console.log('šŸ‘€ Money Watcher activated. Polling billing summary for commercial changes...');
23
105
  const initialState = await getOperationalBillingSummary();
24
- let initialSnapshot = getCommercialRevenueSnapshot(initialState.summary);
106
+ let initialSnapshot = options.initialSnapshot
107
+ || readSnapshotState(options.statePath || DEFAULT_STATE_PATH)
108
+ || getCommercialRevenueSnapshot(initialState.summary);
109
+ writeSnapshotState(initialSnapshot, options.statePath || DEFAULT_STATE_PATH);
25
110
  let polling = false;
26
111
 
27
112
  return setInterval(async () => {
@@ -30,21 +115,18 @@ async function watchMoney(intervalMs = 10000) {
30
115
  try {
31
116
  const { source, summary, fallbackReason } = await getOperationalBillingSummary();
32
117
  const currentSnapshot = getCommercialRevenueSnapshot(summary);
118
+ const alert = buildCommercialAlert(initialSnapshot, currentSnapshot, {
119
+ source,
120
+ fallbackReason,
121
+ });
122
+ writeSnapshotState(currentSnapshot, options.statePath || DEFAULT_STATE_PATH);
33
123
 
34
- const newPaidOrders = currentSnapshot.paidOrders - initialSnapshot.paidOrders;
35
- const newBookedRevenue = currentSnapshot.bookedRevenueCents - initialSnapshot.bookedRevenueCents;
36
-
37
- if (newPaidOrders > 0 || newBookedRevenue > 0) {
124
+ if (alert) {
125
+ recordCommercialAlert(alert, options.alertLogPath || DEFAULT_ALERT_LOG_PATH);
38
126
  console.log('\n🚨🚨🚨 COMMERCIAL ALERT: NET-NEW PAID ACTIVITY DETECTED! 🚨🚨🚨');
39
127
  console.log('Operational billing summary:');
40
128
  console.log(JSON.stringify({
41
- source,
42
- fallbackReason,
43
- newPaidOrders,
44
- newBookedRevenueCents: newBookedRevenue,
45
- latestPaidAt: currentSnapshot.latestPaidAt,
46
- latestPaidOrder: currentSnapshot.latestPaidOrder,
47
- bookedRevenueCents: currentSnapshot.bookedRevenueCents,
129
+ ...alert,
48
130
  activeKeys: summary.keys.active,
49
131
  totalUsage: summary.keys.totalUsage,
50
132
  }, null, 2));
@@ -58,14 +140,82 @@ async function watchMoney(intervalMs = 10000) {
58
140
  }, intervalMs);
59
141
  }
60
142
 
143
+ function parseArgs(argv = []) {
144
+ const options = {
145
+ once: false,
146
+ intervalMs: 10000,
147
+ statePath: DEFAULT_STATE_PATH,
148
+ alertLogPath: DEFAULT_ALERT_LOG_PATH,
149
+ };
150
+
151
+ for (let index = 0; index < argv.length; index += 1) {
152
+ const arg = String(argv[index] || '').trim();
153
+
154
+ if (arg === '--once') {
155
+ options.once = true;
156
+ continue;
157
+ }
158
+
159
+ if (arg === '--interval-ms' && argv[index + 1]) {
160
+ options.intervalMs = Number.parseInt(argv[index + 1], 10) || options.intervalMs;
161
+ index += 1;
162
+ continue;
163
+ }
164
+
165
+ if (arg.startsWith('--interval-ms=')) {
166
+ options.intervalMs = Number.parseInt(arg.split('=').slice(1).join('='), 10) || options.intervalMs;
167
+ continue;
168
+ }
169
+
170
+ if (arg === '--state-path' && argv[index + 1]) {
171
+ options.statePath = path.resolve(String(argv[index + 1]));
172
+ index += 1;
173
+ continue;
174
+ }
175
+
176
+ if (arg.startsWith('--state-path=')) {
177
+ options.statePath = path.resolve(arg.split('=').slice(1).join('='));
178
+ continue;
179
+ }
180
+
181
+ if (arg === '--alert-log-path' && argv[index + 1]) {
182
+ options.alertLogPath = path.resolve(String(argv[index + 1]));
183
+ index += 1;
184
+ continue;
185
+ }
186
+
187
+ if (arg.startsWith('--alert-log-path=')) {
188
+ options.alertLogPath = path.resolve(arg.split('=').slice(1).join('='));
189
+ }
190
+ }
191
+
192
+ return options;
193
+ }
194
+
61
195
  if (require.main === module) {
62
- watchMoney().catch((err) => {
196
+ const options = parseArgs(process.argv.slice(2));
197
+ const runner = options.once
198
+ ? checkForCommercialChange(options).then((result) => {
199
+ console.log(JSON.stringify(result, null, 2));
200
+ return result;
201
+ })
202
+ : watchMoney(options.intervalMs, options);
203
+
204
+ runner.catch((err) => {
63
205
  console.error(err && err.message ? err.message : err);
64
206
  process.exit(1);
65
207
  });
66
208
  }
67
209
 
68
210
  module.exports = {
211
+ DEFAULT_ALERT_LOG_PATH,
212
+ DEFAULT_STATE_PATH,
213
+ buildCommercialAlert,
214
+ checkForCommercialChange,
69
215
  getCommercialRevenueSnapshot,
216
+ parseArgs,
217
+ readSnapshotState,
218
+ recordCommercialAlert,
70
219
  watchMoney,
220
+ writeSnapshotState,
71
221
  };