thumbgate 1.26.5 → 1.26.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate-marketplace",
3
- "version": "1.26.5",
3
+ "version": "1.26.7",
4
4
  "owner": {
5
5
  "name": "Igor Ganapolsky",
6
6
  "email": "ig5973700@gmail.com"
@@ -14,7 +14,7 @@
14
14
  "source": "npm",
15
15
  "package": "thumbgate"
16
16
  },
17
- "version": "1.26.5",
17
+ "version": "1.26.7",
18
18
  "author": {
19
19
  "name": "Igor Ganapolsky",
20
20
  "email": "ig5973700@gmail.com",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "thumbgate",
3
3
  "description": "One 👎 becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
4
- "version": "1.26.5",
4
+ "version": "1.26.7",
5
5
  "author": {
6
6
  "name": "Igor Ganapolsky",
7
7
  "email": "ig5973700@gmail.com",
8
8
  "url": "https://thumbgate.ai"
9
9
  },
10
10
  "homepage": "https://thumbgate.ai",
11
- "repository": "git+https://github.com/IgorGanapolsky/ThumbGate",
11
+ "repository": "https://github.com/IgorGanapolsky/ThumbGate",
12
12
  "license": "MIT",
13
13
  "category": "developer-tools",
14
14
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.26.5",
3
+ "version": "1.26.7",
4
4
  "description": "ThumbGate — 👍👎 feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "transport": "stdio",
package/README.md CHANGED
@@ -335,8 +335,36 @@ npx thumbgate dashboard # open local dashboard
335
335
  npx thumbgate serve # start MCP server on stdio
336
336
  npx thumbgate bench # run reliability benchmark
337
337
  npx thumbgate bench --programbench-smoke # include cleanroom whole-repo proof lane
338
+ npx thumbgate break-glass --reason="ThumbGate over-fired" # short TTL recovery for gate over-fire
338
339
  ```
339
340
 
341
+ ### Recovery if a gate over-fires
342
+
343
+ ThumbGate should block repeated unsafe actions, not trap the operator. If a noisy rule or stale memory pattern blocks the hook/settings change you need to recover, open a short-lived break-glass window:
344
+
345
+ ```bash
346
+ npx thumbgate break-glass --reason="ThumbGate over-fired and blocked operator recovery"
347
+ ```
348
+
349
+ What this unlocks for up to 5 minutes:
350
+
351
+ - Edits to `.claude/settings.local.json`, `.claude/settings.json`, `.codex/config.toml`, and the same files inside nested workspaces.
352
+ - The short-lived proof gates used for PR recovery: `pr_create_allowed` and `pr_threads_checked`.
353
+
354
+ What stays gated:
355
+
356
+ - Force pushes, protected-branch pushes, broad `rm -rf`, unsafe `chmod`, package publishes/releases, and local-only remote side effects.
357
+ - Arbitrary protected files such as `README.md`, `AGENTS.md`, policy bundles, or credentials.
358
+
359
+ Verify the recovery window and runtime health before continuing:
360
+
361
+ ```bash
362
+ npx thumbgate break-glass --reason="verify recovery path" --json
363
+ npx thumbgate doctor
364
+ ```
365
+
366
+ If you change MCP or hook settings, restart the affected agent session so Claude Code, Cursor, Codex, or another runtime reloads `.mcp.json` and local settings.
367
+
340
368
  ---
341
369
 
342
370
  ## Pricing
@@ -505,18 +533,20 @@ Free and self-hosted users can invoke `search_lessons` directly through MCP, and
505
533
  For enterprise subscriptions, ThumbGate natively integrates with Google Cloud Platform and **Vertex AI** to route all agent checks through compliant Gemini models inside your corporate VPC.
506
534
 
507
535
  ### Zero-Friction Setup
508
- To instantly wire your local installation to Google Cloud, simply run:
536
+ To wire local ThumbGate scoring to Vertex AI, run:
509
537
  ```bash
510
538
  npx thumbgate setup-vertex
511
539
  ```
512
540
  * **Auto-Discovery:** Automatically detects your active authenticated `gcloud` session and active project ID.
513
541
  * **Auto-Enablement:** Programmatically enables the Vertex AI API in your project.
514
- * **Auto-Configuration:** Writes secure billing and project credentials directly to your local `.env` file.
542
+ * **Auto-Configuration:** Writes local Vertex routing settings to your `.env` file.
543
+
544
+ This command does **not** create or verify a live Dialogflow CX agent. On current Google Cloud CLI installs, the old alpha gcloud CX command group is not available; verify Conversational Agents / Dialogflow CX with the Google Cloud console or the official Dialogflow CX REST API (`projects.locations.agents`) before claiming a live DFCX deployment.
515
545
 
516
546
  ### Zero-Friction Cost Containment ($10/mo Hard Cap)
517
547
  Google Cloud budget alerts are "alert-only" and do not stop API traffic, risking unexpected bill shock. ThumbGate completely resolves this on the client side:
518
548
  * **Instant Shutdown:** ThumbGate maintains a lightweight, local token ledger and instantly halts outgoing API traffic the millisecond your monthly token spending approaches the **$10 limit** (500k tokens of Gemini 1.5 Flash).
519
- * **Bypasses Console Complexity:** Requires **zero** GCP web console setups, zero Pub/Sub topics, and zero Cloud Functions. Perfect for non-technical managers and teams.
549
+ * **Bypasses extra shutdown plumbing:** Requires no Pub/Sub or Cloud Functions for the local ThumbGate-side stop condition. You still need normal Google Cloud billing/API setup and live-agent verification for DFCX pilots.
520
550
 
521
551
  ---
522
552
 
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.26.5", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.26.7", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.26.5", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.26.7", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -231,7 +231,7 @@ const {
231
231
  finalizeSession: finalizeFeedbackSession,
232
232
  } = require('../../scripts/feedback-session');
233
233
 
234
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.26.5' };
234
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.26.7' };
235
235
  const COMMERCE_CATEGORIES = [
236
236
  'product_recommendation',
237
237
  'brand_compliance',
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.26.5",
10
+ "thumbgate@1.26.7",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/bin/cli.js CHANGED
@@ -700,10 +700,11 @@ async function setupVertex() {
700
700
  // 4. Print gorgeous success activation box
701
701
  console.log('');
702
702
  console.log(' ╭──────────────────────────────────────────────────────────╮');
703
- console.log(' │ 🎉 Vertex AI Setup Complete — ZERO FRICTION! │');
703
+ console.log(' │ Vertex AI Setup Complete │');
704
704
  console.log(' │ │');
705
- console.log(' │ ThumbGate is now fully wired to your GCP environment. │');
706
- console.log(' │ All agent checks will route securely via Vertex AI. │');
705
+ console.log(' │ ThumbGate wrote local Vertex routing config. │');
706
+ console.log(' │ This does not create or verify a Dialogflow CX agent. │');
707
+ console.log(' │ Verify DFCX with the console or Dialogflow CX REST API. │');
707
708
  console.log(' │ │');
708
709
  console.log(' │ Try a test run: │');
709
710
  console.log(' │ npx thumbgate feedback-self-test │');
@@ -2407,6 +2408,11 @@ function optimize() {
2407
2408
  doOptimize();
2408
2409
  }
2409
2410
 
2411
+ function syncGcp() {
2412
+ const { syncToGcp } = require(path.join(PKG_ROOT, 'adapters', 'gcp', 'sync.js'));
2413
+ syncToGcp();
2414
+ }
2415
+
2410
2416
  function cleanup() {
2411
2417
  console.log('Cleaning up ThumbGate processes...');
2412
2418
  try {
@@ -2963,7 +2969,7 @@ const SUBCOMMAND_HELP = {
2963
2969
  suggest: 'Usage: npx thumbgate suggest <gate-id>\n\nSuggest fixes for a specific gate based on lesson history.',
2964
2970
  cost: 'Usage: npx thumbgate cost [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nShow cumulative $ and tokens saved by PreToolUse gate blocks. Reads ~/.thumbgate/gate-stats.json.',
2965
2971
  savings: 'Usage: npx thumbgate savings [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nAlias for `thumbgate cost`.',
2966
- 'setup-vertex': 'Usage: npx thumbgate setup-vertex\n\nAuto-enable Vertex AI API on GCP and write secure credentials to local .env.',
2972
+ 'setup-vertex': 'Usage: npx thumbgate setup-vertex\n\nAuto-enable Vertex AI API on GCP and write local Vertex routing config to .env. This does not create or verify a Dialogflow CX agent; use the Dialogflow CX REST API or console for live-agent evidence.',
2967
2973
  brain: 'Usage: npx thumbgate brain [--write] [--json] [--limit=N]\n\nBuild the agent-readable "context brain" — a single artifact consolidating this\nrepo\'s lessons, prevention rules, active gates, and project context for a coding\nagent to read BEFORE acting. --write saves it to .thumbgate/BRAIN.md (versioned,\ndeterministic). --json emits the structured model. --limit caps lessons (default 15).',
2968
2974
  };
2969
2975
 
@@ -3120,6 +3126,9 @@ switch (COMMAND) {
3120
3126
  case 'cleanup':
3121
3127
  cleanup();
3122
3128
  break;
3129
+ case 'sync-gcp':
3130
+ syncGcp();
3131
+ break;
3123
3132
  case 'gate-check':
3124
3133
  gateCheck().catch((err) => {
3125
3134
  console.error(err && err.message ? err.message : err);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.26.5",
3
+ "version": "1.26.7",
4
4
  "description": "ThumbGate self-improving agent governance: thumbs-up/down turns every mistake into a prevention rule and blocks repeat patterns. 36 pre-action checks, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/IgorGanapolsky/ThumbGate.git"
8
+ "url": "https://github.com/IgorGanapolsky/ThumbGate.git"
9
9
  },
10
10
  "bugs": {
11
11
  "url": "https://github.com/IgorGanapolsky/ThumbGate/issues"
@@ -63,6 +63,7 @@
63
63
  "scripts/context-manager.js",
64
64
  "scripts/contextfs.js",
65
65
  "scripts/conversation-context.js",
66
+ "scripts/dashboard-chat.js",
66
67
  "scripts/dashboard-render-spec.js",
67
68
  "scripts/dashboard.js",
68
69
  "scripts/decision-journal.js",
@@ -189,11 +189,18 @@
189
189
  .settings-card .team-value, .origin-value { font-size: 18px; font-weight: 700; color: var(--text); margin-top: 8px; word-break: break-word; }
190
190
  .origin-list, .layer-list, .routing-list { display: flex; flex-direction: column; gap: 12px; }
191
191
  .origin-note, .layer-note, .routing-note { font-size: 13px; color: var(--text-muted); line-height: 1.55; }
192
+ .enterprise-chat-layout { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr); gap: 16px; align-items: start; }
193
+ .enterprise-chat-box { min-height: 110px; resize: vertical; width: 100%; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; color: var(--text); font-size: 14px; font-family: var(--font); line-height: 1.5; }
194
+ .enterprise-chat-box:focus { outline: none; border-color: var(--cyan); }
195
+ .enterprise-answer { margin-top: 14px; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px; padding: 16px; min-height: 92px; font-size: 14px; color: var(--text); line-height: 1.65; }
196
+ .enterprise-answer.blocked { border-color: rgba(248,113,113,0.45); background: rgba(248,113,113,0.07); }
197
+ .enterprise-source-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
198
+ .enterprise-source { font-size: 11px; border: 1px solid var(--border); border-radius: 999px; padding: 4px 9px; color: var(--text-muted); background: var(--bg-card); }
192
199
 
193
200
  @media (max-width: 700px) {
194
201
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
195
202
  .search-filters { flex-wrap: wrap; }
196
- .team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid { grid-template-columns: 1fr; }
203
+ .team-grid, .template-grid, .team-columns, .settings-grid, .generated-grid, .inventory-grid, .enterprise-chat-layout { grid-template-columns: 1fr; }
197
204
  }
198
205
  </style>
199
206
  </head>
@@ -253,6 +260,19 @@
253
260
  <a class="stat-card" data-card-action="gates" onclick="selectCard(this,'gates');return false;" href="#" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view active checks"><div class="stat-label">Active Gates</div><div class="stat-value cyan" id="statGates">—</div></a>
254
261
  </div>
255
262
 
263
+ <div class="panel" id="chatPanel" style="margin-bottom:20px;">
264
+ <div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
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>
267
+ </div>
268
+ <div id="chatMessages" style="max-height:360px;overflow-y:auto;margin-bottom:12px;display:none;padding-right:4px;"></div>
269
+ <div style="display:flex;gap:8px;">
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
+ <button class="btn" id="chatSend" onclick="sendChat()">Ask</button>
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>
274
+ </div>
275
+
256
276
  <div class="panel" id="reviewDeltaPanel" style="margin-bottom:20px;">
257
277
  <div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;">
258
278
  <div>
@@ -284,6 +304,7 @@
284
304
  <div class="tab active" onclick="switchTab('search')">🔍 Search Memories</div>
285
305
  <div class="tab" onclick="switchTab('gates')">🛡️ Active Gates</div>
286
306
  <div class="tab" onclick="switchTab('team')">👥 Team</div>
307
+ <div class="tab" onclick="switchTab('enterprise')">🏢 Enterprise Chat</div>
287
308
  <div class="tab" onclick="switchTab('generated')">🧩 Generated Views</div>
288
309
  <div class="tab" onclick="switchTab('settings')">⚙️ Policy Origins</div>
289
310
  <div class="tab" onclick="switchTab('templates')">🧱 Gate Templates</div>
@@ -387,6 +408,33 @@
387
408
  </div>
388
409
  </div>
389
410
 
411
+ <!-- ENTERPRISE CHAT TAB -->
412
+ <div class="tab-content" id="tab-enterprise">
413
+ <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>
416
+ <div class="enterprise-chat-layout">
417
+ <div class="panel">
418
+ <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>
420
+ <div style="display:flex;gap:10px;align-items:center;margin-top:12px;flex-wrap:wrap;">
421
+ <button class="btn" id="enterpriseChatBtn" onclick="sendEnterpriseChat()">Ask ThumbGate</button>
422
+ <button class="btn-outline" onclick="setEnterprisePrompt('Which gates are blocking risky actions?')">Gates</button>
423
+ <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>
425
+ </div>
426
+ <div class="enterprise-answer" id="enterpriseChatAnswer">Connect your dashboard, then ask about local ThumbGate data.</div>
427
+ <div class="enterprise-source-list" id="enterpriseChatSources"></div>
428
+ </div>
429
+ <div class="panel">
430
+ <h3>Enterprise Readiness</h3>
431
+ <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>
433
+ </div>
434
+ </div>
435
+ </div>
436
+ </div>
437
+
390
438
  <!-- GENERATED TAB -->
391
439
  <div class="tab-content" id="tab-generated">
392
440
  <div class="templates-section">
@@ -637,6 +685,62 @@ function getHeaders() {
637
685
  return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
638
686
  }
639
687
 
688
+ // --- Chat with your data ---------------------------------------------------
689
+ function chatEscape(s) {
690
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
691
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
692
+ });
693
+ }
694
+ function chatAppend(who, text) {
695
+ var messages = document.getElementById('chatMessages');
696
+ var row = document.createElement('div');
697
+ row.style.cssText = 'margin-bottom:14px;';
698
+ row.innerHTML = '<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px;">'
699
+ + (who === 'you' ? 'You' : 'ThumbGate') + '</div><div class="chat-body" style="line-height:1.5;">' + chatEscape(text) + '</div>';
700
+ messages.appendChild(row);
701
+ return row.querySelector('.chat-body');
702
+ }
703
+ function chatRenderAnswer(a) {
704
+ return chatEscape(a)
705
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
706
+ .replace(/\[(\d+)\]/g, '<sup style="color:var(--cyan);font-weight:600;">[$1]</sup>')
707
+ .replace(/\n/g, '<br>');
708
+ }
709
+ function chatRenderSources(sources) {
710
+ if (!sources || !sources.length) return '';
711
+ var items = sources.map(function (s, i) {
712
+ 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>';
714
+ }).join('');
715
+ return '<div style="margin-top:10px;">' + items + '</div>';
716
+ }
717
+ async function sendChat() {
718
+ var input = document.getElementById('chatInput');
719
+ var q = (input.value || '').trim();
720
+ if (!q) return;
721
+ var messages = document.getElementById('chatMessages');
722
+ var sendBtn = document.getElementById('chatSend');
723
+ messages.style.display = 'block';
724
+ chatAppend('you', q);
725
+ input.value = '';
726
+ sendBtn.disabled = true;
727
+ var pending = chatAppend('bot', 'Thinking…');
728
+ try {
729
+ var res = await fetch('/v1/chat', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ question: q }) });
730
+ var data = await res.json();
731
+ if (data && data.ok) {
732
+ pending.innerHTML = chatRenderAnswer(data.answer) + chatRenderSources(data.sources);
733
+ } else {
734
+ pending.innerHTML = '<em style="color:var(--text-muted);">' + chatEscape((data && data.message) || 'Chat is unavailable.') + '</em>';
735
+ }
736
+ } catch (e) {
737
+ pending.innerHTML = '<em style="color:var(--text-muted);">Chat request failed: ' + chatEscape(String((e && e.message) || e)) + '</em>';
738
+ } finally {
739
+ sendBtn.disabled = false;
740
+ messages.scrollTop = messages.scrollHeight;
741
+ }
742
+ }
743
+
640
744
  function hasBootstrapKey() {
641
745
  return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
642
746
  }
@@ -646,25 +750,29 @@ async function connect(options) {
646
750
  var input = document.getElementById('apiKey');
647
751
  API_KEY = String(opts.key || input.value || '').trim();
648
752
  if (!API_KEY) return;
753
+
754
+ const isEnterprise = API_KEY.startsWith('tg_op_') || API_KEY.startsWith('tg_creator_');
755
+ const tierName = isEnterprise ? 'Enterprise' : 'Pro';
756
+
649
757
  const status = document.getElementById('authStatus');
650
758
  const btn = document.getElementById('connectBtn');
651
759
  btn.disabled = true;
652
760
  status.className = 'auth-status';
653
- status.textContent = opts.localPro ? 'Connecting local dashboard...' : 'Connecting...';
761
+ status.textContent = opts.localPro ? `Connecting local dashboard...` : 'Connecting...';
654
762
  try {
655
763
  const res = await fetch('/v1/feedback/stats', { headers: getHeaders() });
656
764
  if (!res.ok) throw new Error('Invalid API key');
657
765
  const data = await res.json();
658
766
  status.className = 'auth-status ok';
659
- status.textContent = opts.localPro ? '✓ Local Pro dashboard connected' : '✓ Connected';
767
+ status.textContent = opts.localPro ? `✓ Local ${tierName} connected` : '✓ Connected';
660
768
  document.getElementById('dashboardContent').style.display = 'block';
661
769
  if (opts.localPro) {
662
770
  input.value = 'local-license';
663
771
  input.disabled = true;
664
- input.placeholder = 'Local Pro auto-connected';
772
+ input.placeholder = `Local ${tierName} auto-connected`;
665
773
  btn.disabled = true;
666
774
  document.getElementById('demoBtn').style.display = 'none';
667
- document.getElementById('authHelp').textContent = 'Local Pro is active on this machine. Your personal dashboard is using the saved license key automatically.';
775
+ document.getElementById('authHelp').textContent = `Local ${tierName} is active on this machine. Your dashboard is using the saved license key automatically.`;
668
776
  }
669
777
  renderStats(data);
670
778
  setSelectedCard('all');
@@ -677,7 +785,7 @@ async function connect(options) {
677
785
  status.className = 'auth-status err';
678
786
  status.textContent = '✗ ' + e.message;
679
787
  if (opts.localPro) {
680
- document.getElementById('authHelp').textContent = 'Local Pro bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.';
788
+ document.getElementById('authHelp').textContent = `Local ${tierName} bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.`;
681
789
  }
682
790
  }
683
791
  if (!opts.localPro) {
@@ -1034,9 +1142,14 @@ async function markReviewed() {
1034
1142
  try {
1035
1143
  var res = await fetch('/v1/dashboard/review-state', {
1036
1144
  method: 'POST',
1037
- headers: getHeaders()
1145
+ headers: getHeaders(),
1146
+ body: JSON.stringify({ reviewedAt: new Date().toISOString() })
1038
1147
  });
1039
- if (!res.ok) throw new Error('Failed to save review checkpoint');
1148
+ if (!res.ok) {
1149
+ var errText = '';
1150
+ try { errText = await res.text(); } catch (_) { errText = ''; }
1151
+ throw new Error(errText || 'Failed to save review checkpoint');
1152
+ }
1040
1153
  var body = await res.json();
1041
1154
  renderReviewDelta(body.reviewDelta || {});
1042
1155
  } catch (e) {
@@ -1074,6 +1187,7 @@ function renderDashboardData(data) {
1074
1187
  renderRegulatedProof(data.regulatedProof || {});
1075
1188
  renderTemplates(data.templateLibrary || {});
1076
1189
  renderInsights(data);
1190
+ loadEnterpriseDialogflowStatus();
1077
1191
  }
1078
1192
 
1079
1193
  function renderGeneratedViewToolbar(spec) {
@@ -1513,6 +1627,83 @@ function renderTemplates(templateLibrary) {
1513
1627
  : '<div class="empty">No check templates available</div>';
1514
1628
  }
1515
1629
 
1630
+ function renderEnterpriseStatus(status) {
1631
+ var target = document.getElementById('enterpriseStatusCards');
1632
+ if (!target) return;
1633
+ var vertex = status && status.vertex ? status.vertex : {};
1634
+ var dfcx = status && status.dfcx ? status.dfcx : {};
1635
+ 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.' }
1640
+ ];
1641
+ target.innerHTML = rows.map(function(row) {
1642
+ 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
+ }).join('');
1644
+ }
1645
+
1646
+ async function loadEnterpriseDialogflowStatus() {
1647
+ if (!API_KEY || isDemo) {
1648
+ renderEnterpriseStatus({ vertex: {}, dfcx: { verification: 'Connect to load local status.' } });
1649
+ return;
1650
+ }
1651
+ try {
1652
+ var res = await fetch('/v1/enterprise/dialogflow/status', { headers: getHeaders() });
1653
+ if (!res.ok) throw new Error('status unavailable');
1654
+ renderEnterpriseStatus(await res.json());
1655
+ } catch (err) {
1656
+ var target = document.getElementById('enterpriseStatusCards');
1657
+ if (target) target.innerHTML = '<div class="empty">Enterprise status unavailable.</div>';
1658
+ }
1659
+ }
1660
+
1661
+ function setEnterprisePrompt(prompt) {
1662
+ var input = document.getElementById('enterpriseChatPrompt');
1663
+ if (input) input.value = prompt;
1664
+ }
1665
+
1666
+ async function sendEnterpriseChat() {
1667
+ var input = document.getElementById('enterpriseChatPrompt');
1668
+ var answer = document.getElementById('enterpriseChatAnswer');
1669
+ var sources = document.getElementById('enterpriseChatSources');
1670
+ var button = document.getElementById('enterpriseChatBtn');
1671
+ var prompt = input ? input.value.trim() : '';
1672
+ if (!prompt) {
1673
+ answer.textContent = 'Ask a question first.';
1674
+ return;
1675
+ }
1676
+ if (!API_KEY) {
1677
+ answer.textContent = 'Connect your dashboard before using Enterprise Chat.';
1678
+ return;
1679
+ }
1680
+ button.disabled = true;
1681
+ answer.className = 'enterprise-answer';
1682
+ answer.textContent = 'Running the DFCX guard and reading local dashboard data...';
1683
+ sources.innerHTML = '';
1684
+ try {
1685
+ var res = await fetch('/v1/enterprise/dialogflow/chat', {
1686
+ method: 'POST',
1687
+ headers: getHeaders(),
1688
+ body: JSON.stringify({ prompt: prompt })
1689
+ });
1690
+ 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.';
1694
+ var list = Array.isArray(data.sources) ? data.sources : [];
1695
+ sources.innerHTML = list.map(function(source) {
1696
+ return '<span class="enterprise-source">' + escHtml(source) + '</span>';
1697
+ }).join('');
1698
+ renderEnterpriseStatus(data.status || {});
1699
+ } catch (err) {
1700
+ answer.className = 'enterprise-answer blocked';
1701
+ answer.textContent = err.message || 'Enterprise chat failed.';
1702
+ } finally {
1703
+ button.disabled = false;
1704
+ }
1705
+ }
1706
+
1516
1707
  document.addEventListener('click', function(event) {
1517
1708
  var tagButton = event.target.closest('.tag[data-tag]');
1518
1709
  if (!tagButton) return;
@@ -2097,6 +2288,7 @@ function renderGateAuditChartFromData(gateAudit) {
2097
2288
  },
2098
2289
  });
2099
2290
  }
2291
+
2100
2292
  </script>
2101
2293
  </body>
2102
2294
  </html>
package/public/index.html CHANGED
@@ -20,7 +20,7 @@ __GOOGLE_SITE_VERIFICATION_META__
20
20
  <meta property="og:image" content="https://thumbgate.ai/og.png">
21
21
  <meta name="twitter:card" content="summary_large_image">
22
22
  <meta name="twitter:image" content="https://thumbgate.ai/og.png">
23
- <meta name="thumbgate-version" content="1.26.5">
23
+ <meta name="thumbgate-version" content="1.26.7">
24
24
  <meta name="keywords" content="ThumbGate, thumbgate, AI agent orchestration, AI experience orchestration, agentic development cycle, AC/DC framework, Guide Generate Verify Solve, agent enforcement layer, save LLM tokens, reduce Claude API cost, reduce OpenAI cost, AI agent token savings, prevent LLM retries, prevent hallucination retries, stop AI token waste, pre-action checks, agent governance, Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode, workflow hardening, context engineering, AI authenticity, brand authenticity AI">
25
25
  <link rel="canonical" href="__APP_ORIGIN__/">
26
26
  <link rel="alternate" type="text/markdown" title="ThumbGate LLM context" href="__APP_ORIGIN__/llm-context.md">
@@ -1594,7 +1594,7 @@ __GA_BOOTSTRAP__
1594
1594
  <a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
1595
1595
  <a href="/blog">Blog</a>
1596
1596
  </div>
1597
- <span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.26.5</span>
1597
+ <span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.26.7</span>
1598
1598
  </div>
1599
1599
  </footer>
1600
1600
 
@@ -25,7 +25,7 @@
25
25
  "alternateName": "thumbgate",
26
26
  "applicationCategory": "DeveloperApplication",
27
27
  "operatingSystem": "Cross-platform, Node.js >=18.18.0",
28
- "softwareVersion": "1.26.5",
28
+ "softwareVersion": "1.26.7",
29
29
  "url": "https://thumbgate.ai/numbers",
30
30
  "dateModified": "2026-05-07",
31
31
  "creator": {
@@ -202,7 +202,7 @@
202
202
  <main class="container">
203
203
  <h1>The Numbers</h1>
204
204
  <p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
205
- <div class="freshness">Updated: 2026-05-07 · Version 1.26.5</div>
205
+ <div class="freshness">Updated: 2026-05-07 · Version 1.26.7</div>
206
206
  <div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
207
207
 
208
208
  <h2>Gate enforcement</h2>
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ // scripts/dashboard-chat.js
4
+ // -----------------------------------------------------------------------------
5
+ // "Chat with your data" — the dashboard chat backend. Answers a natural-language
6
+ // question about THIS install's ThumbGate data (captured lessons + prevention
7
+ // rules) by retrieving the most relevant lessons and asking Gemini to answer
8
+ // grounded ONLY in that retrieved context (RAG). No data leaves the box except
9
+ // the retrieved snippets + the question, sent to the configured Gemini endpoint.
10
+ //
11
+ // Enterprise framing: this is the in-product "chat with your governed data"
12
+ // experience. (The Dialogflow CX messenger widget is the separate path where a
13
+ // customer connects their own DFCX agent + the ThumbGate webhook gate.)
14
+ // -----------------------------------------------------------------------------
15
+
16
+ const path = require('path');
17
+
18
+ const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models';
19
+ const DEFAULT_MODEL = 'gemini-2.5-flash';
20
+ const MAX_QUESTION_CHARS = 2000;
21
+ const MAX_CONTEXT_LESSONS = 8;
22
+
23
+ function resolveApiKey(opts = {}) {
24
+ return opts.apiKey || process.env.GEMINI_API_KEY || process.env.THUMBGATE_GEMINI_API_KEY || '';
25
+ }
26
+
27
+ // Retrieve the most relevant stored lessons for the question.
28
+ function retrieveContext(question, opts = {}) {
29
+ let searchLessons;
30
+ try {
31
+ ({ searchLessons } = require(path.join(__dirname, 'lesson-search')));
32
+ } catch (_) {
33
+ return [];
34
+ }
35
+ let res;
36
+ try {
37
+ res = searchLessons(String(question || ''), {
38
+ limit: MAX_CONTEXT_LESSONS,
39
+ feedbackDir: opts.feedbackDir,
40
+ });
41
+ } catch (_) {
42
+ return [];
43
+ }
44
+ const rows = (res && (res.results || res.lessons)) || [];
45
+ return rows.slice(0, MAX_CONTEXT_LESSONS).map((l) => ({
46
+ id: l.id,
47
+ signal: l.signal || l.feedback || '',
48
+ title: (l.title || '').replace(/^(?:MISTAKE|SUCCESS):\s*/i, '').slice(0, 160),
49
+ content: String(l.content || l.context || '').replace(/\s+/g, ' ').trim().slice(0, 600),
50
+ tags: l.tags || [],
51
+ })).filter((l) => l.content || l.title);
52
+ }
53
+
54
+ // Build a grounded RAG prompt. Pure function (testable).
55
+ function buildChatPrompt(question, lessons) {
56
+ const q = String(question || '').slice(0, MAX_QUESTION_CHARS).trim();
57
+ const context = (lessons || []).map((l, i) => {
58
+ const mark = /pos|up/i.test(l.signal) ? 'WORKED' : (/neg|down/i.test(l.signal) ? 'MISTAKE' : 'NOTE');
59
+ const tags = (l.tags || []).length ? ` [tags: ${l.tags.join(', ')}]` : '';
60
+ return `(${i + 1}) [${mark}] ${l.title || ''}${tags}\n ${l.content}`;
61
+ }).join('\n');
62
+
63
+ const system = [
64
+ 'You are ThumbGate\'s "chat with your data" assistant. Answer the user\'s question',
65
+ 'using ONLY the captured lessons below (this team\'s real feedback history).',
66
+ 'Be concise and specific. Cite the lesson numbers you used like [1], [3].',
67
+ 'If the lessons do not contain the answer, say so plainly — do not invent facts.',
68
+ ].join(' ');
69
+
70
+ return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n\n=== Question ===\n${q}`;
71
+ }
72
+
73
+ // Parse the Gemini generateContent response into plain text. Pure (testable).
74
+ function parseGeminiAnswer(body) {
75
+ const parts = body
76
+ && body.candidates
77
+ && body.candidates[0]
78
+ && body.candidates[0].content
79
+ && body.candidates[0].content.parts;
80
+ if (!Array.isArray(parts)) return '';
81
+ return parts.map((p) => (p && typeof p.text === 'string' ? p.text : '')).join('').trim();
82
+ }
83
+
84
+ // Answer a question grounded in this install's lessons. Returns
85
+ // { ok, answer, sources, model } or { ok:false, error, ... }.
86
+ async function answerDataQuestion(question, opts = {}) {
87
+ const q = String(question || '').trim();
88
+ if (!q) return { ok: false, error: 'empty_question', message: 'Ask a question about your data.' };
89
+ if (q.length > MAX_QUESTION_CHARS) {
90
+ return { ok: false, error: 'question_too_long', message: `Question exceeds ${MAX_QUESTION_CHARS} characters.` };
91
+ }
92
+
93
+ const apiKey = resolveApiKey(opts);
94
+ const lessons = retrieveContext(q, opts);
95
+ const sources = lessons.map((l) => ({ id: l.id, title: l.title, signal: l.signal }));
96
+
97
+ if (!apiKey) {
98
+ return {
99
+ ok: false,
100
+ error: 'no_api_key',
101
+ message: 'Chat is not configured. Set GEMINI_API_KEY (e.g. `npx thumbgate setup-vertex --write`) to enable "chat with your data".',
102
+ sources,
103
+ };
104
+ }
105
+
106
+ const model = opts.model || process.env.THUMBGATE_GEMINI_MODEL || DEFAULT_MODEL;
107
+ const prompt = buildChatPrompt(q, lessons);
108
+ const fetchImpl = opts.fetch || globalThis.fetch;
109
+
110
+ try {
111
+ const res = await fetchImpl(`${GEMINI_ENDPOINT}/${encodeURIComponent(model)}:generateContent`, {
112
+ method: 'POST',
113
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
114
+ body: JSON.stringify({
115
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
116
+ generationConfig: { temperature: 0.2, maxOutputTokens: 1024 },
117
+ }),
118
+ });
119
+ const json = await res.json().catch(() => ({}));
120
+ if (!res.ok) {
121
+ const msg = (json && json.error && json.error.message) ? String(json.error.message).split('\n')[0] : `HTTP ${res.status}`;
122
+ return { ok: false, error: 'gemini_error', status: res.status, message: msg, sources };
123
+ }
124
+ const answer = parseGeminiAnswer(json);
125
+ return { ok: true, answer: answer || '(no answer returned)', sources, model: json.modelVersion || model };
126
+ } catch (err) {
127
+ return { ok: false, error: 'network', message: err && err.message ? err.message : String(err), sources };
128
+ }
129
+ }
130
+
131
+ module.exports = {
132
+ answerDataQuestion,
133
+ buildChatPrompt,
134
+ parseGeminiAnswer,
135
+ retrieveContext,
136
+ DEFAULT_MODEL,
137
+ MAX_QUESTION_CHARS,
138
+ };
@@ -8,10 +8,33 @@ function getStatuslineMeta(options = {}) {
8
8
  const pkg = require(path.join(__dirname, '..', 'package.json'));
9
9
  const env = options.env || process.env;
10
10
  const homeDir = options.homeDir || env.HOME || env.USERPROFILE || '.';
11
+ const fs = require('fs');
12
+
13
+ // Enterprise detection based on key prefix
14
+ let apiKey = env.THUMBGATE_API_KEY || env.THUMBGATE_OPERATOR_KEY || '';
15
+
16
+ // Fallback to reading from disk if not in env
17
+ if (!apiKey) {
18
+ try {
19
+ const configPath = path.join(homeDir, '.config', 'thumbgate', 'operator.json');
20
+ if (fs.existsSync(configPath)) {
21
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
+ apiKey = config.operatorKey || config.apiKey || '';
23
+ }
24
+ } catch (_) { /* ignore disk read errors */ }
25
+ }
26
+
27
+ let activeTier = 'Free';
28
+
29
+ if (apiKey.startsWith('tg_op_') || apiKey.startsWith('tg_creator_')) {
30
+ activeTier = 'Enterprise';
31
+ } else if (isProLicensed({ homeDir }) || apiKey.startsWith('tg_pro_')) {
32
+ activeTier = 'Pro';
33
+ }
11
34
 
12
35
  return {
13
36
  version: String(pkg.version || '').trim() || 'unknown',
14
- tier: isProLicensed({ homeDir }) ? 'Pro' : 'Free',
37
+ tier: activeTier,
15
38
  };
16
39
  }
17
40
 
@@ -93,19 +93,17 @@ fi
93
93
  LINK_STATE="offline"
94
94
  UP_URL=""; DOWN_URL=""; DASHBOARD_URL=""; LESSONS_URL=""
95
95
  DASHBOARD_LABEL="Dashboard"; LESSONS_LABEL="Lessons"
96
- if [[ "$STATUSLINE_VERBOSE" = "1" ]]; then
97
- _LINKS_JSON=$(node "${SCRIPT_DIR}/statusline-links.js" 2>/dev/null)
98
- if [ -n "$_LINKS_JSON" ]; then
99
- eval "$(echo "$_LINKS_JSON" | jq -r '
100
- @sh "LINK_STATE=\(.state // "offline")",
101
- @sh "UP_URL=\(.upUrl // "")",
102
- @sh "DOWN_URL=\(.downUrl // "")",
103
- @sh "DASHBOARD_URL=\(.dashboardUrl // "")",
104
- @sh "LESSONS_URL=\(.lessonsUrl // "")",
105
- @sh "DASHBOARD_LABEL=\(.dashboardLabel // "Dashboard")",
106
- @sh "LESSONS_LABEL=\(.lessonsLabel // "Lessons")"
107
- ' 2>/dev/null)"
108
- fi
96
+ _LINKS_JSON=$(node "${SCRIPT_DIR}/statusline-links.js" 2>/dev/null)
97
+ if [ -n "$_LINKS_JSON" ]; then
98
+ eval "$(echo "$_LINKS_JSON" | jq -r '
99
+ @sh "LINK_STATE=\(.state // "offline")",
100
+ @sh "UP_URL=\(.upUrl // "")",
101
+ @sh "DOWN_URL=\(.downUrl // "")",
102
+ @sh "DASHBOARD_URL=\(.dashboardUrl // "")",
103
+ @sh "LESSONS_URL=\(.lessonsUrl // "")",
104
+ @sh "DASHBOARD_LABEL=\(.dashboardLabel // "Dashboard")",
105
+ @sh "LESSONS_LABEL=\(.lessonsLabel // "Lessons")"
106
+ ' 2>/dev/null)"
109
107
  fi
110
108
 
111
109
  # ── ThumbGate package metadata ────────────────────────────────────────
@@ -120,15 +118,13 @@ fi
120
118
 
121
119
  # ── Repo context (branch / work item / PR) ───────────────────────────
122
120
  BRANCH_NAME=""; WORK_ITEM_LABEL=""; PR_LABEL=""
123
- if [[ "$STATUSLINE_VERBOSE" = "1" ]]; then
124
- _CONTEXT_JSON=$(node "${SCRIPT_DIR}/statusline-context.js" 2>/dev/null)
125
- if [[ -n "$_CONTEXT_JSON" ]]; then
126
- eval "$(echo "$_CONTEXT_JSON" | jq -r '
127
- @sh "BRANCH_NAME=\(.branchName // "")",
128
- @sh "WORK_ITEM_LABEL=\(.workItemLabel // "")",
129
- @sh "PR_LABEL=\(.prLabel // "")"
130
- ' 2>/dev/null)"
131
- fi
121
+ _CONTEXT_JSON=$(node "${SCRIPT_DIR}/statusline-context.js" 2>/dev/null)
122
+ if [[ -n "$_CONTEXT_JSON" ]]; then
123
+ eval "$(echo "$_CONTEXT_JSON" | jq -r '
124
+ @sh "BRANCH_NAME=\(.branchName // "")",
125
+ @sh "WORK_ITEM_LABEL=\(.workItemLabel // "")",
126
+ @sh "PR_LABEL=\(.prLabel // "")"
127
+ ' 2>/dev/null)"
132
128
  fi
133
129
 
134
130
  # ── Control Tower stats ──────────────────────────────────────────
@@ -144,16 +140,14 @@ fi
144
140
 
145
141
  # ── Latest lesson (data available for extensions; not rendered in statusbar) ──
146
142
  LESSON_TEXT=""; LESSON_ID=""; LESSON_LABEL=""; LESSON_LINK=""
147
- if [[ "$STATUSLINE_VERBOSE" = "1" ]]; then
148
- _LESSON_JSON=$(node "${SCRIPT_DIR}/statusline-lesson.js" 2>/dev/null)
149
- if [[ -n "$_LESSON_JSON" ]]; then
150
- eval "$(echo "$_LESSON_JSON" | jq -r '
151
- @sh "LESSON_TEXT=\(.text // "")",
152
- @sh "LESSON_ID=\(.lessonId // "")",
153
- @sh "LESSON_LABEL=\(.label // "")",
154
- @sh "LESSON_LINK=\(.link // "")"
155
- ' 2>/dev/null)"
156
- fi
143
+ _LESSON_JSON=$(node "${SCRIPT_DIR}/statusline-lesson.js" 2>/dev/null)
144
+ if [[ -n "$_LESSON_JSON" ]]; then
145
+ eval "$(echo "$_LESSON_JSON" | jq -r '
146
+ @sh "LESSON_TEXT=\(.text // "")",
147
+ @sh "LESSON_ID=\(.lessonId // "")",
148
+ @sh "LESSON_LABEL=\(.label // "")",
149
+ @sh "LESSON_LINK=\(.link // "")"
150
+ ' 2>/dev/null)"
157
151
  fi
158
152
 
159
153
  # ── Colors ────────────────────────────────────────────────────────
@@ -206,10 +200,10 @@ LINE="${LINE:+${LINE} · }ThumbGate v${TG_VERSION} · ${TG_TIER}"
206
200
  if [[ "$UP" = "0" && "$DOWN" = "0" ]]; then
207
201
  LINE="${D}${LINE}${RST} · no feedback yet"
208
202
  [[ -n "$PR_LABEL" ]] && LINE="${LINE} · ${D}${PR_LABEL}${RST}"
209
- if [[ "$STATUSLINE_VERBOSE" = "1" ]]; then
210
- LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST} · ${M}${LESSONS_LINK}${RST}"
211
- [[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
212
- fi
203
+
204
+ LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}"
205
+ [[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
206
+
213
207
  printf '%b\n' "$LINE"
214
208
  else
215
209
  LINE="${LINE} · ${G}${BD}${UP}${RST}${UP_LINK} ${R}${BD}${DOWN}${RST}${DOWN_LINK} ${ARROW}"
@@ -219,10 +213,9 @@ else
219
213
  [[ "${AT_RISK:-0}" -gt 0 ]] && LINE="${LINE} ${R}${AT_RISK}⚠${RST}"
220
214
  [[ "${ANOMALIES:-0}" -gt 0 ]] && LINE="${LINE} ${R}${ANOMALIES}☠${RST}"
221
215
  [[ -n "$PR_LABEL" ]] && LINE="${LINE} · ${D}${PR_LABEL}${RST}"
222
- if [[ "$STATUSLINE_VERBOSE" = "1" ]]; then
223
- LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST} · ${M}${LESSONS_LINK}${RST}"
224
- [[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
225
- fi
226
-
216
+
217
+ LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}"
218
+ [[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
219
+
227
220
  printf '%b\n' "$LINE"
228
221
  fi
package/src/api/server.js CHANGED
@@ -166,6 +166,9 @@ const {
166
166
  readDashboardReviewState,
167
167
  writeDashboardReviewState,
168
168
  } = require('../../scripts/dashboard');
169
+ const {
170
+ guardDfcxWebhook,
171
+ } = require('../../adapters/gcp/dfcx-webhook-gate');
169
172
  const {
170
173
  buildDashboardRenderSpec,
171
174
  } = require('../../scripts/dashboard-render-spec');
@@ -1448,6 +1451,178 @@ async function loadLiveDashboardDataOrRespondProblem(res, parsed, feedbackDir, i
1448
1451
  }
1449
1452
  }
1450
1453
 
1454
+ function buildEnterpriseDialogflowStatus(env = process.env) {
1455
+ const vertexProject = normalizeNullableText(env.VERTEX_PROJECT_ID)
1456
+ || normalizeNullableText(env.GOOGLE_VERTEX_PROJECT);
1457
+ const vertexLocation = normalizeNullableText(env.GOOGLE_VERTEX_LOCATION)
1458
+ || normalizeNullableText(env.VERTEX_LOCATION)
1459
+ || 'us-central1';
1460
+ const dfcxFulfillmentUrl = normalizeNullableText(env.THUMBGATE_DFCX_FULFILLMENT_URL);
1461
+ const dfcxAgentId = normalizeNullableText(env.THUMBGATE_DFCX_AGENT_ID);
1462
+ const dfcxLocation = normalizeNullableText(env.THUMBGATE_DFCX_LOCATION);
1463
+
1464
+ return {
1465
+ mode: 'local-dashboard',
1466
+ vertex: {
1467
+ configured: Boolean(vertexProject),
1468
+ projectId: vertexProject,
1469
+ location: vertexLocation,
1470
+ providerMode: normalizeNullableText(env.THUMBGATE_PROVIDER_MODE) || null,
1471
+ },
1472
+ dfcx: {
1473
+ apiSurface: 'Dialogflow CX REST API: projects.locations.agents',
1474
+ liveAgentConfigured: Boolean(dfcxAgentId && dfcxLocation),
1475
+ agentId: dfcxAgentId,
1476
+ location: dfcxLocation,
1477
+ fulfillmentProxyConfigured: Boolean(dfcxFulfillmentUrl),
1478
+ fulfillmentUrlConfigured: Boolean(dfcxFulfillmentUrl),
1479
+ gcloudCxCommandSupported: false,
1480
+ verification: dfcxAgentId && dfcxLocation
1481
+ ? 'Agent metadata is present in env; verify via REST/console before production claims.'
1482
+ : 'No live DFCX agent env configured. Use REST/console/deployed webhook evidence before claiming a live agent.',
1483
+ },
1484
+ chat: {
1485
+ available: true,
1486
+ source: 'local ThumbGate dashboard data',
1487
+ guard: 'DFCX-compatible pre-action gate adapter',
1488
+ },
1489
+ };
1490
+ }
1491
+
1492
+ function normalizeEnterpriseChatPrompt(value) {
1493
+ const text = normalizeNullableText(value);
1494
+ if (!text) return null;
1495
+ return text.slice(0, 800);
1496
+ }
1497
+
1498
+ function classifyEnterpriseChatTopic(prompt) {
1499
+ const lower = String(prompt || '').toLowerCase();
1500
+ if (/gate|block|deny|prevent|guard/.test(lower)) return 'gates';
1501
+ if (/lesson|memory|feedback|thumb|mistake|negative|positive/.test(lower)) return 'feedback';
1502
+ if (/team|agent|org|enterprise|rollout/.test(lower)) return 'team';
1503
+ if (/token|cost|saving|budget|spend/.test(lower)) return 'cost';
1504
+ if (/vertex|gcp|google|dialogflow|dfcx|cloud/.test(lower)) return 'cloud';
1505
+ return 'overview';
1506
+ }
1507
+
1508
+ function containsUnsafeEnterpriseChatInput(prompt) {
1509
+ return /[;&|`$<>\\]/.test(String(prompt || ''));
1510
+ }
1511
+
1512
+ function compactNumber(value) {
1513
+ const n = Number(value || 0);
1514
+ return Number.isFinite(n) ? n : 0;
1515
+ }
1516
+
1517
+ function buildEnterpriseChatAnswer(prompt, dashboardData, status) {
1518
+ const topic = classifyEnterpriseChatTopic(prompt);
1519
+ const approval = dashboardData.approval || {};
1520
+ const gates = Array.isArray(dashboardData.gates) ? dashboardData.gates : [];
1521
+ const gateStats = dashboardData.gateStats || {};
1522
+ const team = dashboardData.team || {};
1523
+ const tokenSavings = dashboardData.tokenSavings || {};
1524
+ const lessonPipeline = dashboardData.lessonPipeline || {};
1525
+ const lines = [];
1526
+ const sources = ['local dashboard data'];
1527
+
1528
+ if (topic === 'feedback') {
1529
+ lines.push(`Feedback total: ${compactNumber(approval.total)} (${compactNumber(approval.positive)} positive, ${compactNumber(approval.negative)} negative).`);
1530
+ lines.push(`Lesson pipeline: ${compactNumber(lessonPipeline.lessons || lessonPipeline.generated || 0)} lessons visible in the current dashboard snapshot.`);
1531
+ sources.push('feedback log', 'lesson pipeline');
1532
+ } else if (topic === 'gates') {
1533
+ lines.push(`Active gates: ${gates.length || compactNumber(gateStats.totalGates)}.`);
1534
+ lines.push(`Blocked actions recorded: ${compactNumber(gateStats.blocked || gateStats.denied || gateStats.totalBlocked)}.`);
1535
+ if (gates[0]) lines.push(`Example gate: ${gates[0].name || gates[0].id || 'unnamed gate'}.`);
1536
+ sources.push('gate stats');
1537
+ } else if (topic === 'team') {
1538
+ lines.push(`Team dashboard is available in this local Enterprise view.`);
1539
+ lines.push(`Tracked agents: ${compactNumber(team.totalAgents || team.agentCount || 0)}; risky agents: ${compactNumber(team.riskyAgents || team.highRiskAgents || 0)}.`);
1540
+ sources.push('team dashboard');
1541
+ } else if (topic === 'cost') {
1542
+ lines.push(`Estimated token savings: ${tokenSavings.dollarsSavedDisplay || '$0.00'} from ${compactNumber(tokenSavings.blockedCalls)} blocked calls.`);
1543
+ lines.push('Google Cloud budget alerts are evidence for spend visibility; ThumbGate-side stop conditions must be verified separately before calling them a hard cap.');
1544
+ sources.push('token savings', 'budget posture');
1545
+ } else if (topic === 'cloud') {
1546
+ lines.push(status.vertex.configured
1547
+ ? `Vertex routing config is present for project ${status.vertex.projectId} (${status.vertex.location}).`
1548
+ : 'Vertex routing config is not present in this server environment.');
1549
+ lines.push(status.dfcx.liveAgentConfigured
1550
+ ? `DFCX env has agent ${status.dfcx.agentId} in ${status.dfcx.location}; verify it with REST/console before production claims.`
1551
+ : 'No live DFCX agent is configured in env. Do not use the old alpha gcloud CX command group; verify agents with the Dialogflow CX REST API or console.');
1552
+ sources.push('enterprise cloud status');
1553
+ } else {
1554
+ lines.push('Ask about feedback, lessons, active gates, team rollout, token savings, or Vertex/DFCX readiness.');
1555
+ lines.push(`Current local snapshot: ${compactNumber(approval.total)} feedback events and ${gates.length || compactNumber(gateStats.totalGates)} active gates.`);
1556
+ }
1557
+
1558
+ return {
1559
+ topic,
1560
+ answer: lines.join(' '),
1561
+ sources,
1562
+ };
1563
+ }
1564
+
1565
+ async function answerEnterpriseDialogflowChat({ prompt, feedbackDir, parsed }) {
1566
+ const normalizedPrompt = normalizeEnterpriseChatPrompt(prompt);
1567
+ if (!normalizedPrompt) {
1568
+ throw createHttpError(400, 'prompt is required');
1569
+ }
1570
+ const status = buildEnterpriseDialogflowStatus();
1571
+ if (containsUnsafeEnterpriseChatInput(normalizedPrompt)) {
1572
+ return {
1573
+ ok: false,
1574
+ blocked: true,
1575
+ answer: 'This prompt contains unsafe control characters and was blocked before data access.',
1576
+ status,
1577
+ dfcx: {
1578
+ blocked: true,
1579
+ evaluation: {
1580
+ allowed: false,
1581
+ gate: 'enterprise-chat-unsafe-input',
1582
+ severity: 'critical',
1583
+ },
1584
+ },
1585
+ sources: ['enterprise input guard'],
1586
+ };
1587
+ }
1588
+
1589
+ const dashboardResult = await buildLiveDashboardData(parsed, feedbackDir);
1590
+ const dashboardData = dashboardResult.data;
1591
+ const chat = buildEnterpriseChatAnswer(normalizedPrompt, dashboardData, status);
1592
+ const dfcxRequest = {
1593
+ fulfillmentInfo: { tag: 'chat-with-data' },
1594
+ sessionInfo: {
1595
+ session: 'local-dashboard/enterprise-chat',
1596
+ parameters: {
1597
+ topic: chat.topic,
1598
+ prompt_key: normalizedPrompt.toLowerCase().replace(/[^a-z0-9._ -]/g, '').slice(0, 64),
1599
+ },
1600
+ },
1601
+ languageCode: 'en',
1602
+ };
1603
+ const guarded = await guardDfcxWebhook(
1604
+ dfcxRequest,
1605
+ async () => ({
1606
+ fulfillment_response: { messages: [{ text: { text: [chat.answer] } }] },
1607
+ session_info: { parameters: { thumbgate_topic: chat.topic } },
1608
+ }),
1609
+ { blockOnRepeat: false },
1610
+ );
1611
+
1612
+ return {
1613
+ ok: !guarded.blocked,
1614
+ blocked: Boolean(guarded.blocked),
1615
+ answer: guarded.blocked ? 'ThumbGate blocked this enterprise chat turn before data access.' : chat.answer,
1616
+ status,
1617
+ dfcx: {
1618
+ blocked: Boolean(guarded.blocked),
1619
+ evaluation: guarded.evaluation,
1620
+ response: guarded.response,
1621
+ },
1622
+ sources: chat.sources,
1623
+ };
1624
+ }
1625
+
1451
1626
  function buildLossAnalyticsResponse(data, summaryOptions) {
1452
1627
  return {
1453
1628
  window: data.analytics.window || summaryOptions,
@@ -2068,6 +2243,7 @@ window.THUMBGATE_DASHBOARD_BOOTSTRAP = { enabled: ${bootstrapActive ? 'true' : '
2068
2243
  <p>This lightweight npm dashboard is bundled without marketing assets, so installs stay small while core feedback, lessons, and API routes remain available.</p>
2069
2244
  <div class="grid">
2070
2245
  <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>
2071
2247
  <a class="card" href="/lessons"><strong>Lessons</strong><span>Review remembered thumbs-up/down lessons and enforcement context.</span></a>
2072
2248
  <a class="card" href="/health"><strong>Health</strong><span>Verify the installed package version and runtime status.</span></a>
2073
2249
  </div>
@@ -6747,6 +6923,20 @@ ${hidden}
6747
6923
  return;
6748
6924
  }
6749
6925
 
6926
+ // Chat with your data — RAG over this install's captured lessons, answered
6927
+ // by Gemini grounded only in the retrieved context. Powers the dashboard
6928
+ // "Chat with your data" panel.
6929
+ if (req.method === 'POST' && pathname === '/v1/chat') {
6930
+ const body = await parseJsonBody(req);
6931
+ const { answerDataQuestion } = require('../../scripts/dashboard-chat');
6932
+ const result = await answerDataQuestion(body.question || body.q || body.message, {
6933
+ feedbackDir: requestFeedbackPaths.FEEDBACK_DIR,
6934
+ model: typeof body.model === 'string' ? body.model : undefined,
6935
+ });
6936
+ sendJson(res, result.ok ? 200 : (result.error === 'no_api_key' ? 503 : 400), result);
6937
+ return;
6938
+ }
6939
+
6750
6940
  // Server-Sent Events stream of live feedback / rule-regen / gate events.
6751
6941
  // Dashboard clients subscribe once (with the same Bearer auth already
6752
6942
  // required for /v1/feedback/stats) and receive pushed events as they
@@ -6802,6 +6992,22 @@ ${hidden}
6802
6992
  return;
6803
6993
  }
6804
6994
 
6995
+ if (req.method === 'GET' && pathname === '/v1/enterprise/dialogflow/status') {
6996
+ sendJson(res, 200, buildEnterpriseDialogflowStatus());
6997
+ return;
6998
+ }
6999
+
7000
+ if (req.method === 'POST' && pathname === '/v1/enterprise/dialogflow/chat') {
7001
+ const body = await parseJsonBody(req, 16 * 1024);
7002
+ const result = await answerEnterpriseDialogflowChat({
7003
+ prompt: body.prompt || body.message || body.query,
7004
+ feedbackDir: requestFeedbackDir,
7005
+ parsed,
7006
+ });
7007
+ sendJson(res, 200, result);
7008
+ return;
7009
+ }
7010
+
6805
7011
  if (req.method === 'GET' && pathname === '/v1/intents/catalog') {
6806
7012
  const mcpProfile = parsed.searchParams.get('mcpProfile') || undefined;
6807
7013
  const bundleId = parsed.searchParams.get('bundleId') || undefined;
@@ -7990,7 +8196,12 @@ ${hidden}
7990
8196
 
7991
8197
  // POST /v1/dashboard/review-state -- mark current dashboard state as reviewed
7992
8198
  if (req.method === 'POST' && pathname === '/v1/dashboard/review-state') {
8199
+ const body = await parseJsonBody(req);
7993
8200
  const snapshot = buildReviewSnapshot(requestFeedbackDir);
8201
+ // Override snapshot timestamp with client-provided one if available
8202
+ if (body && body.reviewedAt) {
8203
+ snapshot.reviewedAt = body.reviewedAt;
8204
+ }
7994
8205
  writeDashboardReviewState(requestFeedbackDir, snapshot);
7995
8206
  const data = generateDashboard(requestFeedbackDir, {
7996
8207
  reviewBaseline: snapshot,
@@ -8306,6 +8517,9 @@ module.exports = {
8306
8517
  resolveLocalPageBootstrap,
8307
8518
  getPublicMcpTools,
8308
8519
  getServerCardTools,
8520
+ buildEnterpriseDialogflowStatus,
8521
+ buildEnterpriseChatAnswer,
8522
+ answerEnterpriseDialogflowChat,
8309
8523
  },
8310
8524
  };
8311
8525