thumbgate 1.28.3 → 1.29.1

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 (66) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/llms.txt +18 -10
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +5 -2
  5. package/adapters/claude/.mcp.json +2 -2
  6. package/adapters/forge/forge.yaml +3 -3
  7. package/adapters/mcp/server-stdio.js +1 -1
  8. package/adapters/opencode/opencode.json +1 -1
  9. package/bin/cli.js +8 -8
  10. package/bin/postinstall.js +4 -13
  11. package/config/github-about.json +5 -4
  12. package/config/post-deploy-marketing-pages.json +6 -6
  13. package/docs/integrations/grafana/README.md +109 -0
  14. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  15. package/openapi/openapi.yaml +240 -5
  16. package/package.json +59 -19
  17. package/public/agent-manager.html +10 -11
  18. package/public/agents-cost-savings.html +2 -2
  19. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  20. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  21. package/public/assets/brand/thumbgate-mark.svg +10 -11
  22. package/public/blog/inside-your-boundary.html +114 -0
  23. package/public/blog/process-over-outcome-gates.html +119 -0
  24. package/public/blog.html +296 -402
  25. package/public/brand/thumbgate-mark.svg +5 -9
  26. package/public/codex-enterprise.html +2 -2
  27. package/public/compare.html +12 -3
  28. package/public/diagnostic.html +79 -29
  29. package/public/guide.html +4 -4
  30. package/public/index.html +1088 -2098
  31. package/public/install.html +3 -3
  32. package/public/js/buyer-intent.js +33 -18
  33. package/public/numbers.html +2 -2
  34. package/public/pricing.html +268 -408
  35. package/public/pro.html +4 -4
  36. package/scripts/billing.js +456 -126
  37. package/scripts/buyer-paths.js +102 -0
  38. package/scripts/cli-feedback.js +5 -3
  39. package/scripts/commercial-offer.js +18 -10
  40. package/scripts/external-customer-audit.js +881 -0
  41. package/scripts/feedback-loop.js +49 -15
  42. package/scripts/gates-engine.js +123 -1
  43. package/scripts/grafana-revenue-evidence.js +856 -0
  44. package/scripts/jsonl-window.js +89 -0
  45. package/scripts/lesson-embedding-index.js +3 -7
  46. package/scripts/meta-agent-loop.js +20 -2
  47. package/scripts/observability-env.js +139 -0
  48. package/scripts/observability-setup.js +55 -0
  49. package/scripts/plausible-domain-config.js +4 -0
  50. package/scripts/provider-live-evidence.js +1290 -0
  51. package/scripts/provider-payment-reconciler.js +442 -0
  52. package/scripts/provider-revenue-evidence.js +249 -0
  53. package/scripts/rate-limiter.js +1 -5
  54. package/scripts/revenue-action-eligibility.js +414 -0
  55. package/scripts/revenue-evidence-remediation.js +694 -0
  56. package/scripts/revenue-offer-system.js +709 -0
  57. package/scripts/sales-pipeline.js +1117 -0
  58. package/scripts/seo-gsd.js +8 -4
  59. package/scripts/stripe-credentials.js +37 -0
  60. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  61. package/scripts/stripe-revenue-catalog.js +164 -0
  62. package/scripts/telemetry-analytics.js +23 -3
  63. package/scripts/vector-store.js +83 -7
  64. package/scripts/workflow-intake-queue.js +483 -0
  65. package/scripts/workflow-sentinel.js +4 -2
  66. package/src/api/server.js +521 -114
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+
5
+ /**
6
+ * Read recent JSONL rows since `sinceMs`, scanning at most maxBytes from EOF.
7
+ * Avoids loading multi-MB ledgers entirely into memory (export timeout fix).
8
+ */
9
+ function readJsonlSinceTail(filePath, {
10
+ sinceMs = 0,
11
+ limit = 1000,
12
+ maxBytes = 8 * 1024 * 1024,
13
+ timestampKeys = ['timestamp', 'receivedAt', 'ts', 'createdAt'],
14
+ } = {}) {
15
+ if (!filePath || !fs.existsSync(filePath)) {
16
+ return { rows: [], totalAfterSince: 0, truncated: false, scannedBytes: 0 };
17
+ }
18
+
19
+ let stat;
20
+ try {
21
+ stat = fs.statSync(filePath);
22
+ } catch {
23
+ return { rows: [], totalAfterSince: 0, truncated: false, scannedBytes: 0 };
24
+ }
25
+
26
+ const size = stat.size || 0;
27
+ if (size === 0) {
28
+ return { rows: [], totalAfterSince: 0, truncated: false, scannedBytes: 0 };
29
+ }
30
+
31
+ const scanBytes = Math.min(size, Math.max(64 * 1024, maxBytes));
32
+ const start = Math.max(0, size - scanBytes);
33
+ let buf;
34
+ try {
35
+ const fd = fs.openSync(filePath, 'r');
36
+ try {
37
+ buf = Buffer.alloc(scanBytes);
38
+ fs.readSync(fd, buf, 0, scanBytes, start);
39
+ } finally {
40
+ fs.closeSync(fd);
41
+ }
42
+ } catch {
43
+ return { rows: [], totalAfterSince: 0, truncated: false, scannedBytes: 0 };
44
+ }
45
+
46
+ let text = buf.toString('utf8');
47
+ // If we started mid-file, drop the partial first line.
48
+ if (start > 0) {
49
+ const nl = text.indexOf('\n');
50
+ text = nl >= 0 ? text.slice(nl + 1) : '';
51
+ }
52
+
53
+ const matched = [];
54
+ for (const line of text.split('\n')) {
55
+ if (!line) continue;
56
+ let obj;
57
+ try {
58
+ obj = JSON.parse(line);
59
+ } catch {
60
+ continue;
61
+ }
62
+ let parsed = NaN;
63
+ for (const key of timestampKeys) {
64
+ const raw = obj && obj[key];
65
+ if (raw == null) continue;
66
+ parsed = typeof raw === 'number' ? raw : Date.parse(raw);
67
+ if (Number.isFinite(parsed)) break;
68
+ }
69
+ // Preserve since filter: only include rows with a parseable timestamp
70
+ // inside the window. Legacy/malformed lines without timestamps must not
71
+ // inflate totalAfterSince or journey summaries.
72
+ if (Number.isFinite(parsed) && parsed >= sinceMs) {
73
+ matched.push(obj);
74
+ }
75
+ }
76
+
77
+ const totalAfterSince = matched.length;
78
+ const rows = matched.slice(-Math.max(1, limit));
79
+ return {
80
+ rows,
81
+ totalAfterSince,
82
+ truncated: totalAfterSince > rows.length || start > 0,
83
+ scannedBytes: scanBytes,
84
+ };
85
+ }
86
+
87
+ module.exports = {
88
+ readJsonlSinceTail,
89
+ };
@@ -99,16 +99,12 @@ function isEmbedderAvailable() {
99
99
  const cfg = resolveGeminiEmbeddingConfig();
100
100
  if (cfg && cfg.enabled && cfg.apiKey) return true;
101
101
  } catch { /* policy module unavailable */ }
102
- // Local transformers path
103
- try {
104
- require.resolve('@huggingface/transformers');
105
- return true;
106
- } catch { /* not installed */ }
107
- return false;
102
+ // The zero-dependency feature-hash provider is always available locally.
103
+ return true;
108
104
  }
109
105
 
110
106
  function defaultEmbedder() {
111
- // Lazy: do not pull in LanceDB/transformers at module require time.
107
+ // Lazy: do not pull in LanceDB or optional embedding providers at module require time.
112
108
  const { embed } = require('./vector-store');
113
109
  return embed;
114
110
  }
@@ -581,6 +581,20 @@ async function main() {
581
581
  const args = process.argv.slice(2);
582
582
  const dryRun = args.includes('--dry-run');
583
583
  const verbose = args.includes('--verbose') || args.includes('-v');
584
+ let hookMode = args.includes('--hook');
585
+
586
+ // Settings may already be cached by a running agent process. Detect the
587
+ // current Stop payload as well, so the legacy command (without --hook) is
588
+ // immediately JSON-safe before the host reloads its settings.
589
+ if (!hookMode && !process.stdin.isTTY) {
590
+ try {
591
+ const raw = fs.readFileSync(0, 'utf8');
592
+ const payload = raw ? JSON.parse(raw) : {};
593
+ hookMode = payload.hook_event_name === 'Stop';
594
+ } catch {
595
+ // Normal CLI invocation with empty/non-JSON stdin.
596
+ }
597
+ }
584
598
 
585
599
  if (args.includes('--status')) {
586
600
  const status = getMetaAgentStatus();
@@ -593,9 +607,13 @@ async function main() {
593
607
  }
594
608
 
595
609
  const mode = dryRun ? 'DRY RUN' : 'LIVE';
596
- console.log(`Meta-agent loop starting [${mode}]...`);
610
+ if (!hookMode) console.log(`Meta-agent loop starting [${mode}]...`);
611
+
612
+ const manifest = await runMetaAgentLoop({ dryRun, verbose: hookMode ? false : true });
597
613
 
598
- const manifest = await runMetaAgentLoop({ dryRun, verbose: verbose || true });
614
+ // Stop hooks may emit only empty stdout or a valid JSON object. The loop's
615
+ // work is intentionally silent in hook mode.
616
+ if (hookMode) return;
599
617
 
600
618
  console.log(`Run ID : ${manifest.runId}`);
601
619
  console.log(`Analysis mode : ${manifest.analysisMode}`);
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Load operator-local observability credentials into process.env.
5
+ *
6
+ * Sources (first wins per key, never overwrite an already-set env var):
7
+ * 1. process.env
8
+ * 2. ~/.config/thumbgate/observability.json
9
+ * 3. ~/.config/thumbgate/operator.json (operatorKey / baseUrl only)
10
+ * 4. Stripe managed secret files via resolveStripeSecretKey
11
+ *
12
+ * The JSON file is gitignored operator state. Never print secret values.
13
+ */
14
+
15
+ const fs = require('node:fs');
16
+ const os = require('node:os');
17
+ const path = require('node:path');
18
+
19
+ const OBSERVABILITY_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'observability.json');
20
+ const OPERATOR_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
21
+
22
+ const JSON_KEY_TO_ENV = Object.freeze({
23
+ stripeSecretKey: 'STRIPE_SECRET_KEY',
24
+ plausibleApiKey: 'PLAUSIBLE_API_KEY',
25
+ plausibleSiteId: 'PLAUSIBLE_SITE_ID',
26
+ plausibleSiteIds: 'PLAUSIBLE_SITE_IDS',
27
+ plausibleRegisteredDomains: 'THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS',
28
+ posthogPersonalApiKey: 'POSTHOG_PERSONAL_API_KEY',
29
+ posthogProjectId: 'POSTHOG_PROJECT_ID',
30
+ operatorKey: 'THUMBGATE_OPERATOR_KEY',
31
+ apiKey: 'THUMBGATE_API_KEY',
32
+ publicAppOrigin: 'THUMBGATE_PUBLIC_APP_ORIGIN',
33
+ billingApiBaseUrl: 'THUMBGATE_BILLING_API_BASE_URL',
34
+ });
35
+
36
+ function normalizeText(value) {
37
+ if (value === undefined || value === null) return null;
38
+ const text = String(value).trim();
39
+ return text || null;
40
+ }
41
+
42
+ function readJsonFile(filePath) {
43
+ try {
44
+ if (!filePath || !fs.existsSync(filePath)) return null;
45
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function applyJsonToEnv(json, env) {
52
+ if (!json || typeof json !== 'object') return [];
53
+ const applied = [];
54
+ for (const [jsonKey, envKey] of Object.entries(JSON_KEY_TO_ENV)) {
55
+ if (normalizeText(env[envKey])) continue;
56
+ const value = normalizeText(json[jsonKey]);
57
+ if (!value) continue;
58
+ env[envKey] = value;
59
+ applied.push(envKey);
60
+ }
61
+ return applied;
62
+ }
63
+
64
+ function loadObservabilityEnv({
65
+ env = process.env,
66
+ observabilityPath = OBSERVABILITY_CONFIG_PATH,
67
+ operatorPath = OPERATOR_CONFIG_PATH,
68
+ applyStripeManagedFiles = true,
69
+ } = {}) {
70
+ const applied = [];
71
+
72
+ const observability = readJsonFile(observabilityPath);
73
+ applied.push(...applyJsonToEnv(observability, env));
74
+
75
+ const operator = readJsonFile(operatorPath);
76
+ if (operator) {
77
+ if (!normalizeText(env.THUMBGATE_OPERATOR_KEY) && normalizeText(operator.operatorKey)) {
78
+ env.THUMBGATE_OPERATOR_KEY = String(operator.operatorKey).trim();
79
+ applied.push('THUMBGATE_OPERATOR_KEY');
80
+ }
81
+ if (!normalizeText(env.THUMBGATE_BILLING_API_BASE_URL) && normalizeText(operator.baseUrl)) {
82
+ env.THUMBGATE_BILLING_API_BASE_URL = String(operator.baseUrl).trim();
83
+ applied.push('THUMBGATE_BILLING_API_BASE_URL');
84
+ }
85
+ }
86
+
87
+ if (applyStripeManagedFiles && !normalizeText(env.STRIPE_SECRET_KEY)) {
88
+ try {
89
+ const { resolveStripeSecretKey } = require('./stripe-credentials');
90
+ const resolved = resolveStripeSecretKey({ env });
91
+ if (resolved.secretKey) {
92
+ env.STRIPE_SECRET_KEY = resolved.secretKey;
93
+ applied.push('STRIPE_SECRET_KEY');
94
+ }
95
+ } catch {
96
+ // stripe-credentials optional at load time
97
+ }
98
+ }
99
+
100
+ // Product-primary Plausible domain is always part of the registered set for
101
+ // doctor/automation unless the operator explicitly overrides site id.
102
+ if (!normalizeText(env.PLAUSIBLE_SITE_ID) && !normalizeText(env.THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS)) {
103
+ env.THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS = 'thumbgate.ai,thumbgate-production.up.railway.app';
104
+ applied.push('THUMBGATE_PLAUSIBLE_REGISTERED_DOMAINS');
105
+ }
106
+
107
+ return {
108
+ applied: [...new Set(applied)],
109
+ observabilityPath,
110
+ operatorPath,
111
+ hasStripe: Boolean(normalizeText(env.STRIPE_SECRET_KEY)),
112
+ hasPlausible: Boolean(normalizeText(env.PLAUSIBLE_API_KEY) && normalizeText(env.PLAUSIBLE_SITE_ID)),
113
+ hasPosthog: Boolean(normalizeText(env.POSTHOG_PERSONAL_API_KEY) && normalizeText(env.POSTHOG_PROJECT_ID)),
114
+ hasOperator: Boolean(normalizeText(env.THUMBGATE_OPERATOR_KEY) || normalizeText(env.THUMBGATE_API_KEY)),
115
+ };
116
+ }
117
+
118
+ function observabilityConfigTemplate() {
119
+ return {
120
+ stripeSecretKey: '',
121
+ plausibleApiKey: '',
122
+ plausibleSiteId: 'thumbgate.ai',
123
+ plausibleRegisteredDomains: 'thumbgate.ai,thumbgate-production.up.railway.app',
124
+ posthogPersonalApiKey: '',
125
+ posthogProjectId: '',
126
+ publicAppOrigin: 'https://thumbgate.ai',
127
+ billingApiBaseUrl: 'https://thumbgate-production.up.railway.app',
128
+ };
129
+ }
130
+
131
+ module.exports = {
132
+ OBSERVABILITY_CONFIG_PATH,
133
+ OPERATOR_CONFIG_PATH,
134
+ JSON_KEY_TO_ENV,
135
+ loadObservabilityEnv,
136
+ observabilityConfigTemplate,
137
+ readJsonFile,
138
+ normalizeText,
139
+ };
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+ const {
8
+ OBSERVABILITY_CONFIG_PATH,
9
+ observabilityConfigTemplate,
10
+ loadObservabilityEnv,
11
+ } = require('./observability-env');
12
+
13
+ function ensureDir(filePath) {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ }
16
+
17
+ function main(argv = process.argv.slice(2)) {
18
+ const write = argv.includes('--write');
19
+ const print = argv.includes('--print') || !write;
20
+ const template = observabilityConfigTemplate();
21
+
22
+ if (print && !write) {
23
+ process.stdout.write(`${JSON.stringify(template, null, 2)}\n`);
24
+ process.stdout.write(
25
+ `\n# Write to ${OBSERVABILITY_CONFIG_PATH} with --write after filling secrets.\n` +
26
+ '# Never commit this file. Doctor/revenue tools load it automatically.\n'
27
+ );
28
+ }
29
+
30
+ if (write) {
31
+ ensureDir(OBSERVABILITY_CONFIG_PATH);
32
+ if (fs.existsSync(OBSERVABILITY_CONFIG_PATH)) {
33
+ process.stderr.write(`Refusing to overwrite existing ${OBSERVABILITY_CONFIG_PATH}\n`);
34
+ process.exitCode = 2;
35
+ return;
36
+ }
37
+ fs.writeFileSync(OBSERVABILITY_CONFIG_PATH, `${JSON.stringify(template, null, 2)}\n`, { mode: 0o600 });
38
+ process.stdout.write(`Wrote template ${OBSERVABILITY_CONFIG_PATH}\n`);
39
+ }
40
+
41
+ const status = loadObservabilityEnv({ env: { ...process.env } });
42
+ process.stdout.write(JSON.stringify({
43
+ configPath: OBSERVABILITY_CONFIG_PATH,
44
+ hasStripe: status.hasStripe,
45
+ hasPlausible: status.hasPlausible,
46
+ hasPosthog: status.hasPosthog,
47
+ hasOperator: status.hasOperator,
48
+ }, null, 2) + '\n');
49
+ }
50
+
51
+ if (require('node:path').resolve(process.argv[1] || '') === require('node:path').resolve(__filename)) {
52
+ main();
53
+ }
54
+
55
+ module.exports = { main };
@@ -30,7 +30,11 @@ function getConfiguredRegisteredDomains(env = process.env) {
30
30
  ...splitDomains(env.PLAUSIBLE_REGISTERED_DOMAINS),
31
31
  ].map(normalizeDomain).filter(Boolean);
32
32
 
33
+ // Both product surfaces are first-class Plausible site ids for ThumbGate.
34
+ // Emitting data-domain=thumbgate.ai while only registering the Railway host
35
+ // previously made primary-domain traffic invisible to automation.
33
36
  return [...new Set([
37
+ PRIMARY_PLAUSIBLE_DOMAIN,
34
38
  FALLBACK_REGISTERED_PLAUSIBLE_DOMAIN,
35
39
  ...configured,
36
40
  ])];