thumbgate 1.27.18 → 1.27.20
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/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
package/scripts/bot-detection.js
CHANGED
|
@@ -36,13 +36,18 @@ const BOT_PATTERNS = [
|
|
|
36
36
|
/\bbaiduspider\b/i,
|
|
37
37
|
/\bduckduckbot\b/i,
|
|
38
38
|
/\bapplebot\b/i,
|
|
39
|
+
/slurp/i,
|
|
40
|
+
/petalbot/i,
|
|
41
|
+
/mediapartners/i,
|
|
39
42
|
// LLM / AI crawlers — these started exploding in 2024+
|
|
40
43
|
/\bgptbot\b/i,
|
|
41
|
-
|
|
44
|
+
/chatgpt/i,
|
|
42
45
|
/\boai-searchbot\b/i,
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
/perplexity/i,
|
|
47
|
+
/anthropic/i,
|
|
45
48
|
/\bclaude(?:bot|-web)\b/i,
|
|
49
|
+
/claude-searchbot/i,
|
|
50
|
+
/google-extended/i,
|
|
46
51
|
/\bccbot\b/i,
|
|
47
52
|
/\bcohere-ai\b/i,
|
|
48
53
|
/\bbytespider\b/i,
|
|
@@ -86,6 +91,8 @@ const BOT_PATTERNS = [
|
|
|
86
91
|
/\blibwww-perl\b/i,
|
|
87
92
|
/\bjava\//i,
|
|
88
93
|
/\bgo-http-client\b/i,
|
|
94
|
+
/scrapy/i,
|
|
95
|
+
/httpclient/i,
|
|
89
96
|
/\bruby\b/i,
|
|
90
97
|
// API/test tools
|
|
91
98
|
/\bpostman(?:runtime)?\b/i,
|
|
@@ -158,8 +165,59 @@ function isProbablyBot(headers) {
|
|
|
158
165
|
return classifyRequester(headers).isBot;
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
// --- Visitor classification (analytics filtering) ---------------------------
|
|
169
|
+
// Classifies telemetry/analytics traffic as bot / owner / real_user.
|
|
170
|
+
// Owner traffic is identified via the THUMBGATE_OWNER_EMAILS env var
|
|
171
|
+
// (comma-separated, case-insensitive). There is no built-in default:
|
|
172
|
+
// operators opt in via config, so the shipped artifact carries no
|
|
173
|
+
// personal data.
|
|
174
|
+
|
|
175
|
+
function getOwnerEmails(env = process.env) {
|
|
176
|
+
const raw = env.THUMBGATE_OWNER_EMAILS;
|
|
177
|
+
if (!raw) return [];
|
|
178
|
+
return raw
|
|
179
|
+
.split(',')
|
|
180
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
181
|
+
.filter(Boolean);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function classifyVisitor(req) {
|
|
185
|
+
const ua = (req.headers && req.headers['user-agent']) || '';
|
|
186
|
+
const email = req.email || (req.query && req.query.email) || '';
|
|
187
|
+
|
|
188
|
+
for (const pattern of BOT_PATTERNS) {
|
|
189
|
+
if (pattern.test(ua)) {
|
|
190
|
+
return { type: 'bot', reason: `UA matches: ${pattern}`, userAgent: ua };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!ua || ua.length < 10) {
|
|
194
|
+
return { type: 'bot', reason: 'Empty or short user-agent', userAgent: ua };
|
|
195
|
+
}
|
|
196
|
+
if (email) {
|
|
197
|
+
const normalized = email.toLowerCase();
|
|
198
|
+
if (getOwnerEmails().some((ownerEmail) => normalized.includes(ownerEmail))) {
|
|
199
|
+
return { type: 'owner', reason: 'Email matches configured owner list', userAgent: ua };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { type: 'real_user', reason: 'No bot pattern matched', userAgent: ua };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function shouldExcludeFromAnalytics(req) {
|
|
206
|
+
const classification = req.visitorClass || classifyVisitor(req);
|
|
207
|
+
return classification.type === 'bot';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function botFilterMiddleware(req, res, next) {
|
|
211
|
+
req.visitorClass = classifyVisitor(req);
|
|
212
|
+
next();
|
|
213
|
+
}
|
|
214
|
+
|
|
161
215
|
module.exports = {
|
|
162
216
|
classifyRequester,
|
|
163
217
|
isProbablyBot,
|
|
218
|
+
classifyVisitor,
|
|
219
|
+
shouldExcludeFromAnalytics,
|
|
220
|
+
botFilterMiddleware,
|
|
221
|
+
getOwnerEmails,
|
|
164
222
|
BOT_PATTERNS,
|
|
165
223
|
};
|
|
@@ -5,6 +5,11 @@ const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
|
5
5
|
const DEFAULT_BUILD_METADATA_PATH = path.join(PROJECT_ROOT, 'config', 'build-metadata.json');
|
|
6
6
|
const BUILD_SHA_ENV_KEY = 'THUMBGATE_BUILD_SHA';
|
|
7
7
|
const BUILD_GENERATED_AT_ENV_KEY = 'THUMBGATE_BUILD_GENERATED_AT';
|
|
8
|
+
// Railway injects this automatically for GitHub-connected deployments: the git
|
|
9
|
+
// SHA of the commit that triggered the deploy. It is the ground truth for what
|
|
10
|
+
// code is actually live, and unlike THUMBGATE_BUILD_SHA it cannot drift (Railway
|
|
11
|
+
// sets it per deploy). https://docs.railway.com/reference/variables
|
|
12
|
+
const RAILWAY_GIT_COMMIT_SHA_ENV_KEY = 'RAILWAY_GIT_COMMIT_SHA';
|
|
8
13
|
|
|
9
14
|
function normalizeNullableText(value) {
|
|
10
15
|
if (typeof value !== 'string') {
|
|
@@ -16,35 +21,69 @@ function normalizeNullableText(value) {
|
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
function resolveBuildMetadata({ env = process.env, filePath } = {}) {
|
|
24
|
+
// Precedence: immutable JSON file (baked into Docker image at build time, so it
|
|
25
|
+
// ALWAYS matches the deployed code) wins over runtime env vars. Env vars are
|
|
26
|
+
// mutable Railway/host config that can drift — they shadowed the freshly-stamped
|
|
27
|
+
// SHA in prod on 2026-05-20 and made /health lie about the deployed commit.
|
|
28
|
+
// Fall back to env vars only when the file is missing or its values are null,
|
|
29
|
+
// and require an explicit SHA env var (not just a stray GENERATED_AT) before
|
|
30
|
+
// trusting the env branch.
|
|
19
31
|
const resolvedPath =
|
|
20
32
|
normalizeNullableText(filePath) ||
|
|
21
33
|
normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
|
|
22
34
|
DEFAULT_BUILD_METADATA_PATH;
|
|
23
35
|
const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
|
|
36
|
+
const railwayGitSha = normalizeNullableText(env[RAILWAY_GIT_COMMIT_SHA_ENV_KEY]);
|
|
24
37
|
const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
|
|
25
38
|
|
|
26
|
-
|
|
39
|
+
let fileBuildSha = null;
|
|
40
|
+
let fileGeneratedAt = null;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
43
|
+
fileBuildSha = normalizeNullableText(parsed.buildSha);
|
|
44
|
+
fileGeneratedAt = normalizeNullableText(parsed.generatedAt);
|
|
45
|
+
} catch {
|
|
46
|
+
// file missing or unreadable — fall through to env branch
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (fileBuildSha) {
|
|
27
50
|
return {
|
|
28
51
|
path: resolvedPath,
|
|
29
|
-
buildSha:
|
|
30
|
-
generatedAt: envGeneratedAt,
|
|
52
|
+
buildSha: fileBuildSha,
|
|
53
|
+
generatedAt: fileGeneratedAt || envGeneratedAt,
|
|
31
54
|
};
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
// No SHA baked into the image. Prefer Railway's own per-deploy commit SHA over
|
|
58
|
+
// THUMBGATE_BUILD_SHA: the latter is set out-of-band by the deploy workflow and
|
|
59
|
+
// has drifted in prod (stuck reporting an old commit while newer code was live,
|
|
60
|
+
// because RAILWAY_SYNC_VARIABLES is off and `railway up` stamping is unreliable).
|
|
61
|
+
// RAILWAY_GIT_COMMIT_SHA is injected by Railway per deploy, so it always matches
|
|
62
|
+
// the code actually serving traffic on a GitHub-connected service.
|
|
63
|
+
if (railwayGitSha) {
|
|
36
64
|
return {
|
|
37
65
|
path: resolvedPath,
|
|
38
|
-
buildSha:
|
|
39
|
-
generatedAt:
|
|
66
|
+
buildSha: railwayGitSha,
|
|
67
|
+
generatedAt: envGeneratedAt,
|
|
40
68
|
};
|
|
41
|
-
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Last resort: the workflow-managed env var. Only trust it when an explicit SHA
|
|
72
|
+
// is set. (Previously a bare GENERATED_AT with no SHA could short-circuit and
|
|
73
|
+
// return { buildSha: null }, losing both signals; now we require the SHA.)
|
|
74
|
+
if (envBuildSha) {
|
|
42
75
|
return {
|
|
43
76
|
path: resolvedPath,
|
|
44
|
-
buildSha:
|
|
45
|
-
generatedAt:
|
|
77
|
+
buildSha: envBuildSha,
|
|
78
|
+
generatedAt: envGeneratedAt,
|
|
46
79
|
};
|
|
47
80
|
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
path: resolvedPath,
|
|
84
|
+
buildSha: null,
|
|
85
|
+
generatedAt: fileGeneratedAt || envGeneratedAt,
|
|
86
|
+
};
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
function writeBuildMetadataFile({ sha, outputPath, generatedAt = new Date().toISOString() }) {
|
|
@@ -105,6 +144,7 @@ if (require.main === module) {
|
|
|
105
144
|
module.exports = {
|
|
106
145
|
BUILD_GENERATED_AT_ENV_KEY,
|
|
107
146
|
BUILD_SHA_ENV_KEY,
|
|
147
|
+
RAILWAY_GIT_COMMIT_SHA_ENV_KEY,
|
|
108
148
|
DEFAULT_BUILD_METADATA_PATH,
|
|
109
149
|
resolveBuildMetadata,
|
|
110
150
|
writeBuildMetadataFile,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', 'config', 'gate-classifier-routing.json');
|
|
8
|
+
|
|
9
|
+
function clamp01(value, fallback = 0) {
|
|
10
|
+
const number = Number(value);
|
|
11
|
+
if (!Number.isFinite(number)) return fallback;
|
|
12
|
+
return Math.min(1, Math.max(0, number));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function riskRank(risk) {
|
|
16
|
+
const normalized = String(risk || 'medium').toLowerCase();
|
|
17
|
+
if (['critical', 'block', 'regulated'].includes(normalized)) return 4;
|
|
18
|
+
if (['high', 'dangerous'].includes(normalized)) return 3;
|
|
19
|
+
if (['medium', 'warn'].includes(normalized)) return 2;
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadClassifierRoutingConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
24
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lane(config, laneName, reason, input, overrides = {}) {
|
|
28
|
+
const laneConfig = (config.lanes && config.lanes[laneName]) || {};
|
|
29
|
+
return {
|
|
30
|
+
lane: laneName,
|
|
31
|
+
reason,
|
|
32
|
+
requiresEvidence: Boolean(laneConfig.requiresEvidence || overrides.requiresEvidence),
|
|
33
|
+
cloudAllowed: Boolean(laneConfig.cloudAllowed),
|
|
34
|
+
maxLatencyMs: laneConfig.maxLatencyMs,
|
|
35
|
+
risk: String(input.risk || 'medium').toLowerCase(),
|
|
36
|
+
ambiguity: clamp01(input.ambiguity),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function routeClassifier(input = {}, config = loadClassifierRoutingConfig()) {
|
|
41
|
+
const thresholds = config.thresholds || {};
|
|
42
|
+
const labelCount = Number(input.labelCount || input.examples || 0);
|
|
43
|
+
const latencyBudgetMs = Number(input.latencyBudgetMs || 0);
|
|
44
|
+
const ambiguity = clamp01(input.ambiguity);
|
|
45
|
+
const risk = riskRank(input.risk);
|
|
46
|
+
const largeBatch = Number(input.batchRows || 0) >= Number(thresholds.largeBatchRows || 50);
|
|
47
|
+
const privacySensitive = Boolean(input.privacySensitive || input.containsSecrets || input.customerData);
|
|
48
|
+
const allowCloud = Boolean(input.allowCloud);
|
|
49
|
+
|
|
50
|
+
if (input.hasHardRule || input.exactPolicyMatch) {
|
|
51
|
+
return lane(config, 'deterministic', 'exact hard rule or policy match; do not spend model tokens', input);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (input.semanticCacheHit || input.equivalentRepeat) {
|
|
55
|
+
return lane(config, 'semantic_cache', 'semantically equivalent repeat; reuse the proven prior decision without a model call', input);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (input.rubricFailed || input.missingEvidence || input.completionClaimWithoutProof) {
|
|
59
|
+
return lane(config, 'rubric_gate', 'rubric or completion evidence failed; block done claims until proof exists', input);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (input.structuredDataset && (input.missingProvenance || input.missingSources)) {
|
|
63
|
+
return lane(config, 'rubric_gate', 'structured data claim is missing source provenance', input);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (privacySensitive && !allowCloud && risk >= 3 && ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
|
|
67
|
+
return lane(config, 'human_review', 'private high-risk ambiguous action; keep data local and require approval', input);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (labelCount >= Number(thresholds.classicalMinExamples || 40) && (largeBatch || latencyBudgetMs <= Number(thresholds.lowLatencyBudgetMs || 300)) && ambiguity < Number(thresholds.mediumAmbiguity || 0.35)) {
|
|
71
|
+
return lane(config, 'local_classical', 'enough examples and low ambiguity; use cheap local classification', input);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (risk >= 3 && ambiguity >= Number(thresholds.highRiskAmbiguity || 0.65)) {
|
|
75
|
+
if (allowCloud && latencyBudgetMs >= Number(thresholds.llmMinLatencyBudgetMs || 2000)) {
|
|
76
|
+
return lane(config, 'llm_judge', 'high-risk semantic ambiguity; use a budget-capped LLM judge with evidence', input);
|
|
77
|
+
}
|
|
78
|
+
return lane(config, 'human_review', 'high-risk ambiguity without approved cloud/budget route', input);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (labelCount < Number(thresholds.classicalMinExamples || 40) || ambiguity >= Number(thresholds.mediumAmbiguity || 0.35)) {
|
|
82
|
+
return lane(config, 'local_semantic', 'sparse labels or fuzzy intent; use local semantic recall before any LLM', input);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return lane(config, config.defaultLane || 'local_classical', 'default local route for routine gate classification', input);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function explainClassifierRoute(input = {}, config = loadClassifierRoutingConfig()) {
|
|
89
|
+
const decision = routeClassifier(input, config);
|
|
90
|
+
const laneConfig = (config.lanes && config.lanes[decision.lane]) || {};
|
|
91
|
+
return {
|
|
92
|
+
...decision,
|
|
93
|
+
description: laneConfig.description || '',
|
|
94
|
+
useFor: laneConfig.useFor || [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseArgs(argv) {
|
|
99
|
+
const input = {};
|
|
100
|
+
for (const arg of argv) {
|
|
101
|
+
if (arg === '--hard-rule') input.hasHardRule = true;
|
|
102
|
+
else if (arg === '--privacy-sensitive') input.privacySensitive = true;
|
|
103
|
+
else if (arg === '--allow-cloud') input.allowCloud = true;
|
|
104
|
+
else if (arg === '--customer-data') input.customerData = true;
|
|
105
|
+
else if (arg === '--semantic-cache-hit') input.semanticCacheHit = true;
|
|
106
|
+
else if (arg === '--equivalent-repeat') input.equivalentRepeat = true;
|
|
107
|
+
else if (arg === '--rubric-failed') input.rubricFailed = true;
|
|
108
|
+
else if (arg === '--missing-evidence') input.missingEvidence = true;
|
|
109
|
+
else if (arg === '--structured-dataset') input.structuredDataset = true;
|
|
110
|
+
else if (arg === '--missing-provenance') input.missingProvenance = true;
|
|
111
|
+
else if (arg.startsWith('--risk=')) input.risk = arg.slice('--risk='.length);
|
|
112
|
+
else if (arg.startsWith('--ambiguity=')) input.ambiguity = Number(arg.slice('--ambiguity='.length));
|
|
113
|
+
else if (arg.startsWith('--labels=')) input.labelCount = Number(arg.slice('--labels='.length));
|
|
114
|
+
else if (arg.startsWith('--latency-ms=')) input.latencyBudgetMs = Number(arg.slice('--latency-ms='.length));
|
|
115
|
+
else if (arg.startsWith('--batch-rows=')) input.batchRows = Number(arg.slice('--batch-rows='.length));
|
|
116
|
+
}
|
|
117
|
+
return input;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
DEFAULT_CONFIG_PATH,
|
|
122
|
+
explainClassifierRoute,
|
|
123
|
+
loadClassifierRoutingConfig,
|
|
124
|
+
routeClassifier,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (require.main === module) {
|
|
128
|
+
const decision = explainClassifierRoute(parseArgs(process.argv.slice(2)));
|
|
129
|
+
process.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
|
|
130
|
+
}
|
package/scripts/cli-feedback.js
CHANGED
|
@@ -88,11 +88,13 @@ function formatCliOutput(result) {
|
|
|
88
88
|
if (result.feedbackResult && result.feedbackResult.accepted !== false) {
|
|
89
89
|
lines.push(`${isDown ? R : G}${BD}${isDown ? '👎 Thumbs down recorded' : '👍 Thumbs up recorded'}${RST}`);
|
|
90
90
|
const feedbackId = (result.feedbackResult.feedbackEvent && result.feedbackResult.feedbackEvent.id) || result.feedbackResult.id;
|
|
91
|
+
const memoryId = (result.feedbackResult.memoryRecord && result.feedbackResult.memoryRecord.id) || result.feedbackResult.memoryId;
|
|
91
92
|
if (feedbackId) {
|
|
92
|
-
lines.push(`${D} ID: ${feedbackId}${RST}`);
|
|
93
|
+
lines.push(`${D} Feedback ID: ${feedbackId}${RST}`);
|
|
94
|
+
if (memoryId) lines.push(`${D} Memory ID : ${memoryId}${RST}`);
|
|
93
95
|
// Echo feedback ID to stderr so it's visible directly in the terminal,
|
|
94
96
|
// not hidden behind Claude Code's "ctrl+o to expand" MCP call collapse.
|
|
95
|
-
process.stderr.write(`✅ Feedback captured (${feedbackId})\n`);
|
|
97
|
+
process.stderr.write(`✅ Feedback captured (${feedbackId}${memoryId ? `, ${memoryId}` : ''})\n`);
|
|
96
98
|
}
|
|
97
99
|
} else {
|
|
98
100
|
lines.push(`${R}Feedback not accepted: ${(result.feedbackResult && result.feedbackResult.reason) || 'unknown'}${RST}`);
|
package/scripts/cli-schema.js
CHANGED
|
@@ -51,6 +51,20 @@ const CLI_COMMANDS = [
|
|
|
51
51
|
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
52
52
|
],
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: 'feedback-self-test',
|
|
56
|
+
aliases: ['dogfood'],
|
|
57
|
+
description: 'Prove thumbs feedback capture works in the current runtime',
|
|
58
|
+
group: 'capture',
|
|
59
|
+
mcpTool: 'capture_feedback',
|
|
60
|
+
flags: [
|
|
61
|
+
{ name: 'feedback', type: 'string', description: 'Signal to test: up or down (default down)' },
|
|
62
|
+
{ name: 'context', type: 'string', description: 'Context to store in the test capture' },
|
|
63
|
+
{ name: 'persist', type: 'boolean', description: 'Use the active ThumbGate store instead of an isolated test store' },
|
|
64
|
+
{ name: 'feedback-dir', type: 'string', description: 'Explicit feedback directory for the self-test' },
|
|
65
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
54
68
|
|
|
55
69
|
// -------------------------------------------------------------------------
|
|
56
70
|
// Discovery
|
|
@@ -80,6 +94,25 @@ const CLI_COMMANDS = [
|
|
|
80
94
|
{ name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
|
|
81
95
|
],
|
|
82
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: 'brain',
|
|
99
|
+
aliases: ['customer-brain', 'repo-brain'],
|
|
100
|
+
description: 'Scaffold, query, or build the governed customer/repo context brain',
|
|
101
|
+
group: 'discovery',
|
|
102
|
+
flags: [
|
|
103
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
104
|
+
{ name: 'task', type: 'string', description: 'Task description for routed context loading' },
|
|
105
|
+
{ name: 'type', type: 'string', description: 'Memory type for remember: decision | pattern | feedback | log' },
|
|
106
|
+
{ name: 'title', type: 'string', description: 'Title for a sourced memory entry' },
|
|
107
|
+
{ name: 'content', type: 'string', description: 'Body for a sourced memory entry' },
|
|
108
|
+
{ name: 'source', type: 'string', description: 'Required provenance for factual memory writes' },
|
|
109
|
+
{ name: 'tags', type: 'string', description: 'Comma-separated memory tags' },
|
|
110
|
+
{ name: 'text', type: 'string', description: 'Text/action to check against never-do rules' },
|
|
111
|
+
{ name: 'stale-days', type: 'number', description: 'Age threshold for cleanup report (default 60)' },
|
|
112
|
+
{ name: 'write', type: 'boolean', description: 'Save to .thumbgate/BRAIN.md (versioned, deterministic)' },
|
|
113
|
+
{ name: 'limit', type: 'number', description: 'Max lessons to include (default 15)' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
83
116
|
{
|
|
84
117
|
name: 'stats',
|
|
85
118
|
description: 'Feedback analytics — approval rate, Revenue-at-Risk, recent trend',
|
|
@@ -337,6 +370,19 @@ const CLI_COMMANDS = [
|
|
|
337
370
|
{ name: 'high-risk-workflows', type: 'string', description: 'Comma-separated workflows touching money, prod, secrets, data, or publishing' },
|
|
338
371
|
],
|
|
339
372
|
}),
|
|
373
|
+
discoveryCommand({
|
|
374
|
+
name: 'ai-inventory',
|
|
375
|
+
aliases: ['ai-component-inventory', 'ml-bom', 'mlbom'],
|
|
376
|
+
description: 'Scan AI/ML components and export enterprise ML-BOM evidence',
|
|
377
|
+
mcpTool: 'ai_component_inventory',
|
|
378
|
+
flags: [
|
|
379
|
+
jsonFlag(),
|
|
380
|
+
{ name: 'root', type: 'string', description: 'Project root to scan' },
|
|
381
|
+
{ name: 'format', type: 'string', description: 'summary, json, or cyclonedx' },
|
|
382
|
+
{ name: 'output', type: 'string', description: 'Write evidence to this path' },
|
|
383
|
+
{ name: 'max-files', type: 'number', description: 'Maximum files to scan' },
|
|
384
|
+
],
|
|
385
|
+
}),
|
|
340
386
|
discoveryCommand({
|
|
341
387
|
name: 'long-running-agent-context-guardrails',
|
|
342
388
|
aliases: ['agent-context-guardrails', 'slack-context-guardrails'],
|
|
@@ -459,6 +505,12 @@ const CLI_COMMANDS = [
|
|
|
459
505
|
group: 'gates',
|
|
460
506
|
flags: [],
|
|
461
507
|
},
|
|
508
|
+
{
|
|
509
|
+
name: 'hermes-gate',
|
|
510
|
+
description: 'Hermes Agent pre_tool_call hook: gate runtime tool calls (incl. skill_manage) before they run',
|
|
511
|
+
group: 'gates',
|
|
512
|
+
flags: [],
|
|
513
|
+
},
|
|
462
514
|
{
|
|
463
515
|
name: 'force-gate',
|
|
464
516
|
description: 'Immediately create a blocking gate from a pattern string',
|
|
@@ -525,6 +577,14 @@ const CLI_COMMANDS = [
|
|
|
525
577
|
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
526
578
|
],
|
|
527
579
|
},
|
|
580
|
+
{
|
|
581
|
+
name: 'audit',
|
|
582
|
+
description: 'Audit an agent transcript for repeat-mistake patterns and estimated token waste',
|
|
583
|
+
group: 'ops',
|
|
584
|
+
flags: [
|
|
585
|
+
{ name: 'file', type: 'string', description: 'Path to the agent transcript to audit' },
|
|
586
|
+
],
|
|
587
|
+
},
|
|
528
588
|
{
|
|
529
589
|
name: 'init',
|
|
530
590
|
description: 'Scaffold .thumbgate/ config and wire agent hooks',
|
|
@@ -582,6 +642,43 @@ const CLI_COMMANDS = [
|
|
|
582
642
|
{ name: 'info', type: 'boolean', description: 'Show Pro feature list' },
|
|
583
643
|
],
|
|
584
644
|
},
|
|
645
|
+
{
|
|
646
|
+
name: 'diagnostic',
|
|
647
|
+
aliases: ['workflow-diagnostic', 'sprint-diagnostic'],
|
|
648
|
+
description: '$499 Workflow Hardening Diagnostic for one repeated AI-agent workflow failure',
|
|
649
|
+
group: 'ops',
|
|
650
|
+
flags: [],
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
{
|
|
654
|
+
name: 'workflow',
|
|
655
|
+
aliases: ['swarm'],
|
|
656
|
+
description: 'Execute a dynamic parallel workflow for security audit, benchmarking, or exploration',
|
|
657
|
+
group: 'ops',
|
|
658
|
+
mcpTool: 'parallel_workflow',
|
|
659
|
+
flags: [
|
|
660
|
+
{ name: 'objective', type: 'string', required: true, description: 'The objective to plan and execute (e.g. security audit, performance benchmark)' },
|
|
661
|
+
{ name: 'concurrency', type: 'number', description: 'Maximum parallel subtasks (default 3)' },
|
|
662
|
+
{ name: 'timeoutMs', type: 'number', description: 'Timeout in milliseconds (default 60000)' },
|
|
663
|
+
{ name: 'json', type: 'boolean', description: 'Output results as JSON' },
|
|
664
|
+
],
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: 'check-update',
|
|
668
|
+
aliases: ['upgrade-check'],
|
|
669
|
+
description: 'Check for newer versions of ThumbGate from npm or GitHub',
|
|
670
|
+
group: 'ops',
|
|
671
|
+
flags: [
|
|
672
|
+
{ name: 'json', type: 'boolean', description: 'Output results as JSON' },
|
|
673
|
+
],
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: 'self-update',
|
|
677
|
+
aliases: ['upgrade-cli'],
|
|
678
|
+
description: 'Automatically install the latest version of ThumbGate globally',
|
|
679
|
+
group: 'ops',
|
|
680
|
+
flags: [],
|
|
681
|
+
},
|
|
585
682
|
];
|
|
586
683
|
|
|
587
684
|
/**
|
package/scripts/cli-telemetry.js
CHANGED
|
@@ -10,6 +10,9 @@ const _DEFAULT_TELEMETRY_HOST = 'https://thumbgate-production.up.railway.app';
|
|
|
10
10
|
const TELEMETRY_ENDPOINT = `${process.env.THUMBGATE_API_URL || _DEFAULT_TELEMETRY_HOST}/v1/telemetry/ping`;
|
|
11
11
|
const INSTALL_ID_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thumbgate', 'install-id');
|
|
12
12
|
|
|
13
|
+
// Session ID: random per process invocation. Groups all events from one CLI run.
|
|
14
|
+
const SESSION_ID = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Get or create a stable anonymous install ID.
|
|
15
18
|
* This is NOT tied to any personal info — it's a random UUID stored locally.
|
|
@@ -61,7 +64,9 @@ function trackEvent(eventType, metadata = {}) {
|
|
|
61
64
|
const payload = JSON.stringify({
|
|
62
65
|
eventType,
|
|
63
66
|
installId: getInstallId(),
|
|
67
|
+
sessionId: SESSION_ID,
|
|
64
68
|
visitorType: classifyInstall(),
|
|
69
|
+
clientType: 'cli',
|
|
65
70
|
platform: os.platform(),
|
|
66
71
|
arch: os.arch(),
|
|
67
72
|
nodeVersion: process.version,
|
|
@@ -87,4 +92,4 @@ function trackEvent(eventType, metadata = {}) {
|
|
|
87
92
|
} catch (_) {} // never crash the CLI
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
module.exports = { trackEvent, getInstallId, classifyInstall, INSTALL_ID_PATH };
|
|
95
|
+
module.exports = { trackEvent, getInstallId, classifyInstall, INSTALL_ID_PATH, SESSION_ID };
|
|
@@ -14,7 +14,12 @@ const TEAM_ANNUAL_PRICE_DOLLARS = 588;
|
|
|
14
14
|
const TEAM_MIN_SEATS = 3;
|
|
15
15
|
|
|
16
16
|
const PRO_PRICE_LABEL = '$19/mo or $149/yr (individual)';
|
|
17
|
-
|
|
17
|
+
// Enterprise is the contact-sales tier (absorbs the former Team workflow + the
|
|
18
|
+
// regulated-industry lane). Pricing is scoped after intake — no self-serve seat
|
|
19
|
+
// price is surfaced. The dormant TEAM_* Stripe constants below remain only as
|
|
20
|
+
// inert billing plumbing pending a dedicated cleanup; they are no longer a
|
|
21
|
+
// customer-facing tier.
|
|
22
|
+
const ENTERPRISE_PRICE_LABEL = 'Custom pricing, scoped after intake — Enterprise agent governance';
|
|
18
23
|
|
|
19
24
|
function normalizePlanId(value) {
|
|
20
25
|
const text = String(value || '').trim().toLowerCase();
|
|
@@ -35,6 +40,78 @@ function normalizeSeatCount(value, fallback = TEAM_MIN_SEATS) {
|
|
|
35
40
|
return Math.max(TEAM_MIN_SEATS, Math.round(parsed));
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
function trackedProUrl(source = 'cli_receipt', content = 'value_receipt') {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(PRO_MONTHLY_PAYMENT_LINK);
|
|
46
|
+
url.searchParams.set('utm_source', source);
|
|
47
|
+
url.searchParams.set('utm_medium', 'cli');
|
|
48
|
+
url.searchParams.set('utm_campaign', 'pro_conversion');
|
|
49
|
+
url.searchParams.set('utm_content', content);
|
|
50
|
+
return url.toString();
|
|
51
|
+
} catch (_) {
|
|
52
|
+
return PRO_MONTHLY_PAYMENT_LINK;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
57
|
+
return Number(count) === 1 ? singular : plural;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildCaptureReceipt({ signal, feedbackId, memoryId, actionType } = {}) {
|
|
61
|
+
const normalizedSignal = String(signal || '').toUpperCase() || 'UNKNOWN';
|
|
62
|
+
const lines = [
|
|
63
|
+
'',
|
|
64
|
+
'Value receipt',
|
|
65
|
+
'─'.repeat(50),
|
|
66
|
+
` Stored proof : ${normalizedSignal} feedback${feedbackId ? ` (${feedbackId})` : ''}`,
|
|
67
|
+
memoryId ? ` Local memory : ${memoryId}` : ' Local memory : saved locally',
|
|
68
|
+
actionType ? ` Rule pressure : ${actionType}` : ' Rule pressure : available for promotion',
|
|
69
|
+
' Free today : this proof protects this local machine',
|
|
70
|
+
' Pro sync : keep this lesson, rule, and dashboard synced across machines and agent runtimes',
|
|
71
|
+
' Next proof : npx thumbgate stats',
|
|
72
|
+
' Cost proof : npx thumbgate cost',
|
|
73
|
+
'',
|
|
74
|
+
` Solo Pro : ${PRO_PRICE_LABEL} for hosted sync, search, dashboard, and exports`,
|
|
75
|
+
` Upgrade : ${trackedProUrl('cli_capture_receipt', actionType || normalizedSignal.toLowerCase())}`,
|
|
76
|
+
` Enterprise : ${ENTERPRISE_PRICE_LABEL}; start with one repeated workflow failure`,
|
|
77
|
+
' https://thumbgate.ai/#workflow-sprint-intake',
|
|
78
|
+
'',
|
|
79
|
+
];
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildStatsReceipt(stats = {}) {
|
|
84
|
+
const negatives = Number(stats.negatives || stats.totalNegative || 0);
|
|
85
|
+
const blocked = Number(stats.gatesBlocked || stats.blocked || 0);
|
|
86
|
+
const warned = Number(stats.gatesWarned || stats.warned || 0);
|
|
87
|
+
const gates = Number(stats.totalGates || 0);
|
|
88
|
+
const autoPromoted = Number(stats.autoPromotedGates || 0);
|
|
89
|
+
const hasFriction = negatives > 0 || blocked > 0 || warned > 0 || gates > 0;
|
|
90
|
+
if (!hasFriction) return '';
|
|
91
|
+
|
|
92
|
+
const interventions = blocked + warned;
|
|
93
|
+
const lines = [
|
|
94
|
+
'',
|
|
95
|
+
'Paid-intent next step',
|
|
96
|
+
'─'.repeat(50),
|
|
97
|
+
];
|
|
98
|
+
if (interventions > 0) {
|
|
99
|
+
lines.push(` Proof already seen : ${interventions} gate ${pluralize(interventions, 'intervention')}`);
|
|
100
|
+
}
|
|
101
|
+
if (gates > 0) {
|
|
102
|
+
lines.push(` Active prevention : ${gates} ${pluralize(gates, 'gate')} (${autoPromoted} auto-promoted)`);
|
|
103
|
+
}
|
|
104
|
+
if (negatives > 0) {
|
|
105
|
+
lines.push(` Failure pressure : ${negatives} negative ${pluralize(negatives, 'signal')}`);
|
|
106
|
+
}
|
|
107
|
+
lines.push(' Show the buyer : npx thumbgate cost');
|
|
108
|
+
lines.push(' Pro sync value : keep these lessons/rules visible across laptops, CI, containers, and agent runtimes');
|
|
109
|
+
lines.push(` Solo Pro : ${trackedProUrl('cli_stats_receipt', 'proof_seen')}`);
|
|
110
|
+
lines.push(' Enterprise : https://thumbgate.ai/#workflow-sprint-intake');
|
|
111
|
+
lines.push('');
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
38
115
|
module.exports = {
|
|
39
116
|
PRO_MONTHLY_PAYMENT_LINK,
|
|
40
117
|
PRO_ANNUAL_PAYMENT_LINK,
|
|
@@ -47,8 +124,11 @@ module.exports = {
|
|
|
47
124
|
TEAM_ANNUAL_PRICE_DOLLARS,
|
|
48
125
|
TEAM_MIN_SEATS,
|
|
49
126
|
PRO_PRICE_LABEL,
|
|
50
|
-
|
|
127
|
+
ENTERPRISE_PRICE_LABEL,
|
|
51
128
|
normalizePlanId,
|
|
52
129
|
normalizeBillingCycle,
|
|
53
130
|
normalizeSeatCount,
|
|
131
|
+
buildCaptureReceipt,
|
|
132
|
+
buildStatsReceipt,
|
|
133
|
+
trackedProUrl,
|
|
54
134
|
};
|