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.
Files changed (50) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +2 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +20 -9
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  9. package/adapters/mcp/server-stdio.js +28 -1
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +147 -10
  13. package/bin/dashboard-cli.js +7 -0
  14. package/config/gate-classifier-routing.json +98 -0
  15. package/config/gate-templates.json +60 -0
  16. package/config/mcp-allowlists.json +8 -7
  17. package/config/model-candidates.json +71 -6
  18. package/package.json +26 -10
  19. package/public/chatgpt-app.html +330 -0
  20. package/public/codex-plugin.html +66 -14
  21. package/public/dashboard.html +203 -17
  22. package/public/index.html +79 -4
  23. package/public/learn.html +70 -0
  24. package/public/lessons.html +129 -6
  25. package/public/numbers.html +2 -2
  26. package/public/pricing.html +20 -2
  27. package/scripts/agent-operations-planner.js +621 -0
  28. package/scripts/agent-reward-model.js +53 -1
  29. package/scripts/ai-component-inventory.js +367 -0
  30. package/scripts/classifier-routing.js +130 -0
  31. package/scripts/cli-schema.js +26 -0
  32. package/scripts/dashboard-chat.js +64 -17
  33. package/scripts/feedback-sanitizer.js +105 -0
  34. package/scripts/gates-engine.js +258 -61
  35. package/scripts/hybrid-feedback-context.js +141 -7
  36. package/scripts/memory-scope-readiness.js +159 -0
  37. package/scripts/parallel-workflow-orchestrator.js +293 -0
  38. package/scripts/plausible-domain-config.js +86 -0
  39. package/scripts/plausible-server-events.js +4 -2
  40. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  41. package/scripts/qa-scenario-planner.js +136 -0
  42. package/scripts/repeat-metric.js +28 -12
  43. package/scripts/secret-fixture-tokens.js +61 -0
  44. package/scripts/secret-scanner.js +44 -5
  45. package/scripts/security-scanner.js +80 -0
  46. package/scripts/seo-gsd.js +53 -0
  47. package/scripts/thumbgate-bench.js +16 -1
  48. package/scripts/tool-registry.js +37 -0
  49. package/scripts/workflow-sentinel.js +189 -4
  50. 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
- 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="thumbgate.ai" 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 &lt;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>`;
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 &lt;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(host)}"`
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 Dialogflow Data Chat</strong><span>Check Vertex/DFCX readiness and use /v1/enterprise/dialogflow/chat to query local ThumbGate data through the DFCX guard.</span></a>
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 title = merged.title || merged.context || 'Untitled Lesson';
2345
- const context = merged.context || '';
2346
- const whatWentWrong = merged.whatWentWrong || '';
2347
- const whatWorked = merged.whatWorked || '';
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
- sendJson(res, 200, analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH));
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);