thumbgate 1.5.4 → 1.6.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/CHANGELOG.md +198 -0
- package/README.md +7 -6
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +25 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +4 -4
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +61 -5
- package/openapi/openapi.yaml +25 -0
- package/package.json +16 -3
- package/public/codex-plugin.html +277 -0
- package/public/dashboard.html +193 -13
- package/public/index.html +150 -48
- package/public/learn.html +13 -2
- package/public/lessons.html +5 -2
- package/public/pro.html +8 -1
- package/scripts/auto-wire-hooks.js +71 -6
- package/scripts/billing.js +503 -8
- package/scripts/contextfs.js +1 -1
- package/scripts/dashboard.js +249 -0
- package/scripts/gates-engine.js +153 -2
- package/scripts/hook-runtime.js +42 -0
- package/scripts/llm-client.js +25 -10
- package/scripts/mailer/index.js +13 -0
- package/scripts/mailer/resend-mailer.js +350 -0
- package/scripts/mcp-config.js +13 -0
- package/scripts/published-cli.js +8 -0
- package/scripts/seo-gsd.js +118 -4
- package/scripts/statusline.sh +8 -0
- package/scripts/vector-store.js +21 -7
- package/src/api/server.js +126 -23
package/src/api/server.js
CHANGED
|
@@ -152,6 +152,9 @@ const {
|
|
|
152
152
|
} = require('../../scripts/decision-journal');
|
|
153
153
|
const {
|
|
154
154
|
generateDashboard,
|
|
155
|
+
buildReviewSnapshot,
|
|
156
|
+
readDashboardReviewState,
|
|
157
|
+
writeDashboardReviewState,
|
|
155
158
|
} = require('../../scripts/dashboard');
|
|
156
159
|
const {
|
|
157
160
|
buildDashboardRenderSpec,
|
|
@@ -216,6 +219,7 @@ const PRO_PAGE_PATH = path.resolve(__dirname, '../../public/pro.html');
|
|
|
216
219
|
const DASHBOARD_PAGE_PATH = path.resolve(__dirname, '../../public/dashboard.html');
|
|
217
220
|
const LESSONS_PAGE_PATH = path.resolve(__dirname, '../../public/lessons.html');
|
|
218
221
|
const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
|
|
222
|
+
const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugin.html');
|
|
219
223
|
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
220
224
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
221
225
|
const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
|
|
@@ -1703,7 +1707,8 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); line
|
|
|
1703
1707
|
.container { max-width: 860px; margin: 0 auto; padding: 0 24px; }
|
|
1704
1708
|
nav { position: sticky; top: 0; z-index: 50; background: rgba(10,10,11,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); padding: 14px 0; }
|
|
1705
1709
|
nav .container { display: flex; justify-content: space-between; align-items: center; }
|
|
1706
|
-
.nav-logo { font-weight: 700; font-size: 15px; color: var(--text); text-decoration: none; }
|
|
1710
|
+
.nav-logo { font-weight: 700; font-size: 15px; color: var(--text); text-decoration: none; display: inline-flex; align-items: center; gap: 8px; }
|
|
1711
|
+
.nav-logo .logo-mark { width: 28px; height: 28px; display: block; }
|
|
1707
1712
|
.nav-links { display: flex; gap: 16px; align-items: center; }
|
|
1708
1713
|
.nav-links a { color: var(--text-muted); text-decoration: none; font-size: 13px; }
|
|
1709
1714
|
.nav-links a:hover { color: var(--text); }
|
|
@@ -1748,7 +1753,7 @@ nav .container { display: flex; justify-content: space-between; align-items: cen
|
|
|
1748
1753
|
</head>
|
|
1749
1754
|
<body>
|
|
1750
1755
|
<nav><div class="container">
|
|
1751
|
-
<a href="/dashboard" class="nav-logo"
|
|
1756
|
+
<a href="/dashboard" class="nav-logo"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="28" height="28"><span class="logo-text">ThumbGate</span></a>
|
|
1752
1757
|
<div class="nav-links">
|
|
1753
1758
|
<a href="/dashboard">Dashboard</a>
|
|
1754
1759
|
<a href="/lessons">Lessons</a>
|
|
@@ -1896,8 +1901,11 @@ function renderRobotsTxt(runtimeConfig) {
|
|
|
1896
1901
|
function renderSitemapXml(runtimeConfig) {
|
|
1897
1902
|
const entries = [
|
|
1898
1903
|
{ path: '/', changefreq: 'weekly', priority: '1.0' },
|
|
1899
|
-
|
|
1904
|
+
// /pro consolidated into /#pro-pitch (2026-04-16) — removed from sitemap
|
|
1905
|
+
// so search engines don't chase the 301 instead of indexing the canonical
|
|
1906
|
+
// homepage directly.
|
|
1900
1907
|
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
1908
|
+
{ path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
|
|
1901
1909
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
1902
1910
|
];
|
|
1903
1911
|
return [
|
|
@@ -2020,6 +2028,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2020
2028
|
--accent-dark: #8f451f;
|
|
2021
2029
|
--card: #fffdf9;
|
|
2022
2030
|
--success: #2f7d4b;
|
|
2031
|
+
--warning: #8f451f;
|
|
2023
2032
|
--radius: 14px;
|
|
2024
2033
|
}
|
|
2025
2034
|
* { box-sizing: border-box; }
|
|
@@ -2071,6 +2080,15 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2071
2080
|
font-weight: 700;
|
|
2072
2081
|
margin-bottom: 8px;
|
|
2073
2082
|
}
|
|
2083
|
+
.email-status {
|
|
2084
|
+
color: var(--muted);
|
|
2085
|
+
font-size: 14px;
|
|
2086
|
+
margin-top: 10px;
|
|
2087
|
+
}
|
|
2088
|
+
.email-status.warning {
|
|
2089
|
+
color: var(--warning);
|
|
2090
|
+
font-weight: 700;
|
|
2091
|
+
}
|
|
2074
2092
|
pre {
|
|
2075
2093
|
white-space: pre-wrap;
|
|
2076
2094
|
word-break: break-word;
|
|
@@ -2106,11 +2124,26 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2106
2124
|
color: var(--muted);
|
|
2107
2125
|
font-size: 14px;
|
|
2108
2126
|
}
|
|
2127
|
+
.brand-header {
|
|
2128
|
+
display: flex;
|
|
2129
|
+
align-items: center;
|
|
2130
|
+
gap: 10px;
|
|
2131
|
+
margin-bottom: 28px;
|
|
2132
|
+
text-decoration: none;
|
|
2133
|
+
color: var(--ink);
|
|
2134
|
+
font-weight: 700;
|
|
2135
|
+
font-size: 16px;
|
|
2136
|
+
letter-spacing: -0.01em;
|
|
2137
|
+
}
|
|
2138
|
+
.brand-header .logo-mark { width: 32px; height: 32px; display: block; }
|
|
2109
2139
|
</style>
|
|
2140
|
+
<link rel="icon" type="image/png" href="/thumbgate-icon.png">
|
|
2141
|
+
<link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
|
|
2110
2142
|
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
2111
2143
|
</head>
|
|
2112
2144
|
<body>
|
|
2113
2145
|
<main>
|
|
2146
|
+
<a href="/" class="brand-header"><img src="/assets/brand/thumbgate-mark-inline.svg" alt="ThumbGate" class="logo-mark" width="32" height="32"><span class="logo-text">ThumbGate</span></a>
|
|
2114
2147
|
<span class="eyebrow">ThumbGate Pro</span>
|
|
2115
2148
|
<h1>Your local Pro dashboard is ready.</h1>
|
|
2116
2149
|
<p class="lead">This page verifies your Stripe session, provisions the key if needed, and gives you the exact command to save your license and launch your personal local dashboard.</p>
|
|
@@ -2118,6 +2151,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2118
2151
|
<div class="card">
|
|
2119
2152
|
<div class="status" id="status">Verifying payment and provisioning your key...</div>
|
|
2120
2153
|
<p class="muted" id="summary">Do not close this tab until the key appears.</p>
|
|
2154
|
+
<p class="email-status" id="email-status">Activation email pending checkout verification.</p>
|
|
2121
2155
|
<pre id="key-block">Waiting for checkout session...</pre>
|
|
2122
2156
|
</div>
|
|
2123
2157
|
|
|
@@ -2129,11 +2163,26 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2129
2163
|
</div>
|
|
2130
2164
|
|
|
2131
2165
|
<div class="card">
|
|
2132
|
-
<h2>
|
|
2166
|
+
<h2>Use ThumbGate from CI, teammates, and remote agents (optional)</h2>
|
|
2167
|
+
<p>The Hosted API lets anything that can make an HTTP request — CI jobs, GitHub Actions, teammates' laptops, scheduled cron, or agents running in Docker or Lambda — push feedback into the same memory pool your local dashboard already reads from.</p>
|
|
2168
|
+
|
|
2169
|
+
<p><strong>When you need this:</strong></p>
|
|
2170
|
+
<ul>
|
|
2171
|
+
<li>You run agents in CI/CD, GitHub Actions, or Docker containers and want their failures captured automatically.</li>
|
|
2172
|
+
<li>Your team wants shared memory — every teammate's thumbs-down feeds the same prevention rules.</li>
|
|
2173
|
+
<li>You dispatch agents from servers, Lambdas, or scheduled jobs that never touch your laptop.</li>
|
|
2174
|
+
</ul>
|
|
2175
|
+
|
|
2176
|
+
<p><strong>When you can skip this:</strong></p>
|
|
2177
|
+
<ul>
|
|
2178
|
+
<li>You only use ThumbGate from your own laptop — the local dashboard already handles everything.</li>
|
|
2179
|
+
</ul>
|
|
2180
|
+
|
|
2181
|
+
<p><strong>How to set it up:</strong></p>
|
|
2133
2182
|
<ol>
|
|
2134
|
-
<li>Copy the environment block below into your
|
|
2135
|
-
<li>Use the curl example to confirm the hosted API captures an event.</li>
|
|
2136
|
-
<li>
|
|
2183
|
+
<li>Copy the environment block below into your CI or server environment.</li>
|
|
2184
|
+
<li>Use the curl example to confirm the hosted API captures an event end-to-end.</li>
|
|
2185
|
+
<li>Treat the key like any other API secret — rotate via your billing portal if it leaks.</li>
|
|
2137
2186
|
</ol>
|
|
2138
2187
|
<pre id="env-block">Waiting for provisioning...</pre>
|
|
2139
2188
|
<pre id="curl-block">Waiting for provisioning...</pre>
|
|
@@ -2152,6 +2201,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2152
2201
|
const telemetryEndpoint = '/v1/telemetry/ping';
|
|
2153
2202
|
const statusEl = document.getElementById('status');
|
|
2154
2203
|
const summaryEl = document.getElementById('summary');
|
|
2204
|
+
const emailStatusEl = document.getElementById('email-status');
|
|
2155
2205
|
const keyBlock = document.getElementById('key-block');
|
|
2156
2206
|
const envBlock = document.getElementById('env-block');
|
|
2157
2207
|
const curlBlock = document.getElementById('curl-block');
|
|
@@ -2269,9 +2319,23 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
2269
2319
|
sendTelemetryOnce('checkout_paid_confirmed');
|
|
2270
2320
|
statusEl.textContent = 'ThumbGate Pro activated.';
|
|
2271
2321
|
const resolvedTraceId = body.traceId || traceId || '';
|
|
2322
|
+
const emailStatus = body.trialEmail || {};
|
|
2323
|
+
const customerEmail = body.customerEmail || (emailStatus && emailStatus.customerEmail) || '';
|
|
2272
2324
|
summaryEl.textContent = resolvedTraceId
|
|
2273
2325
|
? 'Your Pro key is ready. Save it once, launch your local dashboard, and keep the optional hosted snippet for team workflows. Trace: ' + resolvedTraceId + '.'
|
|
2274
2326
|
: 'Your Pro key is ready. Save it once, launch your local dashboard, and keep the optional hosted snippet for team workflows.';
|
|
2327
|
+
if (emailStatus.status === 'sent' || emailStatus.status === 'already_sent') {
|
|
2328
|
+
emailStatusEl.className = 'email-status';
|
|
2329
|
+
emailStatusEl.textContent = customerEmail
|
|
2330
|
+
? 'Activation email sent to ' + customerEmail + '.'
|
|
2331
|
+
: 'Activation email sent.';
|
|
2332
|
+
} else if (emailStatus.status === 'skipped' || emailStatus.status === 'failed') {
|
|
2333
|
+
emailStatusEl.className = 'email-status warning';
|
|
2334
|
+
emailStatusEl.textContent = 'Email delivery is not confirmed. Copy the key below now; this page is your activation source of truth.';
|
|
2335
|
+
} else {
|
|
2336
|
+
emailStatusEl.className = 'email-status warning';
|
|
2337
|
+
emailStatusEl.textContent = 'Email delivery status is unknown. Copy the key below now.';
|
|
2338
|
+
}
|
|
2275
2339
|
keyBlock.textContent = body.apiKey || 'Provisioned, but no key was returned.';
|
|
2276
2340
|
activateBlock.textContent = body.apiKey
|
|
2277
2341
|
? 'npx thumbgate pro --activate --key=' + body.apiKey
|
|
@@ -3453,21 +3517,17 @@ async function addContext(){
|
|
|
3453
3517
|
}
|
|
3454
3518
|
|
|
3455
3519
|
if (isGetLikeRequest && pathname === '/pro') {
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
});
|
|
3468
|
-
} catch (err) {
|
|
3469
|
-
sendText(res, 500, err.message || 'Pro page unavailable');
|
|
3470
|
-
}
|
|
3520
|
+
// Consolidated: /pro content now lives inline on `/` as the #pro-pitch
|
|
3521
|
+
// strip (hero-adjacent pricing card). 301 so external links (README,
|
|
3522
|
+
// plugin manifests, guides, compare pages) pass link equity onto the
|
|
3523
|
+
// single canonical landing page. Query string is preserved so UTM
|
|
3524
|
+
// tracking from inbound campaigns still reaches GA/PostHog on `/`.
|
|
3525
|
+
const redirectTarget = `/#pro-pitch${parsed.search || ''}`;
|
|
3526
|
+
res.writeHead(301, {
|
|
3527
|
+
Location: redirectTarget,
|
|
3528
|
+
'Cache-Control': 'public, max-age=3600',
|
|
3529
|
+
});
|
|
3530
|
+
res.end();
|
|
3471
3531
|
return;
|
|
3472
3532
|
}
|
|
3473
3533
|
|
|
@@ -3481,6 +3541,16 @@ async function addContext(){
|
|
|
3481
3541
|
return;
|
|
3482
3542
|
}
|
|
3483
3543
|
|
|
3544
|
+
if (isGetLikeRequest && pathname === '/codex-plugin') {
|
|
3545
|
+
try {
|
|
3546
|
+
const html = fs.readFileSync(CODEX_PLUGIN_PAGE_PATH, 'utf-8');
|
|
3547
|
+
sendHtml(res, 200, html, {}, { headOnly: isHeadRequest });
|
|
3548
|
+
} catch {
|
|
3549
|
+
sendJson(res, 404, { error: 'Codex plugin page not found' });
|
|
3550
|
+
}
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3484
3554
|
if (isGetLikeRequest && pathname === '/compare') {
|
|
3485
3555
|
try {
|
|
3486
3556
|
const html = fs.readFileSync(COMPARE_PAGE_PATH, 'utf-8');
|
|
@@ -3577,6 +3647,7 @@ async function addContext(){
|
|
|
3577
3647
|
if (isGetLikeRequest && (
|
|
3578
3648
|
pathname === '/favicon.ico'
|
|
3579
3649
|
|| pathname === '/thumbgate-logo.png'
|
|
3650
|
+
|| pathname === '/thumbgate-icon.png'
|
|
3580
3651
|
|| pathname === '/og.png'
|
|
3581
3652
|
|| pathname === '/apple-touch-icon.png'
|
|
3582
3653
|
)) {
|
|
@@ -3591,7 +3662,7 @@ async function addContext(){
|
|
|
3591
3662
|
version: pkg.version,
|
|
3592
3663
|
status: 'ok',
|
|
3593
3664
|
docs: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
3594
|
-
endpoints: ['/health', '/dashboard', '/guide', '/
|
|
3665
|
+
endpoints: ['/health', '/dashboard', '/guide', '/codex-plugin', '/compare', '/learn', '/v1/feedback/capture', '/v1/feedback/stats', '/v1/feedback/summary', '/v1/lessons/search', '/v1/search', '/v1/documents', '/v1/documents/import', '/v1/documents/{documentId}', '/v1/dashboard', '/v1/dashboard/render-spec', '/v1/decisions/evaluate', '/v1/decisions/outcome', '/v1/decisions/metrics', '/v1/settings/status', '/v1/dpo/export', '/v1/jobs', '/v1/jobs/harness', '/v1/analytics/databricks/export'],
|
|
3595
3666
|
}, {}, {
|
|
3596
3667
|
headOnly: isHeadRequest,
|
|
3597
3668
|
});
|
|
@@ -3708,6 +3779,7 @@ async function addContext(){
|
|
|
3708
3779
|
installId: bootstrapBody.installId,
|
|
3709
3780
|
traceId,
|
|
3710
3781
|
metadata: analyticsMetadata,
|
|
3782
|
+
appOrigin: hostedConfig.appOrigin,
|
|
3711
3783
|
});
|
|
3712
3784
|
|
|
3713
3785
|
if (result.url) {
|
|
@@ -4332,6 +4404,7 @@ async function addContext(){
|
|
|
4332
4404
|
installId: body.installId,
|
|
4333
4405
|
traceId,
|
|
4334
4406
|
metadata: analyticsMetadata,
|
|
4407
|
+
appOrigin: hostedConfig.appOrigin,
|
|
4335
4408
|
});
|
|
4336
4409
|
sendJson(res, 200, {
|
|
4337
4410
|
...result,
|
|
@@ -5520,6 +5593,36 @@ async function addContext(){
|
|
|
5520
5593
|
return;
|
|
5521
5594
|
}
|
|
5522
5595
|
|
|
5596
|
+
// GET /v1/dashboard/review-state -- incremental review baseline and deltas
|
|
5597
|
+
if (req.method === 'GET' && pathname === '/v1/dashboard/review-state') {
|
|
5598
|
+
const reviewState = readDashboardReviewState(requestFeedbackDir);
|
|
5599
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
5600
|
+
reviewBaseline: reviewState,
|
|
5601
|
+
authContext: { tier: 'pro' },
|
|
5602
|
+
});
|
|
5603
|
+
sendJson(res, 200, {
|
|
5604
|
+
reviewState,
|
|
5605
|
+
reviewDelta: data.reviewDelta,
|
|
5606
|
+
});
|
|
5607
|
+
return;
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5610
|
+
// POST /v1/dashboard/review-state -- mark current dashboard state as reviewed
|
|
5611
|
+
if (req.method === 'POST' && pathname === '/v1/dashboard/review-state') {
|
|
5612
|
+
const snapshot = buildReviewSnapshot(requestFeedbackDir);
|
|
5613
|
+
writeDashboardReviewState(requestFeedbackDir, snapshot);
|
|
5614
|
+
const data = generateDashboard(requestFeedbackDir, {
|
|
5615
|
+
reviewBaseline: snapshot,
|
|
5616
|
+
authContext: { tier: 'pro' },
|
|
5617
|
+
});
|
|
5618
|
+
sendJson(res, 200, {
|
|
5619
|
+
ok: true,
|
|
5620
|
+
reviewState: snapshot,
|
|
5621
|
+
reviewDelta: data.reviewDelta,
|
|
5622
|
+
});
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5523
5626
|
// GET /v1/dashboard/render-spec -- Constrained hosted dashboard JSON spec
|
|
5524
5627
|
if (req.method === 'GET' && pathname === '/v1/dashboard/render-spec') {
|
|
5525
5628
|
let summaryOptions;
|