thumbgate 1.22.0 → 1.23.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +1 -0
- package/adapters/chatgpt/openapi.yaml +10 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +212 -30
- package/config/post-deploy-marketing-pages.json +5 -0
- package/openapi/openapi.yaml +10 -0
- package/package.json +13 -3
- package/public/agents-cost-savings.html +151 -0
- package/public/ai-malpractice-prevention.html +489 -0
- package/public/codex-plugin.html +1 -1
- package/public/index.html +34 -3
- package/public/numbers.html +2 -2
- package/public/pricing.html +1 -1
- package/public/pro.html +22 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +72 -0
- package/scripts/gates-engine.js +119 -6
- package/scripts/meta-agent-loop.js +32 -0
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/rate-limiter.js +7 -1
- package/scripts/self-healing-check.js +193 -0
- package/scripts/silent-failure-cluster.js +512 -0
- package/scripts/telemetry-analytics.js +38 -0
- package/src/api/server.js +252 -36
package/public/index.html
CHANGED
|
@@ -19,7 +19,7 @@ __GOOGLE_SITE_VERIFICATION_META__
|
|
|
19
19
|
<meta property="og:image" content="https://thumbgate-production.up.railway.app/og.png">
|
|
20
20
|
<meta name="twitter:card" content="summary_large_image">
|
|
21
21
|
<meta name="twitter:image" content="https://thumbgate-production.up.railway.app/og.png">
|
|
22
|
-
<meta name="thumbgate-version" content="1.
|
|
22
|
+
<meta name="thumbgate-version" content="1.23.1">
|
|
23
23
|
<meta name="keywords" content="ThumbGate, thumbgate, AI agent orchestration, AI experience orchestration, agent enforcement layer, save LLM tokens, reduce Claude API cost, reduce OpenAI cost, AI agent token savings, prevent LLM retries, prevent hallucination retries, stop AI token waste, pre-action checks, agent governance, Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode, workflow hardening, context engineering, AI authenticity, brand authenticity AI">
|
|
24
24
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
25
25
|
|
|
@@ -721,7 +721,7 @@ __GA_BOOTSTRAP__
|
|
|
721
721
|
</div>
|
|
722
722
|
<a href="/go/install?utm_source=website&utm_medium=hero_cta&utm_campaign=install_free&cta_id=hero_install_cli&cta_placement=hero" onclick="event.preventDefault(); navigator.clipboard.writeText('npx thumbgate init'); this.textContent='Copied ✓ — paste in your repo'; setTimeout(()=>{this.textContent='Install Free CLI'},2000); try{posthog.capture('hero_install_click',{cta:'install_cli'})}catch(_){}" class="btn-gpt-page btn-install-hero" title="Click to copy: npx thumbgate init">Install Free CLI</a>
|
|
723
723
|
<a href="#workflow-sprint-intake" onclick="try{posthog.capture('hero_sprint_click',{cta:'sprint_intake'})}catch(_){};sendFirstPartyTelemetry('hero_sprint_intake_started',{ctaId:'hero_workflow_sprint',ctaPlacement:'hero',offer:'workflow_sprint'});" class="btn-pro-page hero-pro">Talk to me — Workflow Hardening Sprint →</a>
|
|
724
|
-
<a href="#demo" onclick="try{posthog.capture('hero_demo_click',{cta:'
|
|
724
|
+
<a href="#demo" onclick="try{posthog.capture('hero_demo_click',{cta:'see_enforcement'})}catch(_){};sendFirstPartyTelemetry('hero_demo_clicked',{ctaId:'hero_see_enforcement',ctaPlacement:'hero'});" class="btn-free" style="font-size:15px;padding:14px 22px;">See the enforcement in action</a>
|
|
725
725
|
</div>
|
|
726
726
|
|
|
727
727
|
<div class="hero-trust-bar">
|
|
@@ -817,6 +817,37 @@ __GA_BOOTSTRAP__
|
|
|
817
817
|
</div>
|
|
818
818
|
</section>
|
|
819
819
|
|
|
820
|
+
<section class="compatibility" id="deterministic-prevention">
|
|
821
|
+
<div class="container">
|
|
822
|
+
<div class="section-label">Deterministic Prevention</div>
|
|
823
|
+
<h2 class="section-title">Native thumbs are a black box. ThumbGate is the inspectable control layer.</h2>
|
|
824
|
+
<p style="text-align:center;font-size:16px;color:var(--text-muted);max-width:900px;margin:0 auto 28px;line-height:1.7;">Codex, Claude Code, ChatGPT, and other agent surfaces can collect preference signals, but you usually cannot see exactly what changed, which rule will fire, or why a future tool call is allowed. ThumbGate keeps the prevention layer outside the model: typed feedback becomes a local lesson, repeated mistakes become explicit rules, and every block names the matched rule, source lesson, tool call, and audit event.</p>
|
|
825
|
+
<div class="agent-grid">
|
|
826
|
+
<div class="agent-card">
|
|
827
|
+
<h3>Black-box memory</h3>
|
|
828
|
+
<p>Native thumbs and vendor memories may improve future behavior, but they do not give teams a deterministic allow/block contract at the moment an agent touches files, terminals, APIs, or CI.</p>
|
|
829
|
+
</div>
|
|
830
|
+
<div class="agent-card">
|
|
831
|
+
<h3>Inspectable ThumbGate memory</h3>
|
|
832
|
+
<p>Lessons live in your ThumbGate store, can be searched, exported as JSONL or DPO pairs, and traced back to the exact correction that created the rule.</p>
|
|
833
|
+
</div>
|
|
834
|
+
<div class="agent-card">
|
|
835
|
+
<h3>Rules before execution</h3>
|
|
836
|
+
<p>The final decision is not another model opinion. ThumbGate checks tool name, arguments, working directory, command shape, confidence, and required evidence before the action runs.</p>
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
<div style="margin:26px auto 0;max-width:960px;border:1px solid rgba(34,211,238,0.18);border-radius:8px;background:rgba(34,211,238,0.05);padding:18px 20px;">
|
|
840
|
+
<h3 style="margin:0 0 10px;color:var(--text);font-size:18px;">Why this matters now</h3>
|
|
841
|
+
<ul style="margin:0;padding-left:20px;color:var(--text-muted);line-height:1.7;">
|
|
842
|
+
<li><strong>Agent security is now mainstream risk.</strong> Coding agents run shell commands, write files, query databases, and chain actions with developer permissions, so unattended autonomy needs a local policy boundary.</li>
|
|
843
|
+
<li><strong>MCP adoption is accelerating.</strong> More tools are becoming agent-callable through shared protocols, which means one cross-agent governance layer beats one-off prompt rules per app.</li>
|
|
844
|
+
<li><strong>Repeated failures waste cash and trust.</strong> Every repeat burns tokens, review time, and release confidence. ThumbGate turns the first correction into a reusable prevention check.</li>
|
|
845
|
+
</ul>
|
|
846
|
+
<p style="margin:12px 0 0;font-size:13px;color:var(--text-dim);">Sources to verify the market timing: <a href="https://www.docker.com/blog/ai-coding-agent-horror-stories-security-risks/" target="_blank" rel="noopener" style="color:var(--cyan);">Docker on AI coding agent security risks</a>, <a href="https://www.techradar.com/pro/how-ai-agents-are-wrecking-havoc-in-legacy-security-setups-and-enterprises-are-catching-up" target="_blank" rel="noopener" style="color:var(--cyan);">TechRadar on enterprise agent security pressure</a>, and <a href="https://www.techradar.com/pro/zendesk-becomes-the-latest-to-adopt-mcp-to-futureproof-customers-in-the-ai-first-era" target="_blank" rel="noopener" style="color:var(--cyan);">current MCP adoption coverage</a>.</p>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
</section>
|
|
850
|
+
|
|
820
851
|
<section class="marketing-deep-dive" style="padding:28px 0 10px;">
|
|
821
852
|
<div class="container" style="max-width:1240px;">
|
|
822
853
|
<div class="section-label">Status bar proof</div>
|
|
@@ -1492,7 +1523,7 @@ __GA_BOOTSTRAP__
|
|
|
1492
1523
|
<a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
|
|
1493
1524
|
<a href="/blog">Blog</a>
|
|
1494
1525
|
</div>
|
|
1495
|
-
<span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.
|
|
1526
|
+
<span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.23.1</span>
|
|
1496
1527
|
</div>
|
|
1497
1528
|
</footer>
|
|
1498
1529
|
|
package/public/numbers.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"alternateName": "thumbgate",
|
|
26
26
|
"applicationCategory": "DeveloperApplication",
|
|
27
27
|
"operatingSystem": "Cross-platform, Node.js >=18.18.0",
|
|
28
|
-
"softwareVersion": "1.
|
|
28
|
+
"softwareVersion": "1.23.1",
|
|
29
29
|
"url": "https://thumbgate-production.up.railway.app/numbers",
|
|
30
30
|
"dateModified": "2026-05-07",
|
|
31
31
|
"creator": {
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
<main class="container">
|
|
203
203
|
<h1>The Numbers</h1>
|
|
204
204
|
<p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
|
|
205
|
-
<div class="freshness">Updated: 2026-05-07 · Version 1.
|
|
205
|
+
<div class="freshness">Updated: 2026-05-07 · Version 1.23.1</div>
|
|
206
206
|
<div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
|
|
207
207
|
|
|
208
208
|
<h2>Gate enforcement</h2>
|
package/public/pricing.html
CHANGED
|
@@ -213,7 +213,7 @@ __GA_BOOTSTRAP__
|
|
|
213
213
|
<div class="container">
|
|
214
214
|
<a href="/" class="nav-logo">ThumbGate</a>
|
|
215
215
|
<div class="nav-links">
|
|
216
|
-
<a href="/#
|
|
216
|
+
<a href="/#how-it-works">Features</a>
|
|
217
217
|
<a href="/pricing" style="color:var(--text);">Pricing</a>
|
|
218
218
|
<a href="/guide">Guide</a>
|
|
219
219
|
<a href="/dashboard">Dashboard</a>
|
package/public/pro.html
CHANGED
|
@@ -815,6 +815,28 @@ __GA_BOOTSTRAP__
|
|
|
815
815
|
</div>
|
|
816
816
|
</section>
|
|
817
817
|
|
|
818
|
+
<section class="section" id="deterministic-loop">
|
|
819
|
+
<div class="container">
|
|
820
|
+
<div class="section-label">Why Pro now</div>
|
|
821
|
+
<h2 class="section-title">Black-box thumbs do not prove prevention. Pro gives the operator an audit loop.</h2>
|
|
822
|
+
<p class="section-intro">Native rating buttons can tell a vendor that an answer felt wrong. ThumbGate Pro gives you the operational record: the correction, the lesson, the rule, the blocked tool call, and the export path.</p>
|
|
823
|
+
<div class="grid-3">
|
|
824
|
+
<div class="feature-card">
|
|
825
|
+
<h3>Inspectable memory</h3>
|
|
826
|
+
<p>Search the exact lesson that came from a thumbs-down and see whether it is still active, warning-only, or blocking.</p>
|
|
827
|
+
</div>
|
|
828
|
+
<div class="feature-card">
|
|
829
|
+
<h3>Deterministic checks</h3>
|
|
830
|
+
<p>The enforcement layer evaluates tool name, arguments, working directory, command shape, confidence, and required evidence before the action runs.</p>
|
|
831
|
+
</div>
|
|
832
|
+
<div class="feature-card">
|
|
833
|
+
<h3>Exportable proof</h3>
|
|
834
|
+
<p>Take the same correction history into JSONL, DPO export, review packets, and team rollout conversations instead of trusting hidden memory.</p>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
</section>
|
|
839
|
+
|
|
818
840
|
<section class="section" id="why-pay">
|
|
819
841
|
<div class="container">
|
|
820
842
|
<div class="section-label">Why operators pay</div>
|
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 };
|
|
@@ -35,6 +35,75 @@ function normalizeSeatCount(value, fallback = TEAM_MIN_SEATS) {
|
|
|
35
35
|
return Math.max(TEAM_MIN_SEATS, Math.round(parsed));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function trackedProUrl(source = 'cli_receipt', content = 'value_receipt') {
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(PRO_MONTHLY_PAYMENT_LINK);
|
|
41
|
+
url.searchParams.set('utm_source', source);
|
|
42
|
+
url.searchParams.set('utm_medium', 'cli');
|
|
43
|
+
url.searchParams.set('utm_campaign', 'pro_conversion');
|
|
44
|
+
url.searchParams.set('utm_content', content);
|
|
45
|
+
return url.toString();
|
|
46
|
+
} catch (_) {
|
|
47
|
+
return PRO_MONTHLY_PAYMENT_LINK;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
52
|
+
return Number(count) === 1 ? singular : plural;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildCaptureReceipt({ signal, feedbackId, memoryId, actionType } = {}) {
|
|
56
|
+
const normalizedSignal = String(signal || '').toUpperCase() || 'UNKNOWN';
|
|
57
|
+
const lines = [
|
|
58
|
+
'',
|
|
59
|
+
'Value receipt',
|
|
60
|
+
'─'.repeat(50),
|
|
61
|
+
` Stored proof : ${normalizedSignal} feedback${feedbackId ? ` (${feedbackId})` : ''}`,
|
|
62
|
+
memoryId ? ` Local memory : ${memoryId}` : ' Local memory : saved locally',
|
|
63
|
+
actionType ? ` Rule pressure : ${actionType}` : ' Rule pressure : available for promotion',
|
|
64
|
+
' Next proof : npx thumbgate stats',
|
|
65
|
+
' Cost proof : npx thumbgate cost',
|
|
66
|
+
'',
|
|
67
|
+
` Solo Pro : ${PRO_PRICE_LABEL} for dashboard, search, exports, sync`,
|
|
68
|
+
` Upgrade : ${trackedProUrl('cli_capture_receipt', actionType || normalizedSignal.toLowerCase())}`,
|
|
69
|
+
` Team path : ${TEAM_PRICE_LABEL}; start with one repeated workflow failure`,
|
|
70
|
+
' https://thumbgate.ai/#workflow-sprint-intake',
|
|
71
|
+
'',
|
|
72
|
+
];
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildStatsReceipt(stats = {}) {
|
|
77
|
+
const negatives = Number(stats.negatives || stats.totalNegative || 0);
|
|
78
|
+
const blocked = Number(stats.gatesBlocked || stats.blocked || 0);
|
|
79
|
+
const warned = Number(stats.gatesWarned || stats.warned || 0);
|
|
80
|
+
const gates = Number(stats.totalGates || 0);
|
|
81
|
+
const autoPromoted = Number(stats.autoPromotedGates || 0);
|
|
82
|
+
const hasFriction = negatives > 0 || blocked > 0 || warned > 0 || gates > 0;
|
|
83
|
+
if (!hasFriction) return '';
|
|
84
|
+
|
|
85
|
+
const interventions = blocked + warned;
|
|
86
|
+
const lines = [
|
|
87
|
+
'',
|
|
88
|
+
'Paid-intent next step',
|
|
89
|
+
'─'.repeat(50),
|
|
90
|
+
];
|
|
91
|
+
if (interventions > 0) {
|
|
92
|
+
lines.push(` Proof already seen : ${interventions} gate ${pluralize(interventions, 'intervention')}`);
|
|
93
|
+
}
|
|
94
|
+
if (gates > 0) {
|
|
95
|
+
lines.push(` Active prevention : ${gates} ${pluralize(gates, 'gate')} (${autoPromoted} auto-promoted)`);
|
|
96
|
+
}
|
|
97
|
+
if (negatives > 0) {
|
|
98
|
+
lines.push(` Failure pressure : ${negatives} negative ${pluralize(negatives, 'signal')}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push(' Show the buyer : npx thumbgate cost');
|
|
101
|
+
lines.push(` Solo Pro : ${trackedProUrl('cli_stats_receipt', 'proof_seen')}`);
|
|
102
|
+
lines.push(' Team workflow : https://thumbgate.ai/#workflow-sprint-intake');
|
|
103
|
+
lines.push('');
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
38
107
|
module.exports = {
|
|
39
108
|
PRO_MONTHLY_PAYMENT_LINK,
|
|
40
109
|
PRO_ANNUAL_PAYMENT_LINK,
|
|
@@ -51,4 +120,7 @@ module.exports = {
|
|
|
51
120
|
normalizePlanId,
|
|
52
121
|
normalizeBillingCycle,
|
|
53
122
|
normalizeSeatCount,
|
|
123
|
+
buildCaptureReceipt,
|
|
124
|
+
buildStatsReceipt,
|
|
125
|
+
trackedProUrl,
|
|
54
126
|
};
|
package/scripts/gates-engine.js
CHANGED
|
@@ -7,7 +7,7 @@ const crypto = require('crypto');
|
|
|
7
7
|
const { execSync, execFileSync } = require('child_process');
|
|
8
8
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
9
9
|
|
|
10
|
-
const { isProTier, FREE_TIER_MAX_GATES } = require('./rate-limiter');
|
|
10
|
+
const { isProTier, isInTrialPeriod, FREE_TIER_MAX_GATES, FREE_TIER_DAILY_BLOCKS, todayKey } = require('./rate-limiter');
|
|
11
11
|
const {
|
|
12
12
|
DEFAULT_BASE_BRANCH,
|
|
13
13
|
evaluateOperationalIntegrity,
|
|
@@ -466,6 +466,69 @@ function recordStat(gateId, action, gate) {
|
|
|
466
466
|
}
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// Free-tier daily block cap
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Count today's gate blocks from stats. Free tier gets FREE_TIER_DAILY_BLOCKS
|
|
475
|
+
* blocks/day. After the limit, deny → warn + upgrade CTA so the action proceeds
|
|
476
|
+
* but the user sees they lost protection.
|
|
477
|
+
*/
|
|
478
|
+
function getTodayBlockCount() {
|
|
479
|
+
const stats = loadStats();
|
|
480
|
+
const today = todayKey();
|
|
481
|
+
if (!stats.dailyBlocks || !stats.dailyBlocks[today]) return 0;
|
|
482
|
+
return stats.dailyBlocks[today];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function incrementTodayBlockCount() {
|
|
486
|
+
const stats = loadStats();
|
|
487
|
+
const today = todayKey();
|
|
488
|
+
if (!stats.dailyBlocks) stats.dailyBlocks = {};
|
|
489
|
+
// Clean old dates (keep only last 7 days to prevent unbounded growth)
|
|
490
|
+
const keys = Object.keys(stats.dailyBlocks);
|
|
491
|
+
if (keys.length > 7) {
|
|
492
|
+
keys.sort();
|
|
493
|
+
for (const k of keys.slice(0, keys.length - 7)) {
|
|
494
|
+
delete stats.dailyBlocks[k];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
stats.dailyBlocks[today] = (stats.dailyBlocks[today] || 0) + 1;
|
|
498
|
+
saveStats(stats);
|
|
499
|
+
return stats.dailyBlocks[today];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* If the user is free-tier and has exceeded daily block limit, downgrade
|
|
504
|
+
* a deny result to a warn with an upgrade CTA. Returns null if no cap applies.
|
|
505
|
+
*/
|
|
506
|
+
function applyDailyBlockCap(denyResult) {
|
|
507
|
+
// Pro, trial, CI, and THUMBGATE_NO_RATE_LIMIT users are uncapped
|
|
508
|
+
if (isProTier()) return null;
|
|
509
|
+
if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
|
|
510
|
+
|
|
511
|
+
const todayCount = getTodayBlockCount();
|
|
512
|
+
if (todayCount < FREE_TIER_DAILY_BLOCKS) {
|
|
513
|
+
// Under limit: allow the block, increment counter
|
|
514
|
+
incrementTodayBlockCount();
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Over limit: downgrade deny → warn with upgrade CTA
|
|
519
|
+
const remaining = 0;
|
|
520
|
+
return {
|
|
521
|
+
decision: 'warn',
|
|
522
|
+
gate: denyResult.gate,
|
|
523
|
+
message: `⚠️ ${denyResult.message}\n\n🔓 Daily protection limit reached (${FREE_TIER_DAILY_BLOCKS}/${FREE_TIER_DAILY_BLOCKS} blocks used). This action was allowed through. Upgrade for unlimited protection: https://thumbgate.ai/go/pro`,
|
|
524
|
+
severity: denyResult.severity,
|
|
525
|
+
reasoning: (denyResult.reasoning || []).concat([
|
|
526
|
+
`Free-tier daily block limit (${FREE_TIER_DAILY_BLOCKS}) exceeded — deny downgraded to warn`,
|
|
527
|
+
]),
|
|
528
|
+
dailyBlockCapApplied: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
469
532
|
// ---------------------------------------------------------------------------
|
|
470
533
|
// Reasoning chain builder
|
|
471
534
|
// ---------------------------------------------------------------------------
|
|
@@ -1491,11 +1554,19 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1491
1554
|
});
|
|
1492
1555
|
|
|
1493
1556
|
if (gate.action === 'block') {
|
|
1557
|
+
const denyResult = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1558
|
+
// Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
|
|
1559
|
+
const cappedResult = applyDailyBlockCap(denyResult);
|
|
1560
|
+
if (cappedResult) {
|
|
1561
|
+
recordStat(gate.id, 'warn', gate);
|
|
1562
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
|
|
1563
|
+
auditToFeedback(auditRecord);
|
|
1564
|
+
return cappedResult;
|
|
1565
|
+
}
|
|
1494
1566
|
recordStat(gate.id, 'block', gate);
|
|
1495
|
-
const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1496
1567
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1497
1568
|
auditToFeedback(auditRecord);
|
|
1498
|
-
return
|
|
1569
|
+
return denyResult;
|
|
1499
1570
|
}
|
|
1500
1571
|
|
|
1501
1572
|
if (gate.action === 'approve') {
|
|
@@ -1653,11 +1724,19 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1653
1724
|
const reasoning = buildReasoning(gate, toolName, toolInput, matchDetails);
|
|
1654
1725
|
|
|
1655
1726
|
if (gate.action === 'block') {
|
|
1727
|
+
const denyResult = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1728
|
+
// Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
|
|
1729
|
+
const cappedResult = applyDailyBlockCap(denyResult);
|
|
1730
|
+
if (cappedResult) {
|
|
1731
|
+
recordStat(gate.id, 'warn', gate);
|
|
1732
|
+
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
|
|
1733
|
+
auditToFeedback(auditRecord);
|
|
1734
|
+
return cappedResult;
|
|
1735
|
+
}
|
|
1656
1736
|
recordStat(gate.id, 'block', gate);
|
|
1657
|
-
const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
|
|
1658
1737
|
const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
|
|
1659
1738
|
auditToFeedback(auditRecord);
|
|
1660
|
-
return
|
|
1739
|
+
return denyResult;
|
|
1661
1740
|
}
|
|
1662
1741
|
|
|
1663
1742
|
if (gate.action === 'approve') {
|
|
@@ -1856,6 +1935,35 @@ function buildReminderOutput(context) {
|
|
|
1856
1935
|
};
|
|
1857
1936
|
}
|
|
1858
1937
|
|
|
1938
|
+
// ---------------------------------------------------------------------------
|
|
1939
|
+
// Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
|
|
1940
|
+
// Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
|
|
1941
|
+
// Highest-intent moment — user just saw ThumbGate save them from a mistake.
|
|
1942
|
+
// ---------------------------------------------------------------------------
|
|
1943
|
+
|
|
1944
|
+
function buildBlockActionProCta() {
|
|
1945
|
+
try {
|
|
1946
|
+
if (process.env.THUMBGATE_NO_NUDGE === '1') return null;
|
|
1947
|
+
if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
|
|
1948
|
+
if (isProTier()) return null;
|
|
1949
|
+
if (isInTrialPeriod()) return null; // Already have full access
|
|
1950
|
+
|
|
1951
|
+
const stats = loadStats();
|
|
1952
|
+
const totalBlocks = stats.blocked || 0;
|
|
1953
|
+
if (totalBlocks < 5) return null; // Too early — let them experience the product
|
|
1954
|
+
|
|
1955
|
+
if (totalBlocks < 25) {
|
|
1956
|
+
return '\n\n💡 Pro: sync rules across machines + dashboard analytics → thumbgate.ai/go/pro';
|
|
1957
|
+
}
|
|
1958
|
+
if (totalBlocks < 100) {
|
|
1959
|
+
return `\n\n💡 ${totalBlocks} actions blocked. Pro keeps rules in sync everywhere → thumbgate.ai/go/pro ($19/mo)`;
|
|
1960
|
+
}
|
|
1961
|
+
return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use this → thumbgate.ai/go/pro`;
|
|
1962
|
+
} catch (_) {
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1859
1967
|
function formatOutput(result, behavioralContext) {
|
|
1860
1968
|
if (!result) {
|
|
1861
1969
|
// No gate matched — inject behavioral context if available
|
|
@@ -1874,11 +1982,12 @@ function formatOutput(result, behavioralContext) {
|
|
|
1874
1982
|
if (result.decision === 'deny') {
|
|
1875
1983
|
const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
|
|
1876
1984
|
const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
|
|
1985
|
+
const proCta = buildBlockActionProCta() || '';
|
|
1877
1986
|
return JSON.stringify({
|
|
1878
1987
|
hookSpecificOutput: {
|
|
1879
1988
|
...reminder,
|
|
1880
1989
|
permissionDecision: 'deny',
|
|
1881
|
-
permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}`,
|
|
1990
|
+
permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}${proCta}`,
|
|
1882
1991
|
},
|
|
1883
1992
|
});
|
|
1884
1993
|
}
|
|
@@ -2468,6 +2577,10 @@ module.exports = {
|
|
|
2468
2577
|
isRemoteSideEffectCommand,
|
|
2469
2578
|
evaluateLocalOnlyRemoteSideEffectGate,
|
|
2470
2579
|
PR_THREAD_RESOLUTION_ACTION,
|
|
2580
|
+
buildBlockActionProCta,
|
|
2581
|
+
applyDailyBlockCap,
|
|
2582
|
+
getTodayBlockCount,
|
|
2583
|
+
incrementTodayBlockCount,
|
|
2471
2584
|
};
|
|
2472
2585
|
|
|
2473
2586
|
// ---------------------------------------------------------------------------
|
|
@@ -282,6 +282,8 @@ function buildPromotedGate(candidate, metrics, runId) {
|
|
|
282
282
|
occurrences: metrics.hits,
|
|
283
283
|
promotedAt: new Date().toISOString(),
|
|
284
284
|
source: 'meta-agent',
|
|
285
|
+
// origin distinguishes silent-failure-clustered candidates from feedback-derived ones
|
|
286
|
+
origin: candidate.origin || 'user-feedback',
|
|
285
287
|
runId,
|
|
286
288
|
score: parseFloat(metrics.score.toFixed(3)),
|
|
287
289
|
hitRate: parseFloat(metrics.hitRate.toFixed(3)),
|
|
@@ -371,6 +373,34 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
|
|
|
371
373
|
candidates = generateCandidatesHeuristic(failures, blockPatterns);
|
|
372
374
|
}
|
|
373
375
|
|
|
376
|
+
// Tag existing-pipeline candidates with their origin so downstream precision
|
|
377
|
+
// measurement (silentFailureDerivedGates vs user-feedback-derived) is possible.
|
|
378
|
+
candidates = candidates.map((c) => (c.origin ? c : { ...c, origin: 'user-feedback' }));
|
|
379
|
+
|
|
380
|
+
// Step 3b: Silent-failure clustering — behind THUMBGATE_SILENT_FAILURE_CLUSTERING=1.
|
|
381
|
+
// Candidates flow through the SAME scoring / fp-rate eval below; we do not
|
|
382
|
+
// bypass any guardrail. Off by default to preserve existing behavior.
|
|
383
|
+
let silentFailureStats = null;
|
|
384
|
+
if (process.env.THUMBGATE_SILENT_FAILURE_CLUSTERING === '1') {
|
|
385
|
+
try {
|
|
386
|
+
const { generateSilentFailureCandidates } = require('./silent-failure-cluster');
|
|
387
|
+
const sfResult = generateSilentFailureCandidates({ feedbackLogPath });
|
|
388
|
+
silentFailureStats = sfResult.stats;
|
|
389
|
+
if (sfResult.candidates && sfResult.candidates.length > 0) {
|
|
390
|
+
candidates = candidates.concat(sfResult.candidates);
|
|
391
|
+
}
|
|
392
|
+
if (verbose) {
|
|
393
|
+
process.stdout.write(
|
|
394
|
+
`[meta-agent] silent-failure-cluster: candidates=${sfResult.candidates.length} `
|
|
395
|
+
+ `failed=${sfResult.stats.failedCalls} clusters=${sfResult.stats.clusters} `
|
|
396
|
+
+ `skipped=${sfResult.stats.skippedReason || 'none'}\n`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (verbose) process.stdout.write(`[meta-agent] silent-failure-cluster failed (non-fatal): ${err.message}\n`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
374
404
|
if (verbose) {
|
|
375
405
|
process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
|
|
376
406
|
}
|
|
@@ -507,6 +537,8 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
|
|
|
507
537
|
skipped: evolutionResult.skipped || false,
|
|
508
538
|
}
|
|
509
539
|
: null,
|
|
540
|
+
silentFailureCluster: silentFailureStats,
|
|
541
|
+
silentFailureDerivedGates: promotedGates.filter((g) => g.origin === 'silent-failure-cluster').length,
|
|
510
542
|
};
|
|
511
543
|
|
|
512
544
|
if (!dryRun) {
|
|
@@ -16,7 +16,7 @@ const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
|
|
|
16
16
|
* 2. Env var: THUMBGATE_DEV_BYPASS=[set via THUMBGATE_DEV_SECRET env var]
|
|
17
17
|
* Requires a specific non-obvious value (not boolean) to prevent accidental activation.
|
|
18
18
|
*/
|
|
19
|
-
function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
|
|
19
|
+
function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE || os.homedir() } = {}) {
|
|
20
20
|
// Layer 1: env var with specific value
|
|
21
21
|
if (CREATOR_BYPASS_VALUE && String(env[CREATOR_BYPASS_ENV] || '') === CREATOR_BYPASS_VALUE) {
|
|
22
22
|
return true;
|
|
@@ -37,7 +37,7 @@ function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
|
|
|
37
37
|
* with any non-empty bypass value. No env var needed — just the config file.
|
|
38
38
|
* Used by the server to skip auth on localhost during local development.
|
|
39
39
|
*/
|
|
40
|
-
function hasDevOverride(homeDir = os.homedir()) {
|
|
40
|
+
function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
41
41
|
// Disabled during test runs to avoid interfering with auth assertions
|
|
42
42
|
if (process.env.NODE_TEST_CONTEXT || process.env.THUMBGATE_TESTING) return false;
|
|
43
43
|
try {
|
|
@@ -47,11 +47,11 @@ function hasDevOverride(homeDir = os.homedir()) {
|
|
|
47
47
|
} catch { return false; }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function getLicenseDir(homeDir = os.homedir()) {
|
|
50
|
+
function getLicenseDir(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
51
51
|
return path.join(homeDir, '.thumbgate');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function getLicensePath(homeDir = os.homedir()) {
|
|
54
|
+
function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
55
55
|
return path.join(getLicenseDir(homeDir), 'license.json');
|
|
56
56
|
}
|
|
57
57
|
|
package/scripts/rate-limiter.js
CHANGED
|
@@ -29,6 +29,7 @@ const FREE_TIER_LIMITS = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const FREE_TIER_MAX_GATES = 5; // 5 active prevention rules on free; Pro is unlimited
|
|
32
|
+
const FREE_TIER_DAILY_BLOCKS = 10; // 10 gate blocks/day on free; after limit, deny → warn + upgrade CTA
|
|
32
33
|
|
|
33
34
|
const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
|
|
34
35
|
|
|
@@ -45,7 +46,10 @@ function getInstallAgeDays() {
|
|
|
45
46
|
try {
|
|
46
47
|
const { INSTALL_ID_PATH } = require('./cli-telemetry');
|
|
47
48
|
if (!fs.existsSync(INSTALL_ID_PATH)) return null;
|
|
48
|
-
|
|
49
|
+
// Use mtimeMs — birthtimeMs is unreliable on Linux (ext4 doesn't backdate creation time).
|
|
50
|
+
// The install-id file is written once at install, so mtime == creation time in practice.
|
|
51
|
+
const stat = fs.statSync(INSTALL_ID_PATH);
|
|
52
|
+
const created = stat.mtimeMs || stat.birthtimeMs;
|
|
49
53
|
if (!Number.isFinite(created) || created <= 0) return null;
|
|
50
54
|
return (Date.now() - created) / (1000 * 60 * 60 * 24);
|
|
51
55
|
} catch (_) {
|
|
@@ -211,6 +215,7 @@ function getUsage(action, authContext) {
|
|
|
211
215
|
module.exports = {
|
|
212
216
|
checkLimit,
|
|
213
217
|
getUsage,
|
|
218
|
+
getInstallAgeDays,
|
|
214
219
|
isProTier,
|
|
215
220
|
isInTrialPeriod,
|
|
216
221
|
trialDaysRemaining,
|
|
@@ -219,6 +224,7 @@ module.exports = {
|
|
|
219
224
|
todayKey,
|
|
220
225
|
FREE_TIER_LIMITS,
|
|
221
226
|
FREE_TIER_MAX_GATES,
|
|
227
|
+
FREE_TIER_DAILY_BLOCKS,
|
|
222
228
|
TRIAL_DAYS,
|
|
223
229
|
UPGRADE_MESSAGE,
|
|
224
230
|
PAYWALL_MESSAGES,
|