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.
- package/.claude-plugin/README.md +4 -4
- package/.claude-plugin/marketplace.json +4 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +115 -312
- package/adapters/README.md +2 -2
- package/adapters/amp/skills/{rlhf-feedback ā thumbgate-feedback}/SKILL.md +1 -1
- package/adapters/chatgpt/openapi.yaml +2 -2
- package/adapters/claude/.mcp.json +3 -3
- package/adapters/codex/config.toml +4 -4
- package/adapters/gemini/function-declarations.json +1 -1
- package/adapters/mcp/server-stdio.js +66 -6
- package/adapters/opencode/opencode.json +4 -2
- package/bin/cli.js +188 -39
- package/config/e2e-critical-flows.json +4 -0
- package/config/gates/default.json +74 -2
- package/config/github-about.json +1 -1
- package/config/mcp-allowlists.json +33 -6
- package/config/skill-packs/react-testing.json +1 -1
- package/config/tessl-tiles.json +3 -3
- package/openapi/openapi.yaml +2 -2
- package/package.json +23 -9
- package/plugins/amp-skill/INSTALL.md +3 -2
- package/plugins/amp-skill/SKILL.md +1 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +5 -3
- package/plugins/claude-codex-bridge/README.md +1 -1
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +1 -1
- package/plugins/claude-skill/INSTALL.md +4 -3
- package/plugins/claude-skill/SKILL.md +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +5 -3
- package/plugins/codex-profile/INSTALL.md +2 -2
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +5 -5
- package/plugins/cursor-marketplace/mcp.json +4 -2
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +1 -1
- package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
- package/plugins/gemini-extension/INSTALL.md +4 -4
- package/plugins/opencode-profile/INSTALL.md +5 -5
- package/public/dashboard.html +15 -8
- package/public/index.html +134 -375
- package/public/js/buyer-intent.js +252 -0
- package/public/pro.html +1085 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/adk-consolidator.js +17 -5
- package/scripts/agent-readiness.js +3 -1
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/auto-promote-gates.js +8 -0
- package/scripts/auto-wire-hooks.js +105 -21
- package/scripts/billing.js +111 -7
- package/scripts/build-metadata.js +14 -0
- package/scripts/check-congruence.js +1 -1
- package/scripts/context-engine.js +2 -1
- package/scripts/daemon-manager.js +2 -2
- package/scripts/dashboard.js +2 -2
- package/scripts/data-governance.js +1 -1
- package/scripts/deploy-gcp.sh +1 -1
- package/scripts/deploy-policy.js +22 -4
- package/scripts/dispatch-brief.js +1 -1
- package/scripts/ensure-repo-bootstrap.js +1 -1
- package/scripts/feedback-attribution.js +22 -10
- package/scripts/feedback-fallback.js +3 -2
- package/scripts/feedback-inbox-read.js +1 -1
- package/scripts/feedback-loop.js +41 -3
- package/scripts/feedback-paths.js +8 -8
- package/scripts/feedback-schema.js +1 -1
- package/scripts/feedback-to-memory.js +2 -2
- package/scripts/filesystem-search.js +2 -2
- package/scripts/gates-engine.js +765 -34
- package/scripts/generate-paperbanana-diagrams.sh +3 -3
- package/scripts/github-about.js +1 -1
- package/scripts/gtm-revenue-loop.js +20 -1
- package/scripts/hook-runtime.js +89 -0
- package/scripts/hook-stop-self-score.sh +3 -3
- package/scripts/hook-thumbgate-cache-updater.js +98 -37
- package/scripts/hosted-config.js +12 -10
- package/scripts/hybrid-feedback-context.js +54 -13
- package/scripts/install-mcp.js +14 -1
- package/scripts/intent-router.js +1 -1
- package/scripts/internal-agent-bootstrap.js +1 -1
- package/scripts/lesson-inference.js +6 -1
- package/scripts/license.js +54 -16
- package/scripts/mcp-config.js +69 -7
- package/scripts/memory-migration.js +1 -1
- package/scripts/money-watcher.js +166 -16
- package/scripts/operational-integrity.js +480 -0
- package/scripts/optimize-context.js +1 -1
- package/scripts/perplexity-marketing.js +1 -1
- package/scripts/post-everywhere.js +7 -12
- package/scripts/post-to-x.js +1 -1
- package/scripts/pr-manager.js +14 -11
- package/scripts/problem-detail.js +10 -10
- package/scripts/profile-router.js +2 -0
- package/scripts/prompt-dlp.js +1 -0
- package/scripts/prove-adapters.js +6 -6
- package/scripts/prove-automation.js +1 -1
- package/scripts/prove-autoresearch.js +1 -1
- package/scripts/prove-claim-verification.js +3 -3
- package/scripts/prove-data-pipeline.js +5 -5
- package/scripts/prove-data-quality.js +1 -1
- package/scripts/prove-evolution.js +7 -7
- package/scripts/prove-harnesses.js +2 -2
- package/scripts/prove-lancedb.js +2 -2
- package/scripts/prove-local-intelligence.js +1 -1
- package/scripts/prove-loop-closure.js +1 -1
- package/scripts/prove-predictive-insights.js +2 -2
- package/scripts/prove-runtime.js +6 -6
- package/scripts/prove-seo-gsd.js +1 -1
- package/scripts/prove-settings.js +4 -4
- package/scripts/prove-subway-upgrades.js +1 -1
- package/scripts/prove-tessl.js +2 -2
- package/scripts/prove-xmemory.js +2 -2
- package/scripts/publish-decision.js +10 -0
- package/scripts/published-cli.js +34 -0
- package/scripts/rate-limiter.js +2 -2
- package/scripts/reddit-monitor-cron.sh +2 -2
- package/scripts/reminder-engine.js +1 -1
- package/scripts/schedule-manager.js +3 -3
- package/scripts/self-healing-check.js +1 -1
- package/scripts/shieldcortex-memory-firewall-runner.mjs +1 -1
- package/scripts/skill-quality-tracker.js +1 -1
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
- package/scripts/social-analytics/engagement-audit.js +202 -0
- package/scripts/social-analytics/generate-instagram-card.js +1 -1
- package/scripts/social-analytics/instagram-thumbgate-post.js +5 -1
- package/scripts/social-analytics/install-growth-automation.js +114 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +8 -2
- package/scripts/social-analytics/publish-thumbgate-launch.js +1 -1
- package/scripts/social-analytics/publishers/reddit.js +7 -12
- package/scripts/social-analytics/publishers/zernio.js +19 -0
- package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
- package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
- package/scripts/social-analytics/sync-launch-assets.js +185 -0
- package/scripts/social-pipeline.js +2 -2
- package/scripts/social-post-hourly.js +185 -0
- package/scripts/social-quality-gate.js +119 -3
- package/scripts/social-reply-monitor.js +150 -34
- package/scripts/statusline-cache-path.js +27 -0
- package/scripts/statusline-meta.js +22 -0
- package/scripts/statusline.sh +24 -32
- package/scripts/sync-version.js +24 -12
- package/scripts/telemetry-analytics.js +4 -4
- package/scripts/tessl-export.js +1 -1
- package/scripts/test-coverage.js +20 -13
- package/scripts/thumbgate-search.js +2 -2
- package/scripts/tool-registry.js +98 -1
- package/scripts/train_from_feedback.py +1 -1
- package/scripts/user-profile.js +4 -4
- package/scripts/validate-feedback.js +1 -1
- package/scripts/vector-store.js +1 -1
- package/scripts/verification-loop.js +1 -1
- package/scripts/verify-run.js +1 -1
- package/scripts/weekly-auto-post.js +1 -1
- package/skills/{rlhf-feedback ā thumbgate-feedback}/SKILL.md +1 -1
- package/src/api/server.js +291 -41
- package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
- package/scripts/social-analytics/db/social-analytics.db +0 -0
package/scripts/license.js
CHANGED
|
@@ -3,42 +3,71 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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
|
|
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 =
|
|
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(
|
|
20
|
-
const data = JSON.parse(fs.readFileSync(
|
|
39
|
+
if (fs.existsSync(licensePath)) {
|
|
40
|
+
const data = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
21
41
|
if (isValidKey(data.key)) {
|
|
22
|
-
return {
|
|
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
|
|
62
|
+
return { success: false, error: 'Invalid key format. Expected tg_... or tg_pro_...' };
|
|
36
63
|
}
|
|
37
|
-
|
|
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(
|
|
41
|
-
return { success: true, 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 = {
|
|
79
|
+
module.exports = {
|
|
80
|
+
verifyLicense,
|
|
81
|
+
isProLicensed,
|
|
82
|
+
activateLicense,
|
|
83
|
+
generateLicenseKey,
|
|
84
|
+
isValidKey,
|
|
85
|
+
VALID_PREFIXES,
|
|
86
|
+
LICENSE_PATH,
|
|
87
|
+
getLicensePath,
|
|
88
|
+
};
|
package/scripts/mcp-config.js
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
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
|
|
67
|
+
return effectivePkgRoot;
|
|
42
68
|
}
|
|
43
69
|
|
|
44
|
-
return
|
|
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:
|
|
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.
|
|
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) &&
|
|
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(), '.
|
|
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)) {
|
package/scripts/money-watcher.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|