thumbgate 1.26.6 → 1.26.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +5 -3
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +27 -6
- package/package.json +2 -1
- package/public/dashboard.html +191 -3
- package/public/index.html +2 -2
- package/public/numbers.html +2 -2
- package/scripts/dashboard-chat.js +138 -0
- package/scripts/statusline-meta.js +14 -1
- package/scripts/statusline.sh +28 -34
- package/src/api/server.js +214 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate-marketplace",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.8",
|
|
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.
|
|
17
|
+
"version": "1.26.8",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Igor Ganapolsky",
|
|
20
20
|
"email": "ig5973700@gmail.com",
|
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
4
|
+
"version": "1.26.8",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
7
7
|
"email": "ig5973700@gmail.com",
|
package/README.md
CHANGED
|
@@ -533,18 +533,20 @@ Free and self-hosted users can invoke `search_lessons` directly through MCP, and
|
|
|
533
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.
|
|
534
534
|
|
|
535
535
|
### Zero-Friction Setup
|
|
536
|
-
To
|
|
536
|
+
To wire local ThumbGate scoring to Vertex AI, run:
|
|
537
537
|
```bash
|
|
538
538
|
npx thumbgate setup-vertex
|
|
539
539
|
```
|
|
540
540
|
* **Auto-Discovery:** Automatically detects your active authenticated `gcloud` session and active project ID.
|
|
541
541
|
* **Auto-Enablement:** Programmatically enables the Vertex AI API in your project.
|
|
542
|
-
* **Auto-Configuration:** Writes
|
|
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.
|
|
543
545
|
|
|
544
546
|
### Zero-Friction Cost Containment ($10/mo Hard Cap)
|
|
545
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:
|
|
546
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).
|
|
547
|
-
* **Bypasses
|
|
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.
|
|
548
550
|
|
|
549
551
|
---
|
|
550
552
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.26.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.26.8", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.26.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.26.8", "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.
|
|
234
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.26.8' };
|
|
235
235
|
const COMMERCE_CATEGORIES = [
|
|
236
236
|
'product_recommendation',
|
|
237
237
|
'brand_compliance',
|
package/bin/cli.js
CHANGED
|
@@ -626,10 +626,14 @@ function detectAgent(projectDir) {
|
|
|
626
626
|
return null;
|
|
627
627
|
}
|
|
628
628
|
|
|
629
|
-
async function setupVertex() {
|
|
629
|
+
async function setupVertex(options = {}) {
|
|
630
630
|
const { execSync } = require('child_process');
|
|
631
|
+
const dryRun = options.dryRun === true || options['dry-run'] === true;
|
|
631
632
|
console.log(`\nthumbgate setup-vertex v${pkgVersion()}`);
|
|
632
633
|
console.log(' Zero-friction Google Cloud & Vertex AI onboarding...');
|
|
634
|
+
if (dryRun) {
|
|
635
|
+
console.log(' Dry run: will detect gcloud account/project, but will not enable services or write .env.');
|
|
636
|
+
}
|
|
633
637
|
console.log('');
|
|
634
638
|
|
|
635
639
|
// 1. Detect gcloud CLI
|
|
@@ -666,6 +670,14 @@ async function setupVertex() {
|
|
|
666
670
|
return;
|
|
667
671
|
}
|
|
668
672
|
|
|
673
|
+
if (dryRun) {
|
|
674
|
+
console.log(` DRY-RUN would enable Vertex AI API for project: ${activeProject}`);
|
|
675
|
+
console.log(` DRY-RUN would write THUMBGATE_PROVIDER_MODE=vertex and VERTEX_PROJECT_ID=${activeProject} to .env.`);
|
|
676
|
+
console.log('');
|
|
677
|
+
console.log(' Dry run complete. Re-run without --dry-run to apply these changes.');
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
669
681
|
// 2. Auto-enable Vertex AI API
|
|
670
682
|
console.log(' ⚙️ Enabling Vertex AI API in your project (this can take a few seconds)...');
|
|
671
683
|
try {
|
|
@@ -700,10 +712,11 @@ async function setupVertex() {
|
|
|
700
712
|
// 4. Print gorgeous success activation box
|
|
701
713
|
console.log('');
|
|
702
714
|
console.log(' ╭──────────────────────────────────────────────────────────╮');
|
|
703
|
-
console.log(' │
|
|
715
|
+
console.log(' │ Vertex AI Setup Complete │');
|
|
704
716
|
console.log(' │ │');
|
|
705
|
-
console.log(' │ ThumbGate
|
|
706
|
-
console.log(' │
|
|
717
|
+
console.log(' │ ThumbGate wrote local Vertex routing config. │');
|
|
718
|
+
console.log(' │ This does not create or verify a Dialogflow CX agent. │');
|
|
719
|
+
console.log(' │ Verify DFCX with the console or Dialogflow CX REST API. │');
|
|
707
720
|
console.log(' │ │');
|
|
708
721
|
console.log(' │ Try a test run: │');
|
|
709
722
|
console.log(' │ npx thumbgate feedback-self-test │');
|
|
@@ -2407,6 +2420,11 @@ function optimize() {
|
|
|
2407
2420
|
doOptimize();
|
|
2408
2421
|
}
|
|
2409
2422
|
|
|
2423
|
+
function syncGcp() {
|
|
2424
|
+
const { syncToGcp } = require(path.join(PKG_ROOT, 'adapters', 'gcp', 'sync.js'));
|
|
2425
|
+
syncToGcp();
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2410
2428
|
function cleanup() {
|
|
2411
2429
|
console.log('Cleaning up ThumbGate processes...');
|
|
2412
2430
|
try {
|
|
@@ -2963,7 +2981,7 @@ const SUBCOMMAND_HELP = {
|
|
|
2963
2981
|
suggest: 'Usage: npx thumbgate suggest <gate-id>\n\nSuggest fixes for a specific gate based on lesson history.',
|
|
2964
2982
|
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
2983
|
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
|
|
2984
|
+
'setup-vertex': 'Usage: npx thumbgate setup-vertex [--dry-run]\n\nAuto-enable Vertex AI API on GCP and write local Vertex routing config to .env. With --dry-run, only detect the active account/project and print the planned changes. This does not create or verify a Dialogflow CX agent; use the Dialogflow CX REST API or console for live-agent evidence.',
|
|
2967
2985
|
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
2986
|
};
|
|
2969
2987
|
|
|
@@ -3120,6 +3138,9 @@ switch (COMMAND) {
|
|
|
3120
3138
|
case 'cleanup':
|
|
3121
3139
|
cleanup();
|
|
3122
3140
|
break;
|
|
3141
|
+
case 'sync-gcp':
|
|
3142
|
+
syncGcp();
|
|
3143
|
+
break;
|
|
3123
3144
|
case 'gate-check':
|
|
3124
3145
|
gateCheck().catch((err) => {
|
|
3125
3146
|
console.error(err && err.message ? err.message : err);
|
|
@@ -3148,7 +3169,7 @@ switch (COMMAND) {
|
|
|
3148
3169
|
feedbackSelfTest();
|
|
3149
3170
|
break;
|
|
3150
3171
|
case 'setup-vertex':
|
|
3151
|
-
setupVertex().catch((err) => {
|
|
3172
|
+
setupVertex(parseArgs(process.argv.slice(3))).catch((err) => {
|
|
3152
3173
|
console.error(err && err.message ? err.message : err);
|
|
3153
3174
|
process.exit(1);
|
|
3154
3175
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.8",
|
|
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": {
|
|
@@ -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",
|
package/public/dashboard.html
CHANGED
|
@@ -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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
}
|
|
@@ -1038,9 +1142,14 @@ async function markReviewed() {
|
|
|
1038
1142
|
try {
|
|
1039
1143
|
var res = await fetch('/v1/dashboard/review-state', {
|
|
1040
1144
|
method: 'POST',
|
|
1041
|
-
headers: getHeaders()
|
|
1145
|
+
headers: getHeaders(),
|
|
1146
|
+
body: JSON.stringify({ reviewedAt: new Date().toISOString() })
|
|
1042
1147
|
});
|
|
1043
|
-
if (!res.ok)
|
|
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
|
+
}
|
|
1044
1153
|
var body = await res.json();
|
|
1045
1154
|
renderReviewDelta(body.reviewDelta || {});
|
|
1046
1155
|
} catch (e) {
|
|
@@ -1078,6 +1187,7 @@ function renderDashboardData(data) {
|
|
|
1078
1187
|
renderRegulatedProof(data.regulatedProof || {});
|
|
1079
1188
|
renderTemplates(data.templateLibrary || {});
|
|
1080
1189
|
renderInsights(data);
|
|
1190
|
+
loadEnterpriseDialogflowStatus();
|
|
1081
1191
|
}
|
|
1082
1192
|
|
|
1083
1193
|
function renderGeneratedViewToolbar(spec) {
|
|
@@ -1517,6 +1627,83 @@ function renderTemplates(templateLibrary) {
|
|
|
1517
1627
|
: '<div class="empty">No check templates available</div>';
|
|
1518
1628
|
}
|
|
1519
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
|
+
|
|
1520
1707
|
document.addEventListener('click', function(event) {
|
|
1521
1708
|
var tagButton = event.target.closest('.tag[data-tag]');
|
|
1522
1709
|
if (!tagButton) return;
|
|
@@ -2101,6 +2288,7 @@ function renderGateAuditChartFromData(gateAudit) {
|
|
|
2101
2288
|
},
|
|
2102
2289
|
});
|
|
2103
2290
|
}
|
|
2291
|
+
|
|
2104
2292
|
</script>
|
|
2105
2293
|
</body>
|
|
2106
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.
|
|
23
|
+
<meta name="thumbgate-version" content="1.26.8">
|
|
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.
|
|
1597
|
+
<span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.26.8</span>
|
|
1598
1598
|
</div>
|
|
1599
1599
|
</footer>
|
|
1600
1600
|
|
package/public/numbers.html
CHANGED
|
@@ -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.
|
|
28
|
+
"softwareVersion": "1.26.8",
|
|
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.
|
|
205
|
+
<div class="freshness">Updated: 2026-05-07 · Version 1.26.8</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,9 +8,22 @@ 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');
|
|
11
12
|
|
|
12
13
|
// Enterprise detection based on key prefix
|
|
13
|
-
|
|
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
|
+
|
|
14
27
|
let activeTier = 'Free';
|
|
15
28
|
|
|
16
29
|
if (apiKey.startsWith('tg_op_') || apiKey.startsWith('tg_creator_')) {
|
package/scripts/statusline.sh
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 ────────────────────────────────────────────────────────
|
|
@@ -207,7 +201,7 @@ 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
203
|
|
|
210
|
-
LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}
|
|
204
|
+
LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}"
|
|
211
205
|
[[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
|
|
212
206
|
|
|
213
207
|
printf '%b\n' "$LINE"
|
|
@@ -220,7 +214,7 @@ else
|
|
|
220
214
|
[[ "${ANOMALIES:-0}" -gt 0 ]] && LINE="${LINE} ${R}${ANOMALIES}☠${RST}"
|
|
221
215
|
[[ -n "$PR_LABEL" ]] && LINE="${LINE} · ${D}${PR_LABEL}${RST}"
|
|
222
216
|
|
|
223
|
-
LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}
|
|
217
|
+
LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}"
|
|
224
218
|
[[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
|
|
225
219
|
|
|
226
220
|
printf '%b\n' "$LINE"
|
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
|
|