thumbgate 1.18.0 → 1.20.0
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 +32 -10
- 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 +39 -2
- package/config/model-candidates.json +31 -0
- package/package.json +33 -8
- package/public/compare.html +12 -0
- package/public/federal.html +375 -0
- package/public/guide.html +2 -2
- package/public/index.html +61 -5
- package/public/learn.html +43 -0
- package/public/numbers.html +2 -2
- package/public/pro.html +3 -22
- package/scripts/activation-tracker.js +127 -0
- package/scripts/auto-promote-gates.js +153 -7
- package/scripts/auto-wire-hooks.js +50 -29
- package/scripts/cli-feedback.js +9 -1
- package/scripts/feedback-loop.js +14 -1
- package/scripts/memory-scope-readiness.js +315 -0
- package/scripts/plausible-server-events.js +162 -0
- package/scripts/rate-limiter.js +11 -0
- package/scripts/seo-gsd.js +75 -2
- package/scripts/statusline-links.js +2 -0
- package/scripts/telemetry-analytics.js +1 -0
- package/src/api/server.js +536 -109
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const REQUIRED_SCOPE_FIELDS = ['entityId', 'projectId', 'processId', 'sessionId'];
|
|
5
|
+
|
|
6
|
+
const FIELD_ALIASES = {
|
|
7
|
+
entityId: [
|
|
8
|
+
'entityId',
|
|
9
|
+
'userId',
|
|
10
|
+
'user_id',
|
|
11
|
+
'accountId',
|
|
12
|
+
'tenantId',
|
|
13
|
+
'actorId',
|
|
14
|
+
'scope.entityId',
|
|
15
|
+
'scope.userId',
|
|
16
|
+
'scope.user_id',
|
|
17
|
+
'metadata.entityId',
|
|
18
|
+
'metadata.userId',
|
|
19
|
+
'metadata.user_id',
|
|
20
|
+
'metadata.tenantId',
|
|
21
|
+
'richContext.entityId',
|
|
22
|
+
'richContext.userId',
|
|
23
|
+
'context.entityId',
|
|
24
|
+
'context.userId',
|
|
25
|
+
],
|
|
26
|
+
projectId: [
|
|
27
|
+
'projectId',
|
|
28
|
+
'project_id',
|
|
29
|
+
'project',
|
|
30
|
+
'repoId',
|
|
31
|
+
'workspaceId',
|
|
32
|
+
'scope.projectId',
|
|
33
|
+
'scope.project_id',
|
|
34
|
+
'scope.project',
|
|
35
|
+
'metadata.projectId',
|
|
36
|
+
'metadata.project_id',
|
|
37
|
+
'metadata.project',
|
|
38
|
+
'metadata.repoId',
|
|
39
|
+
'metadata.workspaceId',
|
|
40
|
+
'richContext.projectId',
|
|
41
|
+
'richContext.project',
|
|
42
|
+
'context.projectId',
|
|
43
|
+
'context.project',
|
|
44
|
+
],
|
|
45
|
+
processId: [
|
|
46
|
+
'processId',
|
|
47
|
+
'process_id',
|
|
48
|
+
'agentId',
|
|
49
|
+
'agent_id',
|
|
50
|
+
'role',
|
|
51
|
+
'scope.processId',
|
|
52
|
+
'scope.process_id',
|
|
53
|
+
'scope.agentId',
|
|
54
|
+
'scope.agent_id',
|
|
55
|
+
'metadata.processId',
|
|
56
|
+
'metadata.process_id',
|
|
57
|
+
'metadata.agentId',
|
|
58
|
+
'metadata.agent_id',
|
|
59
|
+
'metadata.role',
|
|
60
|
+
'richContext.processId',
|
|
61
|
+
'richContext.agentId',
|
|
62
|
+
'context.processId',
|
|
63
|
+
'context.agentId',
|
|
64
|
+
],
|
|
65
|
+
sessionId: [
|
|
66
|
+
'sessionId',
|
|
67
|
+
'session_id',
|
|
68
|
+
'conversationId',
|
|
69
|
+
'threadId',
|
|
70
|
+
'runId',
|
|
71
|
+
'scope.sessionId',
|
|
72
|
+
'scope.session_id',
|
|
73
|
+
'metadata.sessionId',
|
|
74
|
+
'metadata.session_id',
|
|
75
|
+
'metadata.conversationId',
|
|
76
|
+
'metadata.threadId',
|
|
77
|
+
'metadata.runId',
|
|
78
|
+
'richContext.sessionId',
|
|
79
|
+
'richContext.conversationId',
|
|
80
|
+
'context.sessionId',
|
|
81
|
+
'context.conversationId',
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function readPath(value, dottedPath) {
|
|
86
|
+
const segments = dottedPath.split('.');
|
|
87
|
+
let current = value;
|
|
88
|
+
for (const segment of segments) {
|
|
89
|
+
if (!current || typeof current !== 'object' || !(segment in current)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
current = current[segment];
|
|
93
|
+
}
|
|
94
|
+
return current;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeId(value) {
|
|
98
|
+
if (value === null || value === undefined) return null;
|
|
99
|
+
const text = String(value).trim();
|
|
100
|
+
return text ? text : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeScope(input = {}) {
|
|
104
|
+
const scope = {};
|
|
105
|
+
for (const field of REQUIRED_SCOPE_FIELDS) {
|
|
106
|
+
let resolved = null;
|
|
107
|
+
for (const alias of FIELD_ALIASES[field]) {
|
|
108
|
+
resolved = normalizeId(readPath(input, alias));
|
|
109
|
+
if (resolved) break;
|
|
110
|
+
}
|
|
111
|
+
scope[field] = resolved;
|
|
112
|
+
}
|
|
113
|
+
return scope;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function missingScopeFields(scope) {
|
|
117
|
+
const normalized = normalizeScope(scope);
|
|
118
|
+
return REQUIRED_SCOPE_FIELDS.filter((field) => !normalized[field]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function memoryScopeKey(scope) {
|
|
122
|
+
const normalized = normalizeScope(scope);
|
|
123
|
+
if (missingScopeFields(normalized).length > 0) return null;
|
|
124
|
+
return REQUIRED_SCOPE_FIELDS.map((field) => `${field}:${normalized[field]}`).join('|');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isSharedMemory(record = {}) {
|
|
128
|
+
const visibility = normalizeId(
|
|
129
|
+
record.visibility
|
|
130
|
+
|| record.scope?.visibility
|
|
131
|
+
|| record.metadata?.visibility
|
|
132
|
+
|| record.metadata?.scope
|
|
133
|
+
);
|
|
134
|
+
return record.shared === true
|
|
135
|
+
|| record.scope?.shared === true
|
|
136
|
+
|| record.metadata?.shared === true
|
|
137
|
+
|| ['shared', 'global', 'public', 'team'].includes(String(visibility || '').toLowerCase());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function recordFingerprint(record = {}) {
|
|
141
|
+
const text = [
|
|
142
|
+
record.title,
|
|
143
|
+
record.content,
|
|
144
|
+
record.context,
|
|
145
|
+
record.whatWentWrong,
|
|
146
|
+
record.whatToChange,
|
|
147
|
+
record.whatWorked,
|
|
148
|
+
]
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join(' ')
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.replace(/\s+/g, ' ')
|
|
153
|
+
.trim();
|
|
154
|
+
return text || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildMemoryScopeReadinessReport(records = []) {
|
|
158
|
+
const byScope = new Map();
|
|
159
|
+
const byFingerprint = new Map();
|
|
160
|
+
const missingFieldsByRecord = [];
|
|
161
|
+
let sharedRecords = 0;
|
|
162
|
+
let readyRecords = 0;
|
|
163
|
+
|
|
164
|
+
records.forEach((record, index) => {
|
|
165
|
+
const scope = normalizeScope(record);
|
|
166
|
+
const missingFields = missingScopeFields(scope);
|
|
167
|
+
const shared = isSharedMemory(record);
|
|
168
|
+
const scopeKey = memoryScopeKey(scope);
|
|
169
|
+
const id = record.id || `record-${index}`;
|
|
170
|
+
|
|
171
|
+
if (shared) sharedRecords += 1;
|
|
172
|
+
if (missingFields.length === 0) readyRecords += 1;
|
|
173
|
+
else missingFieldsByRecord.push({ id, index, missingFields });
|
|
174
|
+
|
|
175
|
+
if (scopeKey) {
|
|
176
|
+
if (!byScope.has(scopeKey)) byScope.set(scopeKey, []);
|
|
177
|
+
byScope.get(scopeKey).push(id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fingerprint = recordFingerprint(record);
|
|
181
|
+
if (fingerprint && scopeKey && !shared) {
|
|
182
|
+
if (!byFingerprint.has(fingerprint)) byFingerprint.set(fingerprint, new Set());
|
|
183
|
+
byFingerprint.get(fingerprint).add(scopeKey);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const duplicateScopeKeys = [...byScope.entries()]
|
|
188
|
+
.filter(([, ids]) => ids.length > 1)
|
|
189
|
+
.map(([scopeKey, ids]) => ({ scopeKey, ids }));
|
|
190
|
+
|
|
191
|
+
const crossScopeDuplicates = [...byFingerprint.entries()]
|
|
192
|
+
.filter(([, scopeKeys]) => scopeKeys.size > 1)
|
|
193
|
+
.map(([fingerprint, scopeKeys]) => ({
|
|
194
|
+
fingerprint,
|
|
195
|
+
scopeKeys: [...scopeKeys].sort((a, b) => a.localeCompare(b)),
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
const unscopedRecords = missingFieldsByRecord.length;
|
|
199
|
+
const riskLevel = unscopedRecords > 0 || crossScopeDuplicates.length > 0 ? 'high' : 'low';
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
totalRecords: records.length,
|
|
203
|
+
readyRecords,
|
|
204
|
+
unscopedRecords,
|
|
205
|
+
sharedRecords,
|
|
206
|
+
isolatedScopeCount: byScope.size,
|
|
207
|
+
duplicateScopeKeys,
|
|
208
|
+
crossScopeDuplicates,
|
|
209
|
+
missingFieldsByRecord,
|
|
210
|
+
requiredFields: [...REQUIRED_SCOPE_FIELDS],
|
|
211
|
+
riskLevel,
|
|
212
|
+
ready: riskLevel === 'low',
|
|
213
|
+
recommendations: buildRecommendations({ unscopedRecords, crossScopeDuplicates }),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildRecommendations({ unscopedRecords, crossScopeDuplicates }) {
|
|
218
|
+
const recommendations = [];
|
|
219
|
+
if (unscopedRecords > 0) {
|
|
220
|
+
recommendations.push('Attach entityId, projectId, processId, and sessionId before writing memories.');
|
|
221
|
+
}
|
|
222
|
+
if (crossScopeDuplicates.length > 0) {
|
|
223
|
+
recommendations.push('Mark intentional shared memories explicitly; otherwise dedupe within the same scope only.');
|
|
224
|
+
}
|
|
225
|
+
if (recommendations.length === 0) {
|
|
226
|
+
recommendations.push('Scope posture is ready for multi-user and multi-session memory retrieval.');
|
|
227
|
+
}
|
|
228
|
+
return recommendations;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function selectRecordsForScope(records = [], requestedScope = {}, options = {}) {
|
|
232
|
+
const requested = normalizeScope(requestedScope);
|
|
233
|
+
const requestedKey = memoryScopeKey(requested);
|
|
234
|
+
if (!requestedKey) {
|
|
235
|
+
throw new Error(`requested scope missing fields: ${missingScopeFields(requested).join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const includeShared = options.includeShared !== false;
|
|
239
|
+
const allowed = [];
|
|
240
|
+
const blocked = [];
|
|
241
|
+
|
|
242
|
+
for (const record of records) {
|
|
243
|
+
const shared = isSharedMemory(record);
|
|
244
|
+
const recordKey = memoryScopeKey(record);
|
|
245
|
+
if (recordKey === requestedKey || (includeShared && shared)) {
|
|
246
|
+
allowed.push(record);
|
|
247
|
+
} else {
|
|
248
|
+
blocked.push(record);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
requestedScope: requested,
|
|
254
|
+
requestedKey,
|
|
255
|
+
allowed,
|
|
256
|
+
blocked,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function buildMemoriStyleBenchmarkRecords() {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
id: 'alice-agent-a-session-1',
|
|
264
|
+
entityId: 'alice',
|
|
265
|
+
projectId: 'thumbgate',
|
|
266
|
+
processId: 'agent-a',
|
|
267
|
+
sessionId: 'session-1',
|
|
268
|
+
content: 'Use the paid sprint checklist before changing checkout code.',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'bob-agent-a-session-1',
|
|
272
|
+
entityId: 'bob',
|
|
273
|
+
projectId: 'thumbgate',
|
|
274
|
+
processId: 'agent-a',
|
|
275
|
+
sessionId: 'session-1',
|
|
276
|
+
content: 'Bob private onboarding note.',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'alice-agent-b-session-1',
|
|
280
|
+
entityId: 'alice',
|
|
281
|
+
projectId: 'thumbgate',
|
|
282
|
+
processId: 'agent-b',
|
|
283
|
+
sessionId: 'session-1',
|
|
284
|
+
content: 'Agent B should use docs-only mode.',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: 'alice-agent-a-session-2',
|
|
288
|
+
entityId: 'alice',
|
|
289
|
+
projectId: 'thumbgate',
|
|
290
|
+
processId: 'agent-a',
|
|
291
|
+
sessionId: 'session-2',
|
|
292
|
+
content: 'Session 2 has a different migration plan.',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'team-shared-checkout-rule',
|
|
296
|
+
entityId: 'alice',
|
|
297
|
+
projectId: 'thumbgate',
|
|
298
|
+
processId: 'agent-a',
|
|
299
|
+
sessionId: 'session-1',
|
|
300
|
+
visibility: 'shared',
|
|
301
|
+
content: 'Shared rule: checkout mutations require audit evidence.',
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
REQUIRED_SCOPE_FIELDS,
|
|
308
|
+
buildMemoriStyleBenchmarkRecords,
|
|
309
|
+
buildMemoryScopeReadinessReport,
|
|
310
|
+
isSharedMemory,
|
|
311
|
+
memoryScopeKey,
|
|
312
|
+
missingScopeFields,
|
|
313
|
+
normalizeScope,
|
|
314
|
+
selectRecordsForScope,
|
|
315
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-side Plausible custom event emitter.
|
|
5
|
+
*
|
|
6
|
+
* Why: CLAUDE.md anti-lying directive + the 2026-05-13 revenue-ROI
|
|
7
|
+
* critique flagged improvement #2 — "0/50 checkout sessions completed
|
|
8
|
+
* and you don't know why." The full /checkout/pro funnel already writes
|
|
9
|
+
* to local JSONL via appendBestEffortTelemetry(), but Plausible (where
|
|
10
|
+
* we actually look at funnel charts) only sees page-views, not the
|
|
11
|
+
* email-submitted / stripe-redirect-started transitions.
|
|
12
|
+
*
|
|
13
|
+
* This helper POSTs to the Plausible events API
|
|
14
|
+
* https://plausible.io/api/event
|
|
15
|
+
* so the three funnel transitions show up alongside page-views in the
|
|
16
|
+
* same dashboard.
|
|
17
|
+
*
|
|
18
|
+
* Hard guarantees:
|
|
19
|
+
* - Fire-and-forget. Never throws. Never blocks a 302 redirect.
|
|
20
|
+
* - Disabled when THUMBGATE_PLAUSIBLE_DISABLE=1 (CI / tests / opt-out).
|
|
21
|
+
* - 2-second hard timeout. The request socket is unref-ed so the
|
|
22
|
+
* process can exit without waiting for the response.
|
|
23
|
+
* - No PII. Only event name + UTM/CTA metadata (already public).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const https = require('node:https');
|
|
27
|
+
|
|
28
|
+
const DEFAULT_PLAUSIBLE_DOMAIN = 'thumbgate-production.up.railway.app';
|
|
29
|
+
const PLAUSIBLE_ENDPOINT = 'https://plausible.io/api/event';
|
|
30
|
+
const REQUEST_TIMEOUT_MS = 2_000;
|
|
31
|
+
|
|
32
|
+
function isPlausibleDisabled() {
|
|
33
|
+
if (process.env.THUMBGATE_PLAUSIBLE_DISABLE === '1') return true;
|
|
34
|
+
if (process.env.DO_NOT_TRACK === '1') return true;
|
|
35
|
+
// NODE_ENV-based detection was tried and dropped: `node --test` does not
|
|
36
|
+
// set NODE_ENV automatically, so the check produced surprising behavior
|
|
37
|
+
// in test vs. production. Tests must opt out explicitly via the dedicated
|
|
38
|
+
// THUMBGATE_PLAUSIBLE_DISABLE flag.
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getPlausibleDomain() {
|
|
43
|
+
return process.env.THUMBGATE_PLAUSIBLE_DOMAIN || DEFAULT_PLAUSIBLE_DOMAIN;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Map raw telemetry fields onto Plausible "props" (string-only). Drop
|
|
48
|
+
* undefined values; cap each prop at 100 chars to satisfy Plausible's limits.
|
|
49
|
+
*/
|
|
50
|
+
function buildProps(metadata = {}) {
|
|
51
|
+
const props = {};
|
|
52
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
53
|
+
if (value === undefined || value === null || value === '') continue;
|
|
54
|
+
const str = typeof value === 'string' ? value : String(value);
|
|
55
|
+
if (str.length === 0) continue;
|
|
56
|
+
props[key] = str.length > 100 ? `${str.slice(0, 97)}...` : str;
|
|
57
|
+
}
|
|
58
|
+
return props;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildPayload(eventName, options = {}) {
|
|
62
|
+
const domain = options.domain || getPlausibleDomain();
|
|
63
|
+
const pageUrl = options.pageUrl || `https://${domain}${options.page || '/'}`;
|
|
64
|
+
return {
|
|
65
|
+
name: eventName,
|
|
66
|
+
url: pageUrl,
|
|
67
|
+
domain,
|
|
68
|
+
referrer: options.referrer || undefined,
|
|
69
|
+
props: buildProps(options.props || {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Send a custom event to Plausible. Always returns a Promise that resolves —
|
|
75
|
+
* the caller can `void plausibleEvent(...)` and ignore. Resolved value is
|
|
76
|
+
* { sent, reason } so tests can assert the dispatch path was taken.
|
|
77
|
+
*/
|
|
78
|
+
function plausibleEvent(eventName, options = {}) {
|
|
79
|
+
if (isPlausibleDisabled()) {
|
|
80
|
+
return Promise.resolve({ sent: false, reason: 'disabled' });
|
|
81
|
+
}
|
|
82
|
+
if (typeof eventName !== 'string' || !eventName.trim()) {
|
|
83
|
+
return Promise.resolve({ sent: false, reason: 'invalid_event_name' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const payload = JSON.stringify(buildPayload(eventName, options));
|
|
87
|
+
const url = new URL(PLAUSIBLE_ENDPOINT);
|
|
88
|
+
const userAgent =
|
|
89
|
+
options.userAgent ||
|
|
90
|
+
`thumbgate-server/${process.env.npm_package_version || 'unknown'} (+https://github.com/IgorGanapolsky/ThumbGate)`;
|
|
91
|
+
const xff = options.forwardedFor || options.remoteAddr;
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
let settled = false;
|
|
95
|
+
const done = (result) => {
|
|
96
|
+
if (settled) return;
|
|
97
|
+
settled = true;
|
|
98
|
+
resolve(result);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const req = https.request(
|
|
103
|
+
{
|
|
104
|
+
hostname: url.hostname,
|
|
105
|
+
port: 443,
|
|
106
|
+
path: url.pathname,
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
111
|
+
'User-Agent': userAgent,
|
|
112
|
+
...(xff ? { 'X-Forwarded-For': xff } : {}),
|
|
113
|
+
},
|
|
114
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
115
|
+
},
|
|
116
|
+
(res) => {
|
|
117
|
+
// Plausible returns 202 on accept. Drain the response body so the
|
|
118
|
+
// socket can be released back to the pool, then resolve.
|
|
119
|
+
res.on('data', () => {});
|
|
120
|
+
res.on('end', () => done({ sent: true, statusCode: res.statusCode }));
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
req.on('error', () => done({ sent: false, reason: 'request_error' }));
|
|
124
|
+
req.on('timeout', () => {
|
|
125
|
+
req.destroy();
|
|
126
|
+
done({ sent: false, reason: 'timeout' });
|
|
127
|
+
});
|
|
128
|
+
req.on('socket', (s) => s.unref());
|
|
129
|
+
req.end(payload);
|
|
130
|
+
} catch {
|
|
131
|
+
done({ sent: false, reason: 'exception' });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convenience: fire the three /checkout/pro funnel events by name without
|
|
138
|
+
* each caller having to know the canonical Plausible event labels.
|
|
139
|
+
* The labels are chosen to read cleanly in the Plausible UI's event list.
|
|
140
|
+
*/
|
|
141
|
+
const CHECKOUT_EVENT_NAMES = Object.freeze({
|
|
142
|
+
view: 'Checkout Pro Viewed',
|
|
143
|
+
emailSubmitted: 'Checkout Pro Email Submitted',
|
|
144
|
+
stripeRedirect: 'Checkout Pro Stripe Redirect Started',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function recordCheckoutFunnelEvent(stage, options = {}) {
|
|
148
|
+
const name = CHECKOUT_EVENT_NAMES[stage];
|
|
149
|
+
if (!name) return Promise.resolve({ sent: false, reason: 'unknown_stage' });
|
|
150
|
+
return plausibleEvent(name, options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
plausibleEvent,
|
|
155
|
+
recordCheckoutFunnelEvent,
|
|
156
|
+
buildPayload,
|
|
157
|
+
buildProps,
|
|
158
|
+
isPlausibleDisabled,
|
|
159
|
+
getPlausibleDomain,
|
|
160
|
+
CHECKOUT_EVENT_NAMES,
|
|
161
|
+
PLAUSIBLE_ENDPOINT,
|
|
162
|
+
};
|
package/scripts/rate-limiter.js
CHANGED
|
@@ -42,6 +42,17 @@ const PAYWALL_MESSAGES = {
|
|
|
42
42
|
function isProTier(authContext) {
|
|
43
43
|
if (authContext && authContext.tier === 'pro') return true;
|
|
44
44
|
if (process.env.THUMBGATE_API_KEY || process.env.THUMBGATE_PRO_MODE === '1' || process.env.THUMBGATE_NO_RATE_LIMIT === '1') return true;
|
|
45
|
+
// Creator/dogfooding bypass: when the owner has the dev secret + bypass
|
|
46
|
+
// configured (env or ~/.config/thumbgate/dev.json), treat the install as Pro
|
|
47
|
+
// so marketing nudges and rate limits stop firing on the maintainer's own
|
|
48
|
+
// machine.
|
|
49
|
+
try {
|
|
50
|
+
const { isCreatorDev } = require('./pro-local-dashboard');
|
|
51
|
+
if (isCreatorDev()) return true;
|
|
52
|
+
} catch (_) {
|
|
53
|
+
// pro-local-dashboard unavailable in this runtime — fall through to
|
|
54
|
+
// license/free-tier handling rather than failing the check.
|
|
55
|
+
}
|
|
45
56
|
try {
|
|
46
57
|
const { isProLicensed } = require('./license');
|
|
47
58
|
if (isProLicensed()) return true;
|
package/scripts/seo-gsd.js
CHANGED
|
@@ -120,6 +120,11 @@ const HIGH_ROI_QUERY_SEEDS = [
|
|
|
120
120
|
95,
|
|
121
121
|
'Bottom-of-funnel service query that turns background-agent governance demand into a paid 48-hour Team intake and implementation wedge.',
|
|
122
122
|
),
|
|
123
|
+
querySeed(
|
|
124
|
+
'ai deployment readiness',
|
|
125
|
+
95,
|
|
126
|
+
'Production AI deployment demand maps directly to ThumbGate readiness audits, pre-action gates, rollout proof, and paid workflow hardening services.',
|
|
127
|
+
),
|
|
123
128
|
querySeed(
|
|
124
129
|
'gpt-5.5 model evaluation',
|
|
125
130
|
94,
|
|
@@ -1064,6 +1069,69 @@ function buildAiAgentGovernanceSprintGuide() {
|
|
|
1064
1069
|
};
|
|
1065
1070
|
}
|
|
1066
1071
|
|
|
1072
|
+
const AI_DEPLOYMENT_READINESS_GUIDE_SPEC = Object.freeze({
|
|
1073
|
+
slug: 'ai-deployment-readiness',
|
|
1074
|
+
meta: {
|
|
1075
|
+
query: 'ai deployment readiness',
|
|
1076
|
+
title: 'AI Deployment Readiness | Production Workflow Gates Before Rollout',
|
|
1077
|
+
heroTitle: 'AI Deployment Readiness Before Agents Touch Production',
|
|
1078
|
+
heroSummary: 'Deployment-company demand proves the buyer problem: teams need help moving AI into real workflows. ThumbGate is the governance and proof layer that makes one priority workflow ready for production with pre-action gates, rollout evidence, and paid sprint paths.',
|
|
1079
|
+
},
|
|
1080
|
+
takeaways: [
|
|
1081
|
+
'AI deployment readiness is about workflow controls, not just prompts, demos, or model selection.',
|
|
1082
|
+
'The high-ROI starting point is one priority workflow with mapped tools, data, owners, risky actions, and proof-backed gates.',
|
|
1083
|
+
'ThumbGate turns deployment demand into revenue through a $499 diagnostic, a $1500 Workflow Hardening Sprint, and Team seats at $49/seat/mo.',
|
|
1084
|
+
],
|
|
1085
|
+
sections: [
|
|
1086
|
+
['paragraphs', 'Why deployment is now the buying category', [
|
|
1087
|
+
'OpenAI-style deployment companies validate a practical market shift: buyers do not only want a model or chatbot. They want AI systems embedded into messy production workflows where data, tools, approvals, and rollback paths already exist.',
|
|
1088
|
+
'That creates a clean ThumbGate wedge. A forward deployment team can map the workflow and build the automation; ThumbGate defines what the automation is allowed to execute, what evidence must exist before it acts, and what proof the buyer can review after rollout.',
|
|
1089
|
+
]],
|
|
1090
|
+
['bullets', 'What ThumbGate adds to AI deployment readiness', [
|
|
1091
|
+
'Intake for one workflow owner, one priority workflow, one repeated failure, and one production rollout target.',
|
|
1092
|
+
'A governance map for data access, tools, protected files, risky commands, approval boundaries, review tiers, and rollback expectations.',
|
|
1093
|
+
'Pre-action gates that stop repeated mistakes before the next shell command, PR, release, or production automation step.',
|
|
1094
|
+
'Background-agent risk checks through npx thumbgate background-governance --check --json before unattended work reaches review.',
|
|
1095
|
+
'A proof pack with blocked-repeat examples, run evidence, verification notes, and rollout decisions the buyer can share internally.',
|
|
1096
|
+
]],
|
|
1097
|
+
['paragraphs', 'How this makes money', [
|
|
1098
|
+
'The page should convert deployment curiosity into concrete buying motion. The $499 Workflow Hardening Diagnostic validates one repeated failure and produces a readiness map. The $1500 sprint implements the first gates and proof pack. Team seats expand the same enforcement path across more repos and workflow owners.',
|
|
1099
|
+
'This keeps the offer honest. ThumbGate does not claim to be the deployment company or a generic AI consultancy. It is the enforcement, governance, and proof layer that lets deployment work survive contact with production.',
|
|
1100
|
+
]],
|
|
1101
|
+
],
|
|
1102
|
+
faq: [
|
|
1103
|
+
[
|
|
1104
|
+
'Is ThumbGate competing with AI deployment companies?',
|
|
1105
|
+
'No. Deployment companies help buyers embed AI into real operations. ThumbGate complements that work by adding the governance and proof layer before AI agents or automations touch production systems.',
|
|
1106
|
+
],
|
|
1107
|
+
[
|
|
1108
|
+
'What should an AI deployment readiness audit produce?',
|
|
1109
|
+
'A useful audit should produce one workflow map, named owners, tool and data boundaries, risky actions, approval rules, rollback expectations, pre-action gates, and proof that the first rollout can be reviewed.',
|
|
1110
|
+
],
|
|
1111
|
+
[
|
|
1112
|
+
'Can this start before procurement?',
|
|
1113
|
+
'Yes. Start with one $499 diagnostic or the Workflow Hardening Sprint intake. The point is to prove one workflow before asking a buyer to fund a broad platform rollout.',
|
|
1114
|
+
],
|
|
1115
|
+
],
|
|
1116
|
+
relatedPaths: ['/guides/ai-agent-governance-sprint', '/guides/background-agent-governance', '/guides/pre-action-checks'],
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
function buildAiDeploymentReadinessGuide() {
|
|
1120
|
+
return {
|
|
1121
|
+
...preActionGuide(AI_DEPLOYMENT_READINESS_GUIDE_SPEC.slug, {
|
|
1122
|
+
...AI_DEPLOYMENT_READINESS_GUIDE_SPEC.meta,
|
|
1123
|
+
takeaways: AI_DEPLOYMENT_READINESS_GUIDE_SPEC.takeaways,
|
|
1124
|
+
sections: AI_DEPLOYMENT_READINESS_GUIDE_SPEC.sections.map(([kind, heading, entries]) => buildSectionFromSpec(kind, heading, entries)),
|
|
1125
|
+
faq: AI_DEPLOYMENT_READINESS_GUIDE_SPEC.faq.map(([question, text]) => answer(question, text)),
|
|
1126
|
+
relatedPaths: AI_DEPLOYMENT_READINESS_GUIDE_SPEC.relatedPaths,
|
|
1127
|
+
}),
|
|
1128
|
+
cta: {
|
|
1129
|
+
label: 'Start deployment readiness intake',
|
|
1130
|
+
href: '/?utm_source=website&utm_medium=seo_page&utm_campaign=ai_deployment_readiness&cta_placement=seo_brief&plan_id=team#workflow-sprint-intake',
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1067
1135
|
const MODEL_UPGRADE_EVALUATION_GUIDE_SPEC = Object.freeze({
|
|
1068
1136
|
slug: 'gpt-5-5-model-evaluation',
|
|
1069
1137
|
meta: {
|
|
@@ -1590,6 +1658,7 @@ const PAGE_BLUEPRINTS = [
|
|
|
1590
1658
|
buildPromptTricksToWorkflowRulesGuide(),
|
|
1591
1659
|
buildBackgroundAgentGovernanceGuide(),
|
|
1592
1660
|
buildAiAgentGovernanceSprintGuide(),
|
|
1661
|
+
buildAiDeploymentReadinessGuide(),
|
|
1593
1662
|
buildModelUpgradeEvaluationGuide(),
|
|
1594
1663
|
{
|
|
1595
1664
|
query: 'stop ai coding agents from repeating mistakes',
|
|
@@ -2102,7 +2171,7 @@ function classifyIntent(query) {
|
|
|
2102
2171
|
if (!normalized) return 'informational';
|
|
2103
2172
|
if (/\b(vs|versus|alternative|compare|comparison|better than)\b/.test(normalized)) return 'comparison';
|
|
2104
2173
|
if (/\b(price|pricing|buy|checkout|purchase|cost)\b/.test(normalized)) return 'transactional';
|
|
2105
|
-
if (/\b(autoresearch|self-improving|benchmark|reward hacking|agent safety|governance|sprint)\b/.test(normalized)) return 'commercial';
|
|
2174
|
+
if (/\b(autoresearch|self-improving|benchmark|reward hacking|agent safety|governance|deployment readiness|sprint)\b/.test(normalized)) return 'commercial';
|
|
2106
2175
|
if (/\b(claude code|cursor|codex|gemini|amp|opencode|integration|plugin|setup|install)\b/.test(normalized)) {
|
|
2107
2176
|
return 'commercial';
|
|
2108
2177
|
}
|
|
@@ -2594,7 +2663,11 @@ function renderWebPageJsonLd(page, runtimeConfig) {
|
|
|
2594
2663
|
}
|
|
2595
2664
|
|
|
2596
2665
|
function renderPaidSprintCheckoutCard(page) {
|
|
2597
|
-
|
|
2666
|
+
const paidSprintGuidePaths = new Set([
|
|
2667
|
+
'/guides/ai-agent-governance-sprint',
|
|
2668
|
+
'/guides/ai-deployment-readiness',
|
|
2669
|
+
]);
|
|
2670
|
+
if (!paidSprintGuidePaths.has(page.path)) return '';
|
|
2598
2671
|
|
|
2599
2672
|
return `<div class="sidebar-card paid-sprint-card">
|
|
2600
2673
|
<h2>Ready to buy the sprint?</h2>
|
|
@@ -99,6 +99,8 @@ function launchLocalServer(options = {}) {
|
|
|
99
99
|
THUMBGATE_LOCAL_API_ORIGIN: origin.origin,
|
|
100
100
|
THUMBGATE_PROJECT_DIR: projectDir,
|
|
101
101
|
THUMBGATE_PRO_MODE: '1',
|
|
102
|
+
THUMBGATE_BUILD_SHA: env.THUMBGATE_BUILD_SHA || 'local-runtime',
|
|
103
|
+
THUMBGATE_BUILD_GENERATED_AT: env.THUMBGATE_BUILD_GENERATED_AT || new Date().toISOString(),
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
if (resolvedKey && resolvedKey.key) {
|
|
@@ -330,6 +330,7 @@ function sanitizeTelemetryPayload(payload = {}, headers = {}) {
|
|
|
330
330
|
failureCode: pickFirstText(raw.failureCode),
|
|
331
331
|
httpStatus: normalizeInteger(raw.httpStatus),
|
|
332
332
|
userAgent: pickFirstText(raw.userAgent, headers['user-agent']),
|
|
333
|
+
isBot: pickFirstText(raw.isBot),
|
|
333
334
|
attributionTagged: Boolean(
|
|
334
335
|
pickFirstText(raw.utmSource, raw.utmMedium, raw.utmCampaign, raw.utmContent, raw.utmTerm)
|
|
335
336
|
),
|