thumbgate 1.26.8 โ†’ 1.27.3

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 (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/agentic-verify.txt +1 -0
  3. package/.well-known/llms.txt +2 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +44 -31
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  8. package/adapters/mcp/server-stdio.js +41 -1
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/thumbgate-bench.json +2 -2
  11. package/bin/cli.js +184 -8
  12. package/bin/dashboard-cli.js +7 -0
  13. package/config/gate-classifier-routing.json +98 -0
  14. package/config/gate-templates.json +60 -0
  15. package/config/mcp-allowlists.json +8 -7
  16. package/config/model-candidates.json +71 -6
  17. package/package.json +28 -12
  18. package/public/about.html +162 -0
  19. package/public/chatgpt-app.html +330 -0
  20. package/public/codex-plugin.html +66 -14
  21. package/public/compare.html +2 -2
  22. package/public/dashboard.html +224 -36
  23. package/public/guide.html +2 -2
  24. package/public/index.html +122 -40
  25. package/public/learn.html +70 -0
  26. package/public/lessons.html +129 -6
  27. package/public/numbers.html +2 -2
  28. package/public/pricing.html +28 -23
  29. package/public/pro.html +3 -3
  30. package/scripts/agent-operations-planner.js +621 -0
  31. package/scripts/agent-reward-model.js +53 -1
  32. package/scripts/ai-component-inventory.js +367 -0
  33. package/scripts/classifier-routing.js +130 -0
  34. package/scripts/cli-schema.js +26 -0
  35. package/scripts/commercial-offer.js +10 -2
  36. package/scripts/dashboard-chat.js +199 -51
  37. package/scripts/feedback-sanitizer.js +105 -0
  38. package/scripts/gates-engine.js +301 -67
  39. package/scripts/hybrid-feedback-context.js +141 -7
  40. package/scripts/memory-scope-readiness.js +159 -0
  41. package/scripts/oss-pr-opportunity-scout.js +35 -5
  42. package/scripts/parallel-workflow-orchestrator.js +293 -0
  43. package/scripts/plausible-domain-config.js +86 -0
  44. package/scripts/plausible-server-events.js +4 -2
  45. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  46. package/scripts/qa-scenario-planner.js +136 -0
  47. package/scripts/rate-limiter.js +2 -2
  48. package/scripts/repeat-metric.js +28 -12
  49. package/scripts/secret-fixture-tokens.js +61 -0
  50. package/scripts/secret-scanner.js +44 -5
  51. package/scripts/security-scanner.js +80 -0
  52. package/scripts/seo-gsd.js +113 -0
  53. package/scripts/thumbgate-bench.js +16 -1
  54. package/scripts/tool-registry.js +37 -0
  55. package/scripts/workflow-sentinel.js +282 -54
  56. package/src/api/server.js +466 -60
  57. package/.claude-plugin/marketplace.json +0 -85
@@ -263,14 +263,18 @@
263
263
  <div class="panel" id="chatPanel" style="margin-bottom:20px;">
264
264
  <div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
265
265
  <h2 style="margin:0;">๐Ÿ’ฌ Chat with your data</h2>
266
- <span style="font-size:13px;color:var(--text-muted);">Ask about your captured lessons, mistakes, and rules โ€” answered by Gemini, grounded only in your data.</span>
266
+ <span style="font-size:13px;color:var(--text-muted);">Ask about your gates, blocks, feedback, and lessons โ€” answered <strong>locally from your own data</strong>, no cloud. (An optional BYO model only kicks in for open-ended questions.)</span>
267
267
  </div>
268
268
  <div id="chatMessages" style="max-height:360px;overflow-y:auto;margin-bottom:12px;display:none;padding-right:4px;"></div>
269
269
  <div style="display:flex;gap:8px;">
270
270
  <input id="chatInput" class="auth-input" style="flex:1;" placeholder="e.g. What mistakes have we made, and how do we avoid them?" onkeydown="if(event.key==='Enter'){event.preventDefault();sendChat();}" />
271
271
  <button class="btn" id="chatSend" onclick="sendChat()">Ask</button>
272
272
  </div>
273
- <div id="chatHint" style="font-size:12px;color:var(--text-muted);margin-top:8px;">Powered by your captured lessons + Gemini. Set <code style="font-family:var(--mono);">GEMINI_API_KEY</code> (<code style="font-family:var(--mono);">npx thumbgate setup-vertex --write</code>) to enable.</div>
273
+ <div id="chatHint" style="font-size:12px;color:var(--text-muted);margin-top:8px;display:flex;align-items:center;gap:8px;">
274
+ <span>Powered by local dashboard data. Optional Gemini/Perplexity keys only expand open-ended analysis.</span>
275
+ <input type="password" id="geminiKeyInput" placeholder="Optional GEMINI_API_KEY..." style="background:var(--bg-raised); border:1px solid var(--border); border-radius:4px; padding:4px 8px; color:var(--text); font-family:var(--mono); font-size:11px; flex:1; max-width: 250px;" onkeydown="if(event.key==='Enter'){event.preventDefault();saveGeminiKey();}" />
276
+ <button class="btn-outline" style="padding:4px 10px;font-size:11px;border-radius:4px;" onclick="saveGeminiKey()">Save</button>
277
+ </div>
274
278
  </div>
275
279
 
276
280
  <div class="panel" id="reviewDeltaPanel" style="margin-bottom:20px;">
@@ -304,7 +308,8 @@
304
308
  <div class="tab active" onclick="switchTab('search')">๐Ÿ” Search Memories</div>
305
309
  <div class="tab" onclick="switchTab('gates')">๐Ÿ›ก๏ธ Active Gates</div>
306
310
  <div class="tab" onclick="switchTab('team')">๐Ÿ‘ฅ Team</div>
307
- <div class="tab" onclick="switchTab('enterprise')">๐Ÿข Enterprise Chat</div>
311
+ <div class="tab" onclick="switchTab('enterprise')">๐Ÿข Governed Data Chat</div>
312
+ <div class="tab" onclick="switchTab('ai-inventory')">๐Ÿงฌ AI Inventory</div>
308
313
  <div class="tab" onclick="switchTab('generated')">๐Ÿงฉ Generated Views</div>
309
314
  <div class="tab" onclick="switchTab('settings')">โš™๏ธ Policy Origins</div>
310
315
  <div class="tab" onclick="switchTab('templates')">๐Ÿงฑ Gate Templates</div>
@@ -411,25 +416,48 @@
411
416
  <!-- ENTERPRISE CHAT TAB -->
412
417
  <div class="tab-content" id="tab-enterprise">
413
418
  <div class="templates-section">
414
- <h2>Enterprise Dialogflow Data Chat</h2>
415
- <p class="template-summary">Ask questions over local ThumbGate feedback, lessons, gates, team posture, and Vertex/DFCX readiness. This local panel uses ThumbGate's DFCX-compatible guard before data access; it does not claim a live Google Dialogflow CX agent unless deployment evidence is configured.</p>
419
+ <h2>Governed Data Chat</h2>
420
+ <p class="template-summary">Ask questions over your local ThumbGate data (lessons, raw feedback via LanceDB vectors, gate stats, receipts). Uses local retrieval plus your configured LLM, including open-source/local endpoints through <code>THUMBGATE_LOCAL_LLM_ENDPOINT</code>. Dialogflow is not the chatbot layer; DFCX/Vertex cards are optional deployment-readiness checks for buyers who run their own Google agent stack.</p>
416
421
  <div class="enterprise-chat-layout">
417
422
  <div class="panel">
418
423
  <h3>Chat With Local ThumbGate Data</h3>
419
- <textarea class="enterprise-chat-box" id="enterpriseChatPrompt" placeholder="Ask: What mistakes are recurring? Which gates blocked the most? Is Vertex configured? What is our DFCX readiness?"></textarea>
424
+ <textarea class="enterprise-chat-box" id="enterpriseChatPrompt" placeholder="Ask: What mistakes are recurring? Which gates blocked the most? Are we running local-only or cloud-integrated?"></textarea>
420
425
  <div style="display:flex;gap:10px;align-items:center;margin-top:12px;flex-wrap:wrap;">
421
426
  <button class="btn" id="enterpriseChatBtn" onclick="sendEnterpriseChat()">Ask ThumbGate</button>
422
427
  <button class="btn-outline" onclick="setEnterprisePrompt('Which gates are blocking risky actions?')">Gates</button>
423
428
  <button class="btn-outline" onclick="setEnterprisePrompt('What feedback mistakes keep repeating?')">Feedback</button>
424
- <button class="btn-outline" onclick="setEnterprisePrompt('Is Vertex and DFCX configured?')">Cloud</button>
429
+ <button class="btn-outline" onclick="setEnterprisePrompt('Are we running local-only or cloud-integrated?')">Runtime</button>
425
430
  </div>
426
431
  <div class="enterprise-answer" id="enterpriseChatAnswer">Connect your dashboard, then ask about local ThumbGate data.</div>
427
432
  <div class="enterprise-source-list" id="enterpriseChatSources"></div>
428
433
  </div>
429
434
  <div class="panel">
430
- <h3>Enterprise Readiness</h3>
435
+ <h3>Runtime Readiness</h3>
431
436
  <div class="inventory-tools" id="enterpriseStatusCards"><div class="loading">Loading enterprise status...</div></div>
432
- <div class="template-summary" style="margin-top:14px;margin-bottom:0;">Live DFCX proof must come from the Conversational Agents console, deployed webhook URL, Cloud Run logs, or the Dialogflow CX REST API <code style="font-family:var(--mono);font-size:12px;">projects.locations.agents</code>.</div>
437
+ <div class="template-summary" style="margin-top:14px;margin-bottom:0;">The chat itself is local RAG plus your LLM. Open-source/local models are the default enterprise path; DFCX/Vertex readiness is optional evidence for teams that already operate Google agents in their own tenancy.</div>
438
+ </div>
439
+ </div>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- AI INVENTORY TAB -->
444
+ <div class="tab-content" id="tab-ai-inventory">
445
+ <div class="templates-section">
446
+ <h2>AI Component Inventory</h2>
447
+ <p class="template-summary">Enterprise evidence for AI/ML usage: provider SDKs, agent frameworks, vector databases, Vertex/Gemini/Dialogflow CX references, and local model artifacts. Export this as ML-BOM proof before claiming an AI system is production-ready.</p>
448
+ <div class="inventory-grid" id="aiInventorySummaryCards"><div class="loading">Scanning AI inventory...</div></div>
449
+ <div class="team-columns" style="margin-top:16px;">
450
+ <div class="panel">
451
+ <h3>Detected Components</h3>
452
+ <div class="inventory-tools" id="aiInventoryComponents"><div class="loading">Scanning components...</div></div>
453
+ </div>
454
+ <div class="panel">
455
+ <h3>Export Evidence</h3>
456
+ <div class="template-summary" style="margin-bottom:12px;">CLI export:</div>
457
+ <pre style="white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;font-size:12px;color:var(--text);">npx thumbgate ai-inventory --format=cyclonedx --output=.thumbgate/ai-mlbom.json</pre>
458
+ <div class="template-summary" style="margin-top:12px;">API export:</div>
459
+ <pre style="white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;font-size:12px;color:var(--text);">GET /v1/dashboard/ai-inventory?format=cyclonedx</pre>
460
+ <div id="aiInventoryExportNote" class="template-summary" style="margin-top:12px;margin-bottom:0;">Waiting for inventory evidence...</div>
433
461
  </div>
434
462
  </div>
435
463
  </div>
@@ -681,8 +709,30 @@ let currentGeneratedView = 'team-review';
681
709
  const BOOTSTRAP_API_KEY = __DASHBOARD_BOOTSTRAP_KEY__;
682
710
  const LOCAL_PRO_BOOTSTRAP = __DASHBOARD_BOOTSTRAP_ENABLED__;
683
711
 
712
+ let ACTIVE_PROJECT_DIR = '';
713
+ try {
714
+ var urlParams = new URLSearchParams(window.location.search);
715
+ ACTIVE_PROJECT_DIR = urlParams.get('project') || '';
716
+ } catch (e) {}
717
+
718
+ function preserveProjectQueryParams() {
719
+ if (!ACTIVE_PROJECT_DIR) return;
720
+ var links = document.querySelectorAll('a[href^="/dashboard"], a[href^="/lessons"], a[href="/"]');
721
+ links.forEach(function(link) {
722
+ try {
723
+ var url = new URL(link.href, window.location.origin);
724
+ url.searchParams.set('project', ACTIVE_PROJECT_DIR);
725
+ link.href = url.pathname + url.search + url.hash;
726
+ } catch (e) {}
727
+ });
728
+ }
729
+
684
730
  function getHeaders() {
685
- return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
731
+ var headers = { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
732
+ if (ACTIVE_PROJECT_DIR) {
733
+ headers['x-thumbgate-project-dir'] = ACTIVE_PROJECT_DIR;
734
+ }
735
+ return headers;
686
736
  }
687
737
 
688
738
  // --- Chat with your data ---------------------------------------------------
@@ -706,13 +756,32 @@ function chatRenderAnswer(a) {
706
756
  .replace(/\[(\d+)\]/g, '<sup style="color:var(--cyan);font-weight:600;">[$1]</sup>')
707
757
  .replace(/\n/g, '<br>');
708
758
  }
709
- function chatRenderSources(sources) {
759
+ function chatRenderSources(sources, answerText) {
710
760
  if (!sources || !sources.length) return '';
761
+ var citedNums = new Set();
762
+ var regex = /\[(\d+)\]/g;
763
+ var match;
764
+ while ((match = regex.exec(answerText || '')) !== null) {
765
+ citedNums.add(parseInt(match[1], 10));
766
+ }
767
+
711
768
  var items = sources.map(function (s, i) {
769
+ var num = i + 1;
770
+ var isCited = citedNums.has(num);
712
771
  var label = chatEscape(String(s.title || s.id || '').slice(0, 64));
713
- return '<span title="' + label + '" style="display:inline-block;font-size:11px;background:var(--cyan-dim);color:var(--cyan);padding:2px 7px;border-radius:5px;margin:4px 5px 0 0;">[' + (i + 1) + '] ' + label + '</span>';
772
+
773
+ var bg = isCited ? 'var(--cyan-dim)' : 'var(--bg-raised)';
774
+ var color = isCited ? 'var(--cyan)' : 'var(--text-muted)';
775
+ var border = isCited ? '1px solid var(--cyan)' : '1px solid var(--border)';
776
+ var weight = isCited ? '600' : 'normal';
777
+
778
+ return '<span title="' + label + '" style="display:inline-block;font-size:11px;background:' + bg + ';color:' + color + ';border:' + border + ';font-weight:' + weight + ';padding:2px 7px;border-radius:5px;margin:4px 5px 0 0;">[' + num + '] ' + label + '</span>';
714
779
  }).join('');
715
- return '<div style="margin-top:10px;">' + items + '</div>';
780
+
781
+ return '<div style="margin-top:12px;border-top:1px dashed var(--border);padding-top:10px;">' +
782
+ '<div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;font-weight:600;">Database logs analyzed to answer your question:</div>' +
783
+ items +
784
+ '</div>';
716
785
  }
717
786
  async function sendChat() {
718
787
  var input = document.getElementById('chatInput');
@@ -729,7 +798,10 @@ async function sendChat() {
729
798
  var res = await fetch('/v1/chat', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ question: q }) });
730
799
  var data = await res.json();
731
800
  if (data && data.ok) {
732
- pending.innerHTML = chatRenderAnswer(data.answer) + chatRenderSources(data.sources);
801
+ pending.innerHTML = chatRenderAnswer(data.answer) + chatRenderSources(data.sources, data.answer);
802
+ } else if (data && data.error === 'gemini_error') {
803
+ pending.innerHTML = '<em style="color:#f87171;">Gemini rejected the key: ' + chatEscape(data.message || 'invalid') +
804
+ '<br>Fix: paste a valid key in the "Set GEMINI_API_KEY..." box below (get one at <a href="https://aistudio.google.com/app/apikey" target="_blank" style="color:#67e8f9;">AI Studio</a>) and click Save.</em>';
733
805
  } else {
734
806
  pending.innerHTML = '<em style="color:var(--text-muted);">' + chatEscape((data && data.message) || 'Chat is unavailable.') + '</em>';
735
807
  }
@@ -741,6 +813,37 @@ async function sendChat() {
741
813
  }
742
814
  }
743
815
 
816
+ async function saveGeminiKey() {
817
+ var input = document.getElementById('geminiKeyInput');
818
+ var val = (input.value || '').trim();
819
+ if (!val) return;
820
+ var oldPlaceholder = input.placeholder;
821
+ input.disabled = true;
822
+ input.placeholder = 'Saving...';
823
+ try {
824
+ var res = await fetch('/v1/settings/gemini-key', {
825
+ method: 'POST',
826
+ headers: getHeaders(),
827
+ body: JSON.stringify({ key: val })
828
+ });
829
+ var data = await res.json();
830
+ if (data && data.ok) {
831
+ input.value = '';
832
+ input.placeholder = 'โœ“ Key saved to .env';
833
+ setTimeout(function() {
834
+ document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โœ“ Key validated. Hybrid (Perplexity/Gemini) supported for chat with your data.</span>';
835
+ }, 2000);
836
+ } else {
837
+ input.placeholder = 'โœ— Failed: ' + (data.message || data.error || 'Unknown error');
838
+ }
839
+ } catch (e) {
840
+ input.placeholder = 'โœ— Network error';
841
+ } finally {
842
+ input.disabled = false;
843
+ setTimeout(function() { if (input.placeholder.startsWith('โœ—')) input.placeholder = oldPlaceholder; }, 3000);
844
+ }
845
+ }
846
+
744
847
  function hasBootstrapKey() {
745
848
  return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
746
849
  }
@@ -751,8 +854,8 @@ async function connect(options) {
751
854
  API_KEY = String(opts.key || input.value || '').trim();
752
855
  if (!API_KEY) return;
753
856
 
754
- const isEnterprise = API_KEY.startsWith('tg_op_') || API_KEY.startsWith('tg_creator_');
755
- const tierName = isEnterprise ? 'Enterprise' : 'Pro';
857
+ var isEnterprise = API_KEY.startsWith('tg_op_') || API_KEY.startsWith('tg_creator_');
858
+ var tierName = isEnterprise ? 'Enterprise' : 'Pro';
756
859
 
757
860
  const status = document.getElementById('authStatus');
758
861
  const btn = document.getElementById('connectBtn');
@@ -763,16 +866,33 @@ async function connect(options) {
763
866
  const res = await fetch('/v1/feedback/stats', { headers: getHeaders() });
764
867
  if (!res.ok) throw new Error('Invalid API key');
765
868
  const data = await res.json();
869
+
870
+ if (data.tier) {
871
+ isEnterprise = (data.tier === 'Enterprise');
872
+ tierName = data.tier;
873
+ }
874
+
875
+ if (data.geminiKeyStatus === 'validated' || data.geminiValidatedAt) {
876
+ var when = data.geminiValidatedAt ? ' (validated ' + new Date(data.geminiValidatedAt).toLocaleTimeString() + ')' : '';
877
+ document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โœ“ Gemini API key validated' + when + '. You can now chat with your data.</span>';
878
+ } else if (data.perplexityConfigured) {
879
+ document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">โœ“ Perplexity hybrid (local-cloud) key present. Supports hybrid inference for chat with your data.</span>';
880
+ } else if (data.geminiConfigured) {
881
+ document.getElementById('chatHint').innerHTML = '<span style="color:#f59e0b">โš  Gemini key present in .env (click Save below to validate it). Perplexity hybrid also supported for cost/privacy.</span>';
882
+ }
883
+
766
884
  status.className = 'auth-status ok';
767
885
  status.textContent = opts.localPro ? `โœ“ Local ${tierName} connected` : 'โœ“ Connected';
768
886
  document.getElementById('dashboardContent').style.display = 'block';
769
887
  if (opts.localPro) {
888
+ const localProActiveMessage = 'Local Pro is active on this machine. Your dashboard is using the saved license key automatically.';
889
+ const localEnterpriseActiveMessage = 'Local Enterprise is active on this machine. Your dashboard is using the saved license key automatically.';
770
890
  input.value = 'local-license';
771
891
  input.disabled = true;
772
892
  input.placeholder = `Local ${tierName} auto-connected`;
773
893
  btn.disabled = true;
774
894
  document.getElementById('demoBtn').style.display = 'none';
775
- document.getElementById('authHelp').textContent = `Local ${tierName} is active on this machine. Your dashboard is using the saved license key automatically.`;
895
+ document.getElementById('authHelp').textContent = isEnterprise ? localEnterpriseActiveMessage : localProActiveMessage;
776
896
  }
777
897
  renderStats(data);
778
898
  setSelectedCard('all');
@@ -971,11 +1091,11 @@ function switchTab(name) {
971
1091
  /**
972
1092
  * Resolve deep-link tab target from URL hash or query string.
973
1093
  * Supports: /dashboard#insights, /dashboard?tab=gates, /dashboard#tab-export.
974
- * Valid targets match tab-content ids (search, gates, team, generated,
975
- * settings, templates, insights, export).
1094
+ * Valid targets match tab-content ids (search, gates, team, enterprise,
1095
+ * ai-inventory, generated, settings, templates, insights, export).
976
1096
  */
977
1097
  function getDeepLinkTab() {
978
- var valid = ['search', 'gates', 'team', 'generated', 'settings', 'templates', 'insights', 'export'];
1098
+ var valid = ['search', 'gates', 'team', 'enterprise', 'ai-inventory', 'generated', 'settings', 'templates', 'insights', 'export'];
979
1099
  var raw = (window.location.hash || '').replace(/^#/, '').replace(/^tab-/, '');
980
1100
  if (!raw) {
981
1101
  try {
@@ -1187,7 +1307,68 @@ function renderDashboardData(data) {
1187
1307
  renderRegulatedProof(data.regulatedProof || {});
1188
1308
  renderTemplates(data.templateLibrary || {});
1189
1309
  renderInsights(data);
1190
- loadEnterpriseDialogflowStatus();
1310
+ loadAiInventory();
1311
+ loadEnterpriseDataChatStatus();
1312
+ }
1313
+
1314
+ async function loadAiInventory() {
1315
+ var summaryEl = document.getElementById('aiInventorySummaryCards');
1316
+ var componentsEl = document.getElementById('aiInventoryComponents');
1317
+ var exportEl = document.getElementById('aiInventoryExportNote');
1318
+ if (!summaryEl || !componentsEl) return;
1319
+
1320
+ try {
1321
+ var res = await fetch('/v1/dashboard/ai-inventory', { headers: getHeaders() });
1322
+ if (!res.ok) {
1323
+ var text = await res.text();
1324
+ throw new Error(text || 'Failed to load AI inventory');
1325
+ }
1326
+ var inventory = await res.json();
1327
+ renderAiInventory(inventory);
1328
+ } catch (e) {
1329
+ var message = e && e.message ? e.message : 'Failed to load AI inventory';
1330
+ setMessageState(summaryEl, 'empty', message);
1331
+ setMessageState(componentsEl, 'empty', message);
1332
+ if (exportEl) exportEl.textContent = message;
1333
+ }
1334
+ }
1335
+
1336
+ function renderAiInventory(inventory) {
1337
+ var summaryEl = document.getElementById('aiInventorySummaryCards');
1338
+ var componentsEl = document.getElementById('aiInventoryComponents');
1339
+ var exportEl = document.getElementById('aiInventoryExportNote');
1340
+ var components = Array.isArray(inventory.components) ? inventory.components : [];
1341
+ var byCategory = inventory.summary && inventory.summary.byCategory ? inventory.summary.byCategory : {};
1342
+ var modelArtifacts = byCategory.model_artifact || 0;
1343
+ var vectorDbs = byCategory.vector_database || 0;
1344
+
1345
+ summaryEl.innerHTML = [
1346
+ '<div class="team-card"><div class="team-kicker">Files scanned</div><div class="team-value">' + escHtml(String(inventory.filesScanned || 0)) + '</div><div class="team-note">source, manifests, model artifacts</div></div>',
1347
+ '<div class="team-card"><div class="team-kicker">AI components</div><div class="team-value">' + escHtml(String(inventory.componentCount || components.length)) + '</div><div class="team-note">inventory evidence items</div></div>',
1348
+ '<div class="team-card"><div class="team-kicker">Vector stores</div><div class="team-value">' + escHtml(String(vectorDbs)) + '</div><div class="team-note">retrieval governance surface</div></div>',
1349
+ '<div class="team-card"><div class="team-kicker">Model artifacts</div><div class="team-value">' + escHtml(String(modelArtifacts)) + '</div><div class="team-note">local ML-BOM artifacts</div></div>'
1350
+ ].join('');
1351
+
1352
+ if (!components.length) {
1353
+ componentsEl.innerHTML = '<div class="empty">No AI/ML components detected in this project root.</div>';
1354
+ } else {
1355
+ componentsEl.innerHTML = components.map(function(item) {
1356
+ var evidence = Array.isArray(item.evidence) ? item.evidence.slice(0, 4) : [];
1357
+ var evidenceHtml = evidence.map(function(e) {
1358
+ var loc = e.line ? (e.file + ':' + e.line) : e.file;
1359
+ return '<li><code>' + escHtml(loc || 'unknown') + '</code> <span style="color:var(--text-muted);">[' + escHtml(e.kind || 'evidence') + ']</span></li>';
1360
+ }).join('');
1361
+ return '<div class="tool-card">' +
1362
+ '<div class="tool-title">' + escHtml(item.name || item.id || 'AI component') + '</div>' +
1363
+ '<div class="tool-meta">' + escHtml(item.category || 'unknown') + ' ยท ' + escHtml(item.ecosystem || 'unknown') + '</div>' +
1364
+ '<ul style="margin:8px 0 0 18px;padding:0;">' + evidenceHtml + '</ul>' +
1365
+ '</div>';
1366
+ }).join('');
1367
+ }
1368
+
1369
+ if (exportEl) {
1370
+ exportEl.textContent = 'Ready: export ' + String(inventory.componentCount || components.length) + ' component(s) as CycloneDX ML-BOM evidence.';
1371
+ }
1191
1372
  }
1192
1373
 
1193
1374
  function renderGeneratedViewToolbar(spec) {
@@ -1632,24 +1813,29 @@ function renderEnterpriseStatus(status) {
1632
1813
  if (!target) return;
1633
1814
  var vertex = status && status.vertex ? status.vertex : {};
1634
1815
  var dfcx = status && status.dfcx ? status.dfcx : {};
1816
+ var localLlm = status && status.chat ? status.chat.localLlmEndpointConfigured : false;
1817
+ var connectionMode = localLlm
1818
+ ? 'Local/open-source LLM configured'
1819
+ : (dfcx.liveAgentConfigured ? 'Google agent guard configured' : (vertex.configured ? 'Vertex routing configured' : 'Local deterministic fallback'));
1635
1820
  var rows = [
1636
- { name: 'Vertex routing', value: vertex.configured ? 'Configured' : 'Not configured', note: vertex.projectId ? ('Project: ' + vertex.projectId + ' ยท ' + (vertex.location || '')) : 'Run setup-vertex or set GOOGLE_VERTEX_PROJECT.' },
1637
- { name: 'DFCX live agent', value: dfcx.liveAgentConfigured ? 'Env present' : 'Not proven', note: dfcx.verification || 'Verify with REST/console evidence.' },
1638
- { name: 'Fulfillment proxy', value: dfcx.fulfillmentProxyConfigured ? 'Configured' : 'Not configured', note: 'Set THUMBGATE_DFCX_FULFILLMENT_URL for a deployed proxy.' },
1639
- { name: 'gcloud CX command', value: 'Unsupported', note: 'Do not use the old alpha gcloud CX command group; use REST API or console.' }
1821
+ { name: 'Chat runtime', value: connectionMode, note: localLlm ? 'Using THUMBGATE_LOCAL_LLM_ENDPOINT for local/open-source inference.' : 'Local deterministic summaries work without cloud credentials; configure a local LLM for generated answers.' },
1822
+ { name: 'Data boundary', value: 'Local by default', note: 'Dashboard answers are grounded in local ThumbGate data and do not require Google Cloud.' },
1823
+ { name: 'Vertex adapter', value: vertex.configured ? 'Configured' : 'Optional', note: vertex.projectId ? ('Project: ' + vertex.projectId + ' ยท ' + (vertex.location || '')) : 'Only needed when the buyer chooses Google-hosted routing.' },
1824
+ { name: 'Dialogflow guard adapter', value: dfcx.liveAgentConfigured ? 'Configured' : 'Optional', note: dfcx.verification || 'Only needed for customer-owned Dialogflow CX agent deployments.' },
1825
+ { name: 'Legacy gcloud CX command', value: 'Unsupported', note: 'Do not use the old alpha gcloud CX command group; use REST API or console for DFCX proof.' }
1640
1826
  ];
1641
1827
  target.innerHTML = rows.map(function(row) {
1642
1828
  return '<div class="inventory-row"><div><div class="inventory-name">' + escHtml(row.name) + '</div><div class="inventory-subtitle">' + escHtml(row.note) + '</div></div><span class="remediation-action">' + escHtml(row.value) + '</span></div>';
1643
1829
  }).join('');
1644
1830
  }
1645
1831
 
1646
- async function loadEnterpriseDialogflowStatus() {
1832
+ async function loadEnterpriseDataChatStatus() {
1647
1833
  if (!API_KEY || isDemo) {
1648
1834
  renderEnterpriseStatus({ vertex: {}, dfcx: { verification: 'Connect to load local status.' } });
1649
1835
  return;
1650
1836
  }
1651
1837
  try {
1652
- var res = await fetch('/v1/enterprise/dialogflow/status', { headers: getHeaders() });
1838
+ var res = await fetch('/v1/enterprise/data-chat/status', { headers: getHeaders() });
1653
1839
  if (!res.ok) throw new Error('status unavailable');
1654
1840
  renderEnterpriseStatus(await res.json());
1655
1841
  } catch (err) {
@@ -1679,26 +1865,27 @@ async function sendEnterpriseChat() {
1679
1865
  }
1680
1866
  button.disabled = true;
1681
1867
  answer.className = 'enterprise-answer';
1682
- answer.textContent = 'Running the DFCX guard and reading local dashboard data...';
1868
+ answer.textContent = 'Local RAG (LanceDB + lessons) plus your LLM...';
1683
1869
  sources.innerHTML = '';
1684
1870
  try {
1685
- var res = await fetch('/v1/enterprise/dialogflow/chat', {
1871
+ var res = await fetch('/v1/chat', {
1686
1872
  method: 'POST',
1687
1873
  headers: getHeaders(),
1688
- body: JSON.stringify({ prompt: prompt })
1874
+ body: JSON.stringify({ question: prompt })
1689
1875
  });
1690
1876
  var data = await res.json();
1691
- if (!res.ok) throw new Error(data.detail || data.error || 'chat failed');
1692
- answer.className = 'enterprise-answer' + (data.blocked ? ' blocked' : '');
1693
- answer.textContent = data.answer || 'No answer returned.';
1877
+ if (!res.ok) throw new Error(data.detail || data.error || data.message || 'chat failed');
1878
+ answer.className = 'enterprise-answer';
1879
+ answer.textContent = data.answer || data.message || 'No answer returned.';
1694
1880
  var list = Array.isArray(data.sources) ? data.sources : [];
1695
1881
  sources.innerHTML = list.map(function(source) {
1696
- return '<span class="enterprise-source">' + escHtml(source) + '</span>';
1882
+ var label = typeof source === 'string' ? source : (source && (source.title || source.id || ''));
1883
+ return '<span class="enterprise-source">' + escHtml(label) + '</span>';
1697
1884
  }).join('');
1698
- renderEnterpriseStatus(data.status || {});
1885
+ loadEnterpriseDataChatStatus();
1699
1886
  } catch (err) {
1700
1887
  answer.className = 'enterprise-answer blocked';
1701
- answer.textContent = err.message || 'Enterprise chat failed.';
1888
+ answer.textContent = err.message || 'Chat failed. Configure a local LLM endpoint or API key for RAG.';
1702
1889
  } finally {
1703
1890
  button.disabled = false;
1704
1891
  }
@@ -1980,6 +2167,7 @@ function loadDemo() {
1980
2167
 
1981
2168
  // Auto-load demo on first visit so visitors see the product immediately
1982
2169
  window.addEventListener('DOMContentLoaded', function() {
2170
+ preserveProjectQueryParams();
1983
2171
  if (hasBootstrapKey()) {
1984
2172
  connect({ key: BOOTSTRAP_API_KEY, localPro: true });
1985
2173
  return;
package/public/guide.html CHANGED
@@ -330,7 +330,7 @@ npx thumbgate init --agent gemini</code></pre>
330
330
  <tr><td>Auto-generates rules</td><td>Yes โ€” from repeated failures</td><td>No</td><td>No</td></tr>
331
331
  <tr><td>Agent support</td><td>Claude Code, Codex, Gemini, Amp, Cursor, OpenCode</td><td>Claude Code, Cursor, Windsurf, Cline</td><td>Claude, Cursor</td></tr>
332
332
  <tr><td>Install</td><td><code>npx thumbgate init</code></td><td><code>npx speclock setup</code></td><td>Cloud signup</td></tr>
333
- <tr><td>Cost</td><td>Free (Pro $19/mo or $149/yr, Team rollout $49/seat/mo)</td><td>Free</td><td>Free tier + paid</td></tr>
333
+ <tr><td>Cost</td><td>Free (Pro $19/mo or $149/yr, Enterprise custom pricing)</td><td>Free</td><td>Free tier + paid</td></tr>
334
334
  </table>
335
335
 
336
336
  <h2>Common Scenarios</h2>
@@ -379,7 +379,7 @@ npx thumbgate init --agent gemini</code></pre>
379
379
  <a rel="nofollow noopener noreferrer" target="_blank" href="https://buy.stripe.com/00w14neyUcXA5pL5e33sI0e" class="cta cta-secondary">Pay $499 diagnostic</a>
380
380
  <a rel="nofollow noopener noreferrer" target="_blank" href="https://buy.stripe.com/fZu9AT76saPsg4pbCr3sI0f" class="cta cta-secondary">Pay $1500 sprint</a>
381
381
  <a href="https://thumbgate.ai/#workflow-sprint-intake" class="cta cta-secondary">Send workflow first</a>
382
- <p style="color:var(--muted); font-size:0.85rem;">Free: 5 captures/day, 25 total captures, 3 active prevention rules, hook blocking. Pro: hosted sync, dashboard, recall, lesson search, unlimited captures/rules, DPO export. Team: intake first, then $49/seat/mo with a 3-seat minimum.</p>
382
+ <p style="color:var(--muted); font-size:0.85rem;">Free: 5 captures/day, 25 total captures, 3 active prevention rules, hook blocking. Pro: hosted sync, dashboard, recall, lesson search, unlimited captures/rules, DPO export. Enterprise: intake first, then custom pricing scoped to your rollout.</p>
383
383
 
384
384
  </div>
385
385
  </body>