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
@@ -3,12 +3,12 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>ThumbGate for Codex - Auto-Updating MCP Plugin</title>
6
+ <title>ThumbGate for Codex - CLI Setup and Plugin Bundle</title>
7
7
  <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
8
- <meta name="description" content="Install ThumbGate for Codex with an auto-updating MCP plugin, Pre-Action Checks, thumbs-up/down feedback memory, and a local-first Reliability Gateway.">
8
+ <meta name="description" content="Install ThumbGate for Codex with CLI setup first, plus a portable Codex plugin bundle for review, marketplace, and offline workflows. Includes Pre-Action Checks and thumbs-up/down feedback memory.">
9
9
  <meta name="keywords" content="ThumbGate Codex plugin, Codex MCP server, Codex pre-action checks, Codex guardrails, thumbgate latest, AI coding agent reliability">
10
10
  <meta property="og:title" content="ThumbGate for Codex">
11
- <meta property="og:description" content="Auto-updating MCP and hook launcher for Codex. One install, then ThumbGate resolves the latest npm runtime when Codex starts.">
11
+ <meta property="og:description" content="CLI-first Codex setup with auto-updating MCP, hooks, and a portable plugin bundle for review or marketplace workflows.">
12
12
  <meta property="og:type" content="website">
13
13
  <meta property="og:url" content="https://thumbgate.ai/codex-plugin">
14
14
  <link rel="canonical" href="https://thumbgate.ai/codex-plugin">
@@ -21,7 +21,7 @@
21
21
  "name": "ThumbGate for Codex",
22
22
  "applicationCategory": "DeveloperApplication",
23
23
  "operatingSystem": "macOS, Linux, Windows with Node.js",
24
- "description": "ThumbGate for Codex installs an MCP server and hook launcher that resolves thumbgate@latest at startup, captures thumbs-up/down feedback, and enforces Pre-Action Checks before risky agent actions run.",
24
+ "description": "ThumbGate for Codex installs an MCP server and hook launcher that resolves thumbgate@latest at startup, captures thumbs-up/down feedback, and enforces Pre-Action Checks before risky agent actions run. The supported fast path is npx thumbgate init --agent codex; the zip is a portable plugin bundle, not a double-click desktop installer.",
25
25
  "url": "https://thumbgate.ai/codex-plugin",
26
26
  "downloadUrl": "https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-codex-plugin.zip",
27
27
  "installUrl": "https://thumbgate.ai/codex-plugin",
@@ -71,7 +71,15 @@
71
71
  "name": "What is the fastest Codex install path?",
72
72
  "acceptedAnswer": {
73
73
  "@type": "Answer",
74
- "text": "Run npx thumbgate init --agent codex for the automatic local setup, or use the standalone Codex plugin bundle if you want a portable plugin surface."
74
+ "text": "Run npx thumbgate init --agent codex for the automatic local setup. Use the standalone Codex plugin bundle only when you want a portable plugin folder for review, marketplace wiring, offline handoff, or a Codex build that exposes local plugin import."
75
+ }
76
+ },
77
+ {
78
+ "@type": "Question",
79
+ "name": "Does the release zip install itself in Codex Desktop?",
80
+ "acceptedAnswer": {
81
+ "@type": "Answer",
82
+ "text": "No. The zip is not a double-click installer. Extract it and install the folder through a Codex plugin directory or marketplace flow if your Codex build exposes one. Otherwise use npx thumbgate init --agent codex."
75
83
  }
76
84
  }
77
85
  ]
@@ -220,9 +228,9 @@
220
228
  <p class="sub" style="font-size:13px;opacity:0.85;">Updated: <time datetime="2026-04-20">2026-04-20</time> · by <a href="https://github.com/IgorGanapolsky" style="color:inherit;">Igor Ganapolsky</a></p>
221
229
  <p class="sub">ThumbGate wires Codex into local-first feedback memory, MCP tools, and hook enforcement. The launcher resolves <code>thumbgate@latest</code> when Codex starts, so published npm fixes reach your active MCP server after a restart.</p>
222
230
  <div class="actions">
223
- <a class="button primary" href="https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-codex-plugin.zip" target="_blank" rel="noopener" onclick="if(typeof plausible==='function')plausible('codex_plugin_download')">Download Codex plugin</a>
231
+ <a class="button primary" href="/guide" onclick="if(typeof plausible==='function')plausible('codex_cli_setup')">Install with CLI setup</a>
224
232
  <a class="button secondary" href="https://github.com/IgorGanapolsky/ThumbGate/blob/main/plugins/codex-profile/INSTALL.md" target="_blank" rel="noopener">Read install docs</a>
225
- <a class="button secondary" href="/guide">Use CLI setup</a>
233
+ <a class="button secondary" href="https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-codex-plugin.zip" target="_blank" rel="noopener" onclick="if(typeof plausible==='function')plausible('codex_plugin_download')">Download zip for review</a>
226
234
  </div>
227
235
  <nav class="proof-bar" aria-label="Codex proof and conversion links">
228
236
  <a href="https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md" target="_blank" rel="noopener">Verification evidence</a>
@@ -231,8 +239,9 @@
231
239
  <a href="/?utm_source=codex&utm_medium=plugin_page&utm_campaign=codex_team_follow_on&utm_content=workflow_sprint&campaign_variant=teams_follow_on&offer_code=CODEX-TEAMS_FOLLOW_ON&cta_id=codex_team_follow_on&cta_placement=plugin_page&surface=codex_plugin#workflow-sprint-intake">Team workflow sprint</a>
232
240
  </nav>
233
241
  <pre class="terminal">$ npx thumbgate init --agent codex
242
+ $ npx thumbgate feedback-self-test
234
243
  # Writes ~/.codex/config.toml and ~/.codex/config.json
235
- # MCP + hooks install thumbgate@latest before serving or checking gates</pre>
244
+ # Restart Codex, then confirm ThumbGate is enabled in Plugins or MCP settings</pre>
236
245
  </div>
237
246
  </section>
238
247
 
@@ -251,11 +260,31 @@
251
260
  </div>
252
261
  </section>
253
262
 
263
+ <section class="wrap">
264
+ <div class="eyebrow">Role plugins, Sites, and team OS</div>
265
+ <h2>Codex is becoming a business-work surface. ThumbGate is the action boundary.</h2>
266
+ <div class="steps">
267
+ <div class="step">
268
+ <h3>Role plugins</h3>
269
+ <p>Codex plugins bundle skills, apps, and MCP servers. ThumbGate checks role-specific writes before sales, analytics, design, finance, or support agents modify business systems.</p>
270
+ </div>
271
+ <div class="step">
272
+ <h3>Sites deploys</h3>
273
+ <p>Before a Sites workflow publishes or widens access, ThumbGate can require build proof, intended audience, secret handling, and deployment evidence.</p>
274
+ </div>
275
+ <div class="step">
276
+ <h3>Team Agentic OS</h3>
277
+ <p>Human-editable docs, agent-operating files, and git backups still need runtime checks. ThumbGate gates protected skills, MCP config, memory scope, and workflow contracts.</p>
278
+ </div>
279
+ </div>
280
+ <p><a href="/learn/codex-role-plugins-need-governance">Read the Codex role-plugin governance guide</a> · <a href="/learn/agentic-os-team-governance">Read the Agentic OS team governance guide</a> · <a href="/learn/cost-aware-agent-gate-routing">Read cost-aware gate routing</a></p>
281
+ </section>
282
+
254
283
  <section class="wrap split">
255
284
  <div>
256
285
  <div class="eyebrow">What Codex gets</div>
257
286
  <h2>MCP memory, hook checks, and a dashboard lane in the same install path.</h2>
258
- <p>Use the standalone plugin when you want a portable Codex bundle. Use <code>npx thumbgate init --agent codex</code> when you want the shortest path on this machine. Both paths point at the same Reliability Gateway and the same npm runtime.</p>
287
+ <p>Use <code>npx thumbgate init --agent codex</code> when you want the shortest path on this machine. Use the standalone zip only when you need a portable plugin folder for audit, offline handoff, marketplace wiring, or a Codex build that exposes local plugin import.</p>
259
288
  <p class="status">The Codex launcher checks npm on startup. Restart Codex after a ThumbGate publish to let the MCP server and hook bundle pick up the latest runtime.</p>
260
289
  </div>
261
290
  <figure class="proof">
@@ -273,12 +302,31 @@
273
302
  <p>Run <code>npx thumbgate init --agent codex</code>. ThumbGate writes the MCP server block and hook bundle into your Codex config files.</p>
274
303
  </div>
275
304
  <div class="step">
276
- <h3>2. Standalone plugin</h3>
277
- <p>Use the release bundle when Codex loads plugin surfaces directly. The bundle includes the manifest, MCP config, marketplace entry, and install docs.</p>
305
+ <h3>2. Plugin directory</h3>
306
+ <p>For a true plugin install, use Codex Plugins or a marketplace source. In the Add marketplace dialog, do not keep Codex's default <code>plugins/codex</code> sparse path for this repo; use <code>.agents/plugins/marketplace.json</code> and <code>plugins/codex-profile</code>, or leave sparse paths blank for a local checkout.</p>
278
307
  </div>
279
308
  <div class="step">
280
- <h3>3. Verify in Codex</h3>
281
- <p>Open Codex settings, confirm <code>thumbgate</code> is toggled on, then restart Codex after npm releases to pick up the latest runtime.</p>
309
+ <h3>3. Zip for review</h3>
310
+ <p>The zip is a portable folder, not a double-click installer. Extract it, inspect <code>.codex-plugin/plugin.json</code>, and use CLI setup when local plugin import is unavailable.</p>
311
+ </div>
312
+ </div>
313
+ </section>
314
+
315
+ <section class="wrap">
316
+ <div class="eyebrow">Desktop install reality</div>
317
+ <h2>Do not make users guess what to do with a zip.</h2>
318
+ <div class="steps">
319
+ <div class="step">
320
+ <h3>Use first</h3>
321
+ <p><code>npx thumbgate init --agent codex</code> is the supported self-serve path. It configures MCP, hooks, and the status line without asking the user to understand plugin internals.</p>
322
+ </div>
323
+ <div class="step">
324
+ <h3>Use when available</h3>
325
+ <p>If Codex shows Plugins, open the directory, clear restrictive filters like <code>Built by OpenAI</code>, install ThumbGate from a marketplace or shared plugin entry, then start a new thread after install.</p>
326
+ </div>
327
+ <div class="step">
328
+ <h3>Use for operators</h3>
329
+ <p>Download the zip only for security review, offline delivery, or manual marketplace wiring. The user selects the extracted folder, never the compressed file.</p>
282
330
  </div>
283
331
  </div>
284
332
  </section>
@@ -312,7 +360,11 @@
312
360
  </div>
313
361
  <div class="faq">
314
362
  <h3>Where is the direct asset?</h3>
315
- <p>The standalone zip remains available at <a href="https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-codex-plugin.zip" target="_blank" rel="noopener">GitHub Releases</a>. This page is the human install surface so users do not land on an unexplained file download.</p>
363
+ <p>The standalone zip remains available at <a href="https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-codex-plugin.zip" target="_blank" rel="noopener">GitHub Releases</a>. It is for review, offline delivery, and manual marketplace workflows. It is not a double-click Codex Desktop installer.</p>
364
+ </div>
365
+ <div class="faq">
366
+ <h3>I searched Plugins for ThumbGate. Why is it missing?</h3>
367
+ <p>First clear the <code>Built by OpenAI</code> filter. ThumbGate is a local or third-party marketplace plugin, so an OpenAI-only filter hides it. Then confirm the marketplace includes <code>.agents/plugins/marketplace.json</code> and <code>plugins/codex-profile</code>; the default <code>plugins/codex</code> sparse path will miss this repo.</p>
316
368
  </div>
317
369
  </div>
318
370
  </section>
@@ -270,7 +270,11 @@
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 your captured lessons + Gemini/Perplexity hybrid (local-cloud).</span>
275
+ <input type="password" id="geminiKeyInput" placeholder="Set 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')">🏢 Enterprise 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,8 +416,8 @@
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>Enterprise Data Chat</h2>
420
+ <p class="template-summary">Ask questions over local ThumbGate feedback, lessons, gates, team posture, and Vertex/DFCX readiness. This is a governed local data-query panel with a DFCX-compatible guard before data access; it does not claim a live Google Dialogflow CX agent unless deployment evidence is configured.</p>
416
421
  <div class="enterprise-chat-layout">
417
422
  <div class="panel">
418
423
  <h3>Chat With Local ThumbGate Data</h3>
@@ -429,7 +434,30 @@
429
434
  <div class="panel">
430
435
  <h3>Enterprise 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;">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>. Without that proof, the panel stays local-only.</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,9 +1307,70 @@ function renderDashboardData(data) {
1187
1307
  renderRegulatedProof(data.regulatedProof || {});
1188
1308
  renderTemplates(data.templateLibrary || {});
1189
1309
  renderInsights(data);
1310
+ loadAiInventory();
1190
1311
  loadEnterpriseDialogflowStatus();
1191
1312
  }
1192
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
+ }
1372
+ }
1373
+
1193
1374
  function renderGeneratedViewToolbar(spec) {
1194
1375
  var views = Array.isArray(spec.availableViews) ? spec.availableViews : [];
1195
1376
  document.getElementById('generatedViewToolbar').innerHTML = views.map(function(view) {
@@ -1632,7 +1813,11 @@ 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 connectionMode = dfcx.liveAgentConfigured
1817
+ ? 'Dialogflow CX configured'
1818
+ : (vertex.configured ? 'Vertex configured' : 'Local-only');
1635
1819
  var rows = [
1820
+ { name: 'Connection mode', value: connectionMode, note: dfcx.liveAgentConfigured ? 'Live DFCX evidence is present in env.' : (vertex.configured ? 'Vertex routing is configured; DFCX still needs live-agent proof.' : 'Local data Q&A only; no cloud agent claim.') },
1636
1821
  { 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
1822
  { name: 'DFCX live agent', value: dfcx.liveAgentConfigured ? 'Env present' : 'Not proven', note: dfcx.verification || 'Verify with REST/console evidence.' },
1638
1823
  { name: 'Fulfillment proxy', value: dfcx.fulfillmentProxyConfigured ? 'Configured' : 'Not configured', note: 'Set THUMBGATE_DFCX_FULFILLMENT_URL for a deployed proxy.' },
@@ -1679,7 +1864,7 @@ async function sendEnterpriseChat() {
1679
1864
  }
1680
1865
  button.disabled = true;
1681
1866
  answer.className = 'enterprise-answer';
1682
- answer.textContent = 'Running the DFCX guard and reading local dashboard data...';
1867
+ answer.textContent = 'Running the data-access guard and reading local dashboard data...';
1683
1868
  sources.innerHTML = '';
1684
1869
  try {
1685
1870
  var res = await fetch('/v1/enterprise/dialogflow/chat', {
@@ -1980,6 +2165,7 @@ function loadDemo() {
1980
2165
 
1981
2166
  // Auto-load demo on first visit so visitors see the product immediately
1982
2167
  window.addEventListener('DOMContentLoaded', function() {
2168
+ preserveProjectQueryParams();
1983
2169
  if (hasBootstrapKey()) {
1984
2170
  connect({ key: BOOTSTRAP_API_KEY, localPro: true });
1985
2171
  return;