thumbgate 1.26.7 → 1.27.2
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/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +2 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +20 -9
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/mcp/server-stdio.js +28 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +147 -10
- package/bin/dashboard-cli.js +7 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +60 -0
- package/config/mcp-allowlists.json +8 -7
- package/config/model-candidates.json +71 -6
- package/package.json +26 -10
- package/public/chatgpt-app.html +330 -0
- package/public/codex-plugin.html +66 -14
- package/public/dashboard.html +203 -17
- package/public/index.html +79 -4
- package/public/learn.html +70 -0
- package/public/lessons.html +129 -6
- package/public/numbers.html +2 -2
- package/public/pricing.html +20 -2
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-schema.js +26 -0
- package/scripts/dashboard-chat.js +64 -17
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/gates-engine.js +258 -61
- package/scripts/hybrid-feedback-context.js +141 -7
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plausible-domain-config.js +86 -0
- package/scripts/plausible-server-events.js +4 -2
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +28 -12
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +80 -0
- package/scripts/seo-gsd.js +53 -0
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/tool-registry.js +37 -0
- package/scripts/workflow-sentinel.js +189 -4
- package/src/api/server.js +276 -10
package/src/api/server.js
CHANGED
|
@@ -96,6 +96,9 @@ const {
|
|
|
96
96
|
const {
|
|
97
97
|
recordCheckoutFunnelEvent,
|
|
98
98
|
} = require('../../scripts/plausible-server-events');
|
|
99
|
+
const {
|
|
100
|
+
resolvePlausibleDataDomain,
|
|
101
|
+
} = require('../../scripts/plausible-domain-config');
|
|
99
102
|
const {
|
|
100
103
|
buildCloudflareSandboxPlan,
|
|
101
104
|
} = require('../../scripts/cloudflare-dynamic-sandbox');
|
|
@@ -221,6 +224,7 @@ const PRO_PAGE_PATH = path.resolve(__dirname, '../../public/pro.html');
|
|
|
221
224
|
const DASHBOARD_PAGE_PATH = path.resolve(__dirname, '../../public/dashboard.html');
|
|
222
225
|
const LESSONS_PAGE_PATH = path.resolve(__dirname, '../../public/lessons.html');
|
|
223
226
|
const GUIDE_PAGE_PATH = path.resolve(__dirname, '../../public/guide.html');
|
|
227
|
+
const CHATGPT_APP_PAGE_PATH = path.resolve(__dirname, '../../public/chatgpt-app.html');
|
|
224
228
|
const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugin.html');
|
|
225
229
|
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
226
230
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
@@ -1703,7 +1707,8 @@ function buildCheckoutIntentHref(baseUrl, metadata = {}, overrides = {}) {
|
|
|
1703
1707
|
}
|
|
1704
1708
|
|
|
1705
1709
|
function renderCheckoutIntentPage() {
|
|
1706
|
-
|
|
1710
|
+
const plausibleDomain = escapeHtmlAttribute(resolvePlausibleDataDomain({ host: 'thumbgate.ai' }));
|
|
1711
|
+
return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm — ThumbGate Pro</title><script defer data-domain="${plausibleDomain}" src="https://plausible.io/js/script.tagged-events.js"></script><script>window.plausible=window.plausible||function(){(window.plausible.q=window.plausible.q||[]).push(arguments)};</script><style>body{background:#0a0a0a;color:#eee;font-family:system-ui,-apple-system,sans-serif;line-height:1.5}main{max-width:520px;margin:8vh auto;padding:0 20px}.brand{display:flex;align-items:center;gap:10px;margin-bottom:24px;font-size:14px;color:#94a3b8}.brand-mark{width:24px;height:24px;background:#22d3ee;border-radius:6px;display:inline-block}h1{font-size:24px;margin:0 0 8px;color:#fff}.price{font-size:32px;font-weight:700;color:#22d3ee;margin:8px 0 4px}.price small{font-size:14px;color:#94a3b8;font-weight:400}p{color:#cbd5e1;margin:8px 0}form{margin:0}input[type=email]{width:100%;box-sizing:border-box;padding:14px 16px;border:1px solid #374151;border-radius:8px;background:#111827;color:#fff;font-size:15px;margin:16px 0 0;outline:none}input[type=email]:focus{border-color:#22d3ee}input[type=email]::placeholder{color:#64748b}button.primary{background:#22d3ee;color:#000;padding:16px;text-align:center;border-radius:8px;font-weight:700;font-size:16px;margin:10px 0;border:none;cursor:pointer;width:100%}a{display:block;text-decoration:none}a.secondary{border:1px solid #374151;color:#cbd5e1;padding:12px;text-align:center;border-radius:8px;margin:8px 0 0;font-size:14px}.trust{margin:24px 0;padding:16px;border:1px solid #1f2937;border-radius:8px;background:#0f172a}.trust-item{font-size:13px;color:#cbd5e1;padding:4px 0;display:flex;gap:8px}.trust-item::before{content:"✓";color:#22d3ee;font-weight:700}.choice-note{font-size:13px;color:#94a3b8;margin-top:14px}.back{text-align:center;color:#64748b;font-size:12px;margin-top:24px}.back a{color:#64748b;display:inline}.email-note{font-size:12px;color:#64748b;margin:4px 0 0}</style><main><div class="brand"><span class="brand-mark"></span><span>ThumbGate</span></div><h1>Start ThumbGate Pro</h1><div class="price">$19<small>/mo</small></div><p>The npm package runs your gates locally. <strong>Pro</strong> is what keeps them working across every machine, every agent runtime, and every breaking-change week.</p><form action="/checkout/pro" method="GET" data-i="pro_checkout_confirmed"><input type="hidden" name="confirm" value="1"><input type="email" name="customer_email" placeholder="you@company.com" required autocomplete="email"><p class="email-note">Pre-fills your Stripe receipt. We only email if you ask.</p><button type="submit" class="primary">Pay $19/mo with Stripe →</button></form><a class="secondary" data-i="workflow_sprint_intake" href="/#workflow-sprint-intake">Not sure yet? Send the workflow first</a><p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p><div class="trust"><div class="trust-item">Lessons synced across all your machines — no local SQLite to babysit</div><div class="trust-item">Adapter matrix kept current for Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode — version drift is our problem, not yours</div><div class="trust-item">Hosted dashboard: gate stats, DPO export, org-wide rule library</div><div class="trust-item">24×7 ops on the rule engine — SonarCloud regressions fixed in <24h</div></div><p class="back"><a href="/">← Back to thumbgate.ai</a></p></main><script>document.querySelector('form').addEventListener('submit',e=>{if(navigator.sendBeacon)navigator.sendBeacon('/v1/telemetry/ping',new Blob([JSON.stringify({eventType:'checkout_interstitial_cta_clicked',clientType:'web',page:'/checkout/pro',ctaId:'pro_checkout_confirmed',ctaPlacement:'checkout_interstitial',customerEmail:document.querySelector('input[name=customer_email]').value})],{type:'application/json'}));try{window.plausible&&window.plausible('Checkout Pro Email Submitted',{props:{page:'/checkout/pro',source:'interstitial'}})}catch(_){}})</script></html>`;
|
|
1707
1712
|
}
|
|
1708
1713
|
|
|
1709
1714
|
function buildCheckoutBootstrapBody(parsed, req, journeyState = resolveJourneyState(req, parsed)) {
|
|
@@ -2119,9 +2124,10 @@ function normalizePublicMarketingHtml(html, runtimeConfig) {
|
|
|
2119
2124
|
output = output.replaceAll(DEFAULT_PUBLIC_APP_ORIGIN, appOrigin);
|
|
2120
2125
|
try {
|
|
2121
2126
|
const host = new URL(appOrigin).host;
|
|
2127
|
+
const plausibleDomain = resolvePlausibleDataDomain({ host });
|
|
2122
2128
|
output = output.replaceAll(
|
|
2123
2129
|
'data-domain="thumbgate-production.up.railway.app"',
|
|
2124
|
-
`data-domain="${escapeHtmlAttribute(
|
|
2130
|
+
`data-domain="${escapeHtmlAttribute(plausibleDomain)}"`
|
|
2125
2131
|
);
|
|
2126
2132
|
} catch {
|
|
2127
2133
|
// appOrigin is normalized by hosted-config; leave static analytics domains
|
|
@@ -2202,7 +2208,7 @@ function resolveLocalPageBootstrap(req, expectedApiKey) {
|
|
|
2202
2208
|
const localProBootstrap = process.env.THUMBGATE_PRO_MODE === '1' && Boolean(expectedApiKey) && isLoopbackHost(hostHeader);
|
|
2203
2209
|
const devOverride = expectedApiKey === null && isLoopbackHost(hostHeader);
|
|
2204
2210
|
const bootstrapActive = localProBootstrap || devOverride;
|
|
2205
|
-
const serializedBootstrapKey = JSON.stringify(localProBootstrap ? expectedApiKey : devOverride ? 'dev-override' : '').replace(/</g, '\\u003c');
|
|
2211
|
+
const serializedBootstrapKey = JSON.stringify(localProBootstrap ? expectedApiKey : devOverride ? (process.env.THUMBGATE_API_KEY || 'dev-override') : '').replace(/</g, '\\u003c');
|
|
2206
2212
|
|
|
2207
2213
|
return {
|
|
2208
2214
|
bootstrapActive,
|
|
@@ -2243,7 +2249,7 @@ window.THUMBGATE_DASHBOARD_BOOTSTRAP = { enabled: ${bootstrapActive ? 'true' : '
|
|
|
2243
2249
|
<p>This lightweight npm dashboard is bundled without marketing assets, so installs stay small while core feedback, lessons, and API routes remain available.</p>
|
|
2244
2250
|
<div class="grid">
|
|
2245
2251
|
<a class="card" href="/v1/dashboard"><strong>Dashboard JSON</strong><span>Inspect feedback totals, lesson counts, and Reliability Gateway health.</span></a>
|
|
2246
|
-
<a class="card" href="/v1/enterprise/dialogflow/status"><strong>Enterprise
|
|
2252
|
+
<a class="card" href="/v1/enterprise/dialogflow/status"><strong>Enterprise Data Chat</strong><span>Check Vertex/DFCX readiness and use /v1/enterprise/dialogflow/chat to query local ThumbGate data through the data-access guard. This does not claim a live Dialogflow CX agent unless deployment evidence is configured.</span></a>
|
|
2247
2253
|
<a class="card" href="/lessons"><strong>Lessons</strong><span>Review remembered thumbs-up/down lessons and enforcement context.</span></a>
|
|
2248
2254
|
<a class="card" href="/health"><strong>Health</strong><span>Verify the installed package version and runtime status.</span></a>
|
|
2249
2255
|
</div>
|
|
@@ -2341,10 +2347,63 @@ a{color:#22d3ee;text-decoration:none}</style></head><body>
|
|
|
2341
2347
|
const signal = normalizeLessonSignal(merged.signal);
|
|
2342
2348
|
const emoji = signal === 'up' ? '👍' : '👎';
|
|
2343
2349
|
const signalColor = signal === 'up' ? '#4ade80' : '#f87171';
|
|
2344
|
-
const
|
|
2345
|
-
const
|
|
2346
|
-
const
|
|
2347
|
-
const
|
|
2350
|
+
const rawTitle = merged.title || merged.context || 'Untitled Lesson';
|
|
2351
|
+
const rawContext = merged.context || '';
|
|
2352
|
+
const rawWhatWentWrong = merged.whatWentWrong || '';
|
|
2353
|
+
const rawWhatWorked = merged.whatWorked || '';
|
|
2354
|
+
|
|
2355
|
+
function cleanTitle(titleText) {
|
|
2356
|
+
if (!titleText) return 'Untitled Lesson';
|
|
2357
|
+
let prefix = '';
|
|
2358
|
+
let rest = titleText;
|
|
2359
|
+
const match = titleText.match(/^(MISTAKE|SUCCESS|LEARNING|PREFERENCE):\s*(.*)/i);
|
|
2360
|
+
if (match) {
|
|
2361
|
+
prefix = match[1].toUpperCase() + ': ';
|
|
2362
|
+
rest = match[2];
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
const trimmed = rest.trim();
|
|
2366
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
2367
|
+
try {
|
|
2368
|
+
const parsed = JSON.parse(trimmed);
|
|
2369
|
+
const promptVal = parsed.prompt;
|
|
2370
|
+
const hookVal = parsed.hook_event_name || parsed.hookEventName;
|
|
2371
|
+
if (promptVal) {
|
|
2372
|
+
const dirName = parsed.cwd ? parsed.cwd.split('/').pop() : '';
|
|
2373
|
+
return prefix + `Prompt "${promptVal}"` + (dirName ? ` inside ${dirName}` : '');
|
|
2374
|
+
}
|
|
2375
|
+
if (hookVal) {
|
|
2376
|
+
const dirName = parsed.cwd ? parsed.cwd.split('/').pop() : '';
|
|
2377
|
+
return prefix + `Hook event ${hookVal}` + (dirName ? ` inside ${dirName}` : '');
|
|
2378
|
+
}
|
|
2379
|
+
if (parsed.signal) {
|
|
2380
|
+
const dirName = parsed.cwd ? parsed.cwd.split('/').pop() : '';
|
|
2381
|
+
return prefix + (parsed.signal === 'up' ? 'Thumbs Up' : 'Thumbs Down') + (dirName ? ` inside ${dirName}` : '');
|
|
2382
|
+
}
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
// ignore
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return titleText;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
function formatTextValue(value) {
|
|
2391
|
+
if (!value) return '';
|
|
2392
|
+
const trimmed = String(value).trim();
|
|
2393
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
2394
|
+
try {
|
|
2395
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
2396
|
+
} catch (e) {
|
|
2397
|
+
// ignore
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return value;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const title = cleanTitle(rawTitle);
|
|
2404
|
+
const context = formatTextValue(rawContext);
|
|
2405
|
+
const whatWentWrong = formatTextValue(rawWhatWentWrong);
|
|
2406
|
+
const whatWorked = formatTextValue(rawWhatWorked);
|
|
2348
2407
|
const whatToChange = merged.whatToChange || '';
|
|
2349
2408
|
const tags = Array.isArray(merged.tags) ? merged.tags.join(', ') : (merged.tags || '');
|
|
2350
2409
|
const timestamp = merged.timestamp ? new Date(merged.timestamp).toLocaleString() : '';
|
|
@@ -2742,6 +2801,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
2742
2801
|
{ path: '/pro', changefreq: 'weekly', priority: '0.9' },
|
|
2743
2802
|
{ path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
|
|
2744
2803
|
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
2804
|
+
{ path: '/chatgpt-app', changefreq: 'weekly', priority: '0.85' },
|
|
2745
2805
|
{ path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
|
|
2746
2806
|
{ path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
|
|
2747
2807
|
{ path: '/agents-cost-savings', changefreq: 'weekly', priority: '0.85' },
|
|
@@ -2749,6 +2809,11 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
2749
2809
|
{ path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
|
|
2750
2810
|
{ path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
|
|
2751
2811
|
{ path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
|
|
2812
|
+
{ path: '/learn/agentic-enterprise-context-brain', changefreq: 'weekly', priority: '0.85' },
|
|
2813
|
+
{ path: '/learn/deterministic-agent-workflows', changefreq: 'weekly', priority: '0.85' },
|
|
2814
|
+
{ path: '/learn/codex-role-plugins-need-governance', changefreq: 'weekly', priority: '0.85' },
|
|
2815
|
+
{ path: '/learn/agentic-os-team-governance', changefreq: 'weekly', priority: '0.85' },
|
|
2816
|
+
{ path: '/learn/cost-aware-agent-gate-routing', changefreq: 'weekly', priority: '0.85' },
|
|
2752
2817
|
{ path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
|
|
2753
2818
|
{ path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
|
|
2754
2819
|
{ path: '/compare/anthropic-containment', changefreq: 'weekly', priority: '0.85' },
|
|
@@ -4430,6 +4495,17 @@ function createApiServer() {
|
|
|
4430
4495
|
return;
|
|
4431
4496
|
}
|
|
4432
4497
|
|
|
4498
|
+
if (isGetLikeRequest && pathname === '/.well-known/agentic-verify.txt') {
|
|
4499
|
+
const agenticVerifyPath = path.join(__dirname, '..', '..', '.well-known', 'agentic-verify.txt');
|
|
4500
|
+
try {
|
|
4501
|
+
const content = fs.readFileSync(agenticVerifyPath, 'utf8');
|
|
4502
|
+
sendText(res, 200, content, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=3600' }, { headOnly: isHeadRequest });
|
|
4503
|
+
} catch {
|
|
4504
|
+
sendJson(res, 404, { error: 'agentic verification file not found' });
|
|
4505
|
+
}
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4433
4509
|
if (isGetLikeRequest && pathname === '/sitemap.xml') {
|
|
4434
4510
|
sendText(res, 200, renderSitemapXml(hostedConfig), {
|
|
4435
4511
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
@@ -4742,6 +4818,28 @@ async function addContext(){
|
|
|
4742
4818
|
return;
|
|
4743
4819
|
}
|
|
4744
4820
|
|
|
4821
|
+
if (isGetLikeRequest && (
|
|
4822
|
+
pathname === '/chatgpt-app'
|
|
4823
|
+
|| pathname === '/chatgpt-app.html'
|
|
4824
|
+
|| pathname === '/chatgpt-plugin'
|
|
4825
|
+
|| pathname === '/chatgpt-plugin.html'
|
|
4826
|
+
)) {
|
|
4827
|
+
try {
|
|
4828
|
+
servePublicMarketingPage({
|
|
4829
|
+
req,
|
|
4830
|
+
res,
|
|
4831
|
+
parsed,
|
|
4832
|
+
hostedConfig,
|
|
4833
|
+
isHeadRequest,
|
|
4834
|
+
renderHtml: () => fs.readFileSync(CHATGPT_APP_PAGE_PATH, 'utf-8'),
|
|
4835
|
+
extraTelemetry: { pageType: 'chatgpt_app' },
|
|
4836
|
+
});
|
|
4837
|
+
} catch {
|
|
4838
|
+
sendJson(res, 404, { error: 'ChatGPT app page not found' });
|
|
4839
|
+
}
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4745
4843
|
if (isGetLikeRequest && (pathname === '/compare' || pathname === '/compare.html')) {
|
|
4746
4844
|
try {
|
|
4747
4845
|
const html = fs.readFileSync(COMPARE_PAGE_PATH, 'utf-8');
|
|
@@ -5035,7 +5133,7 @@ async function addContext(){
|
|
|
5035
5133
|
version: pkg.version,
|
|
5036
5134
|
status: 'ok',
|
|
5037
5135
|
docs: 'https://github.com/IgorGanapolsky/ThumbGate',
|
|
5038
|
-
endpoints: ['/health', '/dashboard', '/guide', '/codex-plugin', '/compare', '/learn', '/pricing', '/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'],
|
|
5136
|
+
endpoints: ['/health', '/dashboard', '/guide', '/chatgpt-app', '/codex-plugin', '/compare', '/learn', '/pricing', '/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/ai-inventory', '/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'],
|
|
5039
5137
|
}, {}, {
|
|
5040
5138
|
headOnly: isHeadRequest,
|
|
5041
5139
|
});
|
|
@@ -6919,7 +7017,54 @@ ${hidden}
|
|
|
6919
7017
|
|
|
6920
7018
|
try {
|
|
6921
7019
|
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
6922
|
-
|
|
7020
|
+
const stats = analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH);
|
|
7021
|
+
try {
|
|
7022
|
+
const { getStatuslineMeta } = require('../../scripts/statusline-meta');
|
|
7023
|
+
const meta = getStatuslineMeta({ env: process.env });
|
|
7024
|
+
stats.tier = meta.tier;
|
|
7025
|
+
} catch (_) {
|
|
7026
|
+
stats.tier = 'Pro';
|
|
7027
|
+
}
|
|
7028
|
+
|
|
7029
|
+
let projectGeminiKey = '';
|
|
7030
|
+
let projectPerplexityKey = '';
|
|
7031
|
+
let geminiValidatedAt = null;
|
|
7032
|
+
try {
|
|
7033
|
+
const projectDir = resolveRequestProjectDir(req, parsed);
|
|
7034
|
+
const envPath = path.join(projectDir, '.env');
|
|
7035
|
+
if (fs.existsSync(envPath)) {
|
|
7036
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
7037
|
+
const geminiMatch = content.match(/^(?:GEMINI_API_KEY|GOOGLE_API_KEY|THUMBGATE_GEMINI_API_KEY)=(.*)$/m);
|
|
7038
|
+
if (geminiMatch) {
|
|
7039
|
+
projectGeminiKey = geminiMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
7040
|
+
}
|
|
7041
|
+
const perplexityMatch = content.match(/^(?:PERPLEXITY_API_KEY|THUMBGATE_PERPLEXITY_API_KEY)=(.*)$/m);
|
|
7042
|
+
if (perplexityMatch) {
|
|
7043
|
+
projectPerplexityKey = perplexityMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
const statusPath = path.join(projectDir, '.gemini-validated.json');
|
|
7047
|
+
if (fs.existsSync(statusPath)) {
|
|
7048
|
+
const st = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
7049
|
+
geminiValidatedAt = st.validatedAt || null;
|
|
7050
|
+
}
|
|
7051
|
+
} catch (_) {}
|
|
7052
|
+
|
|
7053
|
+
stats.geminiConfigured = Boolean(
|
|
7054
|
+
projectGeminiKey ||
|
|
7055
|
+
process.env.GEMINI_API_KEY ||
|
|
7056
|
+
process.env.THUMBGATE_GEMINI_API_KEY ||
|
|
7057
|
+
process.env.GOOGLE_API_KEY
|
|
7058
|
+
);
|
|
7059
|
+
stats.perplexityConfigured = Boolean(
|
|
7060
|
+
projectPerplexityKey ||
|
|
7061
|
+
process.env.PERPLEXITY_API_KEY ||
|
|
7062
|
+
process.env.THUMBGATE_PERPLEXITY_API_KEY
|
|
7063
|
+
);
|
|
7064
|
+
stats.geminiValidatedAt = geminiValidatedAt;
|
|
7065
|
+
stats.geminiKeyStatus = geminiValidatedAt ? 'validated' : (projectGeminiKey ? 'present' : 'none');
|
|
7066
|
+
stats.hybridInferenceAvailable = !!(stats.geminiConfigured || stats.perplexityConfigured);
|
|
7067
|
+
sendJson(res, 200, stats);
|
|
6923
7068
|
return;
|
|
6924
7069
|
}
|
|
6925
7070
|
|
|
@@ -6929,14 +7074,100 @@ ${hidden}
|
|
|
6929
7074
|
if (req.method === 'POST' && pathname === '/v1/chat') {
|
|
6930
7075
|
const body = await parseJsonBody(req);
|
|
6931
7076
|
const { answerDataQuestion } = require('../../scripts/dashboard-chat');
|
|
7077
|
+
|
|
7078
|
+
let projectGeminiKey = '';
|
|
7079
|
+
let projectPerplexityKey = '';
|
|
7080
|
+
try {
|
|
7081
|
+
const projectDir = resolveRequestProjectDir(req, parsed);
|
|
7082
|
+
const envPath = path.join(projectDir, '.env');
|
|
7083
|
+
if (fs.existsSync(envPath)) {
|
|
7084
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
7085
|
+
const geminiMatch = content.match(/^(?:GEMINI_API_KEY|GOOGLE_API_KEY|THUMBGATE_GEMINI_API_KEY)=(.*)$/m);
|
|
7086
|
+
if (geminiMatch) {
|
|
7087
|
+
projectGeminiKey = geminiMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
7088
|
+
}
|
|
7089
|
+
const perplexityMatch = content.match(/^(?:PERPLEXITY_API_KEY|THUMBGATE_PERPLEXITY_API_KEY)=(.*)$/m);
|
|
7090
|
+
if (perplexityMatch) {
|
|
7091
|
+
projectPerplexityKey = perplexityMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
7092
|
+
}
|
|
7093
|
+
}
|
|
7094
|
+
} catch (_) {}
|
|
7095
|
+
|
|
6932
7096
|
const result = await answerDataQuestion(body.question || body.q || body.message, {
|
|
6933
7097
|
feedbackDir: requestFeedbackPaths.FEEDBACK_DIR,
|
|
6934
7098
|
model: typeof body.model === 'string' ? body.model : undefined,
|
|
7099
|
+
apiKey: projectPerplexityKey || projectGeminiKey || process.env.PERPLEXITY_API_KEY || process.env.THUMBGATE_PERPLEXITY_API_KEY || process.env.GEMINI_API_KEY || process.env.THUMBGATE_GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '',
|
|
6935
7100
|
});
|
|
6936
7101
|
sendJson(res, result.ok ? 200 : (result.error === 'no_api_key' ? 503 : 400), result);
|
|
6937
7102
|
return;
|
|
6938
7103
|
}
|
|
6939
7104
|
|
|
7105
|
+
// Save Gemini API key from the dashboard UI
|
|
7106
|
+
if (req.method === 'POST' && pathname === '/v1/settings/gemini-key') {
|
|
7107
|
+
const body = await parseJsonBody(req);
|
|
7108
|
+
const key = String(body.key || '').trim();
|
|
7109
|
+
if (!key) {
|
|
7110
|
+
sendJson(res, 400, { ok: false, error: 'missing_key', message: 'No API key provided.' });
|
|
7111
|
+
return;
|
|
7112
|
+
}
|
|
7113
|
+
|
|
7114
|
+
// Validate the candidate key using the *exact* same code path as /v1/chat
|
|
7115
|
+
// (project-scoped .env read + RAG + Gemini call). This prevents saving a
|
|
7116
|
+
// key that will later produce the confusing "API key not valid" error in chat.
|
|
7117
|
+
let validation;
|
|
7118
|
+
try {
|
|
7119
|
+
const { answerDataQuestion } = require('../../scripts/dashboard-chat');
|
|
7120
|
+
validation = await answerDataQuestion('Reply with the single word: PONG', {
|
|
7121
|
+
feedbackDir: requestFeedbackPaths.FEEDBACK_DIR,
|
|
7122
|
+
apiKey: key,
|
|
7123
|
+
});
|
|
7124
|
+
} catch (e) {
|
|
7125
|
+
validation = { ok: false, error: 'validation_exception', message: String(e && e.message || e) };
|
|
7126
|
+
}
|
|
7127
|
+
|
|
7128
|
+
if (!validation.ok) {
|
|
7129
|
+
const detail = validation.error === 'gemini_error'
|
|
7130
|
+
? (validation.message || 'Gemini rejected the key')
|
|
7131
|
+
: (validation.message || validation.error || 'unknown error');
|
|
7132
|
+
sendJson(res, 400, {
|
|
7133
|
+
ok: false,
|
|
7134
|
+
error: 'invalid_key',
|
|
7135
|
+
message: 'Key validation failed: ' + detail + '. Get a fresh key from https://aistudio.google.com/app/apikey (or run `npx thumbgate setup-vertex` for Vertex) and try again.'
|
|
7136
|
+
});
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
|
|
7140
|
+
try {
|
|
7141
|
+
const projectDir = resolveRequestProjectDir(req, parsed);
|
|
7142
|
+
const envPath = path.join(projectDir, '.env');
|
|
7143
|
+
let content = '';
|
|
7144
|
+
if (fs.existsSync(envPath)) {
|
|
7145
|
+
content = fs.readFileSync(envPath, 'utf8');
|
|
7146
|
+
}
|
|
7147
|
+
const regex = /^GEMINI_API_KEY=.*$/m;
|
|
7148
|
+
if (regex.test(content)) {
|
|
7149
|
+
content = content.replace(regex, `GEMINI_API_KEY=${key}`);
|
|
7150
|
+
} else {
|
|
7151
|
+
content = content.trim() + `\nGEMINI_API_KEY=${key}\n`;
|
|
7152
|
+
}
|
|
7153
|
+
fs.writeFileSync(envPath, content, 'utf8');
|
|
7154
|
+
// Also set it in the current process so it takes effect immediately without restart
|
|
7155
|
+
process.env.GEMINI_API_KEY = key;
|
|
7156
|
+
// Persist validation success for reliable "configured" status in stats/hints
|
|
7157
|
+
try {
|
|
7158
|
+
const statusPath = path.join(projectDir, '.gemini-validated.json');
|
|
7159
|
+
fs.writeFileSync(statusPath, JSON.stringify({
|
|
7160
|
+
validatedAt: new Date().toISOString(),
|
|
7161
|
+
validatedBy: 'dashboard-save'
|
|
7162
|
+
}, null, 2));
|
|
7163
|
+
} catch (_) { /* non-fatal */ }
|
|
7164
|
+
sendJson(res, 200, { ok: true, message: 'Key saved and validated.' });
|
|
7165
|
+
} catch (e) {
|
|
7166
|
+
sendJson(res, 500, { ok: false, error: 'fs_error', message: 'Failed to write to .env file: ' + e.message });
|
|
7167
|
+
}
|
|
7168
|
+
return;
|
|
7169
|
+
}
|
|
7170
|
+
|
|
6940
7171
|
// Server-Sent Events stream of live feedback / rule-regen / gate events.
|
|
6941
7172
|
// Dashboard clients subscribe once (with the same Bearer auth already
|
|
6942
7173
|
// required for /v1/feedback/stats) and receive pushed events as they
|
|
@@ -7292,6 +7523,7 @@ ${hidden}
|
|
|
7292
7523
|
summary: body.summary,
|
|
7293
7524
|
allowedPaths: body.allowedPaths,
|
|
7294
7525
|
protectedPaths: body.protectedPaths,
|
|
7526
|
+
workflowContract: body.workflowContract,
|
|
7295
7527
|
repoPath: body.repoPath,
|
|
7296
7528
|
localOnly: body.localOnly === true,
|
|
7297
7529
|
clear: body.clear === true,
|
|
@@ -8180,6 +8412,40 @@ ${hidden}
|
|
|
8180
8412
|
return;
|
|
8181
8413
|
}
|
|
8182
8414
|
|
|
8415
|
+
// GET /v1/dashboard/ai-inventory -- Enterprise AI inventory evidence
|
|
8416
|
+
if (req.method === 'GET' && pathname === '/v1/dashboard/ai-inventory') {
|
|
8417
|
+
try {
|
|
8418
|
+
const {
|
|
8419
|
+
scanAiComponents,
|
|
8420
|
+
buildCycloneDxMlBom,
|
|
8421
|
+
} = require('../../scripts/ai-component-inventory');
|
|
8422
|
+
const requestedRoot = parsed.searchParams.get('root');
|
|
8423
|
+
const serverRoot = process.cwd();
|
|
8424
|
+
const rootDir = requestedRoot ? path.resolve(requestedRoot) : serverRoot;
|
|
8425
|
+
const rootRel = path.relative(serverRoot, rootDir);
|
|
8426
|
+
if (rootRel.startsWith('..') || path.isAbsolute(rootRel)) {
|
|
8427
|
+
sendJson(res, 400, {
|
|
8428
|
+
error: 'ai_inventory_root_out_of_scope',
|
|
8429
|
+
message: 'Dashboard AI inventory root must stay within the server working directory. Use the CLI for explicit cross-project scans.',
|
|
8430
|
+
});
|
|
8431
|
+
return;
|
|
8432
|
+
}
|
|
8433
|
+
const inventory = scanAiComponents({
|
|
8434
|
+
rootDir,
|
|
8435
|
+
maxFiles: parsed.searchParams.get('maxFiles') ? Number(parsed.searchParams.get('maxFiles')) : undefined,
|
|
8436
|
+
includeSnippets: parsed.searchParams.get('snippets') !== '0',
|
|
8437
|
+
});
|
|
8438
|
+
const format = String(parsed.searchParams.get('format') || 'json').toLowerCase();
|
|
8439
|
+
sendJson(res, 200, format === 'cyclonedx' ? buildCycloneDxMlBom(inventory, { version: pkg.version }) : inventory);
|
|
8440
|
+
} catch (err) {
|
|
8441
|
+
sendJson(res, 500, {
|
|
8442
|
+
error: 'ai_inventory_failed',
|
|
8443
|
+
message: err && err.message ? err.message : 'Unable to scan AI component inventory.',
|
|
8444
|
+
});
|
|
8445
|
+
}
|
|
8446
|
+
return;
|
|
8447
|
+
}
|
|
8448
|
+
|
|
8183
8449
|
// GET /v1/dashboard/review-state -- incremental review baseline and deltas
|
|
8184
8450
|
if (req.method === 'GET' && pathname === '/v1/dashboard/review-state') {
|
|
8185
8451
|
const reviewState = readDashboardReviewState(requestFeedbackDir);
|