thumbgate 1.27.3 → 1.27.6
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/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude-plugin/plugin.json +2 -1
- package/.well-known/mcp/server-card.json +1 -1
- 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 +50 -4
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gates/claim-verification.json +6 -0
- package/config/post-deploy-marketing-pages.json +5 -0
- package/package.json +4 -2
- package/public/dashboard.html +5 -1
- package/public/index.html +2 -2
- package/public/learn.html +14 -0
- package/public/numbers.html +2 -2
- package/scripts/billing.js +12 -1
- package/scripts/cli-schema.js +20 -10
- package/scripts/dashboard-chat.js +51 -6
- package/scripts/dashboard.js +1 -0
- package/scripts/feedback-loop.js +87 -0
- package/scripts/gates-engine.js +36 -1
- package/scripts/hybrid-feedback-context.js +1 -0
- package/scripts/plausible-domain-config.js +11 -0
- package/scripts/plausible-server-events.js +4 -4
- package/src/api/server.js +184 -28
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dashboard
|
|
3
|
+
description: Open the local HTTP dashboard for the current project in your web browser.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Open Dashboard
|
|
7
|
+
|
|
8
|
+
Open the local HTTP dashboard for the current project in your web browser.
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
Execute the following command in the project directory to open the browser dashboard scoped to the current repository:
|
|
12
|
+
```bash
|
|
13
|
+
thumbgate-dashboard
|
|
14
|
+
```
|
|
15
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-dashboard
|
|
3
|
+
description: Open the local HTTP dashboard for the current project in your web browser.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Open Scoped ThumbGate Dashboard
|
|
7
|
+
|
|
8
|
+
Open the local HTTP dashboard for the current project in your web browser.
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
Execute the following command in the project directory to open the browser dashboard:
|
|
12
|
+
```bash
|
|
13
|
+
thumbgate-dashboard
|
|
14
|
+
```
|
|
15
|
+
|
|
@@ -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.27.
|
|
4
|
+
"version": "1.27.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
7
7
|
"email": "ig5973700@gmail.com",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"memory"
|
|
29
29
|
],
|
|
30
30
|
"skills": "./skills/",
|
|
31
|
+
"commands": "./.claude/commands/",
|
|
31
32
|
"mcpServers": {
|
|
32
33
|
"thumbgate": {
|
|
33
34
|
"command": "npx",
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.27.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.27.6", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.27.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.27.6", "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.27.
|
|
234
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.27.6' };
|
|
235
235
|
const COMMERCE_CATEGORIES = [
|
|
236
236
|
'product_recommendation',
|
|
237
237
|
'brand_compliance',
|
package/bin/cli.js
CHANGED
|
@@ -565,6 +565,20 @@ function setupCodex() {
|
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
function setupGemini() {
|
|
568
|
+
// Try to import custom commands as a Gemini plugin if the CLI is installed
|
|
569
|
+
const { execSync } = require('child_process');
|
|
570
|
+
let pluginImported = false;
|
|
571
|
+
for (const binName of ['agy', 'gemini']) {
|
|
572
|
+
try {
|
|
573
|
+
execSync(`${binName} plugin import "${PKG_ROOT}" --force`, { stdio: 'ignore' });
|
|
574
|
+
console.log(` Gemini: imported thumbgate plugin via ${binName}`);
|
|
575
|
+
pluginImported = true;
|
|
576
|
+
break;
|
|
577
|
+
} catch (err) {
|
|
578
|
+
// ignore errors if command doesn't exist or fails
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
568
582
|
const settingsPath = path.join(HOME, '.gemini', 'settings.json');
|
|
569
583
|
if (fs.existsSync(settingsPath)) {
|
|
570
584
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
@@ -585,13 +599,14 @@ function setupGemini() {
|
|
|
585
599
|
}
|
|
586
600
|
}
|
|
587
601
|
|
|
588
|
-
if (!changed) return
|
|
602
|
+
if (!changed) return pluginImported;
|
|
589
603
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
590
604
|
console.log(' Gemini: updated ~/.gemini/settings.json');
|
|
591
605
|
return true;
|
|
592
606
|
}
|
|
593
607
|
// Fallback: project-level .gemini/settings.json
|
|
594
|
-
|
|
608
|
+
const mcpChanged = mergeMcpJson(path.join(CWD, '.gemini', 'settings.json'), 'Gemini', 'project');
|
|
609
|
+
return pluginImported || mcpChanged;
|
|
595
610
|
}
|
|
596
611
|
|
|
597
612
|
function setupAmp() {
|
|
@@ -910,6 +925,32 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
910
925
|
// Always create .mcp.json (project-level MCP config used by Claude, Codex, Cursor)
|
|
911
926
|
mergeMcpJson(path.join(CWD, '.mcp.json'), 'MCP');
|
|
912
927
|
|
|
928
|
+
// Copy custom slash commands (.claude/commands/*.md) to the project's config directories
|
|
929
|
+
const pkgCommandsDir = path.join(PKG_ROOT, '.claude', 'commands');
|
|
930
|
+
if (fs.existsSync(pkgCommandsDir)) {
|
|
931
|
+
const targets = [
|
|
932
|
+
path.join(CWD, '.claude', 'commands'),
|
|
933
|
+
path.join(CWD, '.gemini', 'commands'),
|
|
934
|
+
path.join(CWD, '.antigravitycli', 'commands')
|
|
935
|
+
];
|
|
936
|
+
for (const projectCommandsDir of targets) {
|
|
937
|
+
if (!fs.existsSync(projectCommandsDir)) {
|
|
938
|
+
fs.mkdirSync(projectCommandsDir, { recursive: true });
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
const files = fs.readdirSync(pkgCommandsDir);
|
|
942
|
+
for (const file of files) {
|
|
943
|
+
if (file.endsWith('.md')) {
|
|
944
|
+
fs.copyFileSync(path.join(pkgCommandsDir, file), path.join(projectCommandsDir, file));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} catch (err) {
|
|
948
|
+
console.log(` Failed to copy custom commands to ${path.relative(CWD, projectCommandsDir)}: ${err.message}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
console.log('Scaffolded custom slash commands directories (.claude, .gemini, .antigravitycli)');
|
|
952
|
+
}
|
|
953
|
+
|
|
913
954
|
// Auto-detect and configure platform-specific locations
|
|
914
955
|
console.log('');
|
|
915
956
|
console.log('Detecting platforms...');
|
|
@@ -3323,8 +3364,13 @@ switch (COMMAND) {
|
|
|
3323
3364
|
break;
|
|
3324
3365
|
}
|
|
3325
3366
|
case 'brain': {
|
|
3326
|
-
const
|
|
3327
|
-
|
|
3367
|
+
const sub = process.argv.slice(3).find((arg) => !arg.startsWith('--'));
|
|
3368
|
+
if (sub && ['init', 'context', 'remember', 'check', 'cleanup', 'status'].includes(sub)) {
|
|
3369
|
+
brain();
|
|
3370
|
+
} else {
|
|
3371
|
+
const brainArgs = parseArgs(process.argv.slice(3));
|
|
3372
|
+
process.exitCode = cmdBrain(brainArgs);
|
|
3373
|
+
}
|
|
3328
3374
|
break;
|
|
3329
3375
|
}
|
|
3330
3376
|
case 'billing:setup':
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dashboard
|
|
3
|
+
description: Open the local HTTP dashboard for the current project in your web browser.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Open Dashboard
|
|
7
|
+
|
|
8
|
+
Open the local HTTP dashboard for the current project in your web browser.
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
Execute the following command in the project directory to open the browser dashboard scoped to the current repository:
|
|
12
|
+
```bash
|
|
13
|
+
thumbgate-dashboard
|
|
14
|
+
```
|
|
15
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-dashboard
|
|
3
|
+
description: Open the local HTTP dashboard for the current project in your web browser.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Open Scoped ThumbGate Dashboard
|
|
7
|
+
|
|
8
|
+
Open the local HTTP dashboard for the current project in your web browser.
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
Execute the following command in the project directory to open the browser dashboard:
|
|
12
|
+
```bash
|
|
13
|
+
thumbgate-dashboard
|
|
14
|
+
```
|
|
15
|
+
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"requiredActions": ["on_device_verified"],
|
|
25
25
|
"message": "You claimed on-device verification without evidence. Capture the proof first, then call track_action('on_device_verified').",
|
|
26
26
|
"createdAt": 1774972800000
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"pattern": "(github|repo|repository).*(about|metadata|description|topics?).*(updated|verified|fixed|match(?:es|ed)?)|(about|metadata|description|topics?).*(updated|verified|fixed|match(?:es|ed)?).*(github|repo|repository)",
|
|
30
|
+
"requiredActions": ["github_metadata_verified"],
|
|
31
|
+
"message": "You claimed GitHub repository metadata was updated or verified without source-of-truth evidence. Read it back with gh api/gh repo view and rendered HTML, then call track_action('github_metadata_verified').",
|
|
32
|
+
"createdAt": 1780677400000
|
|
27
33
|
}
|
|
28
34
|
]
|
|
29
35
|
}
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
"route": "/sitemap.xml",
|
|
52
52
|
"sentinel": "<urlset",
|
|
53
53
|
"description": "XML sitemap for search-engine discoverability"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"route": "/about",
|
|
57
|
+
"sentinel": "Igor Ganapolsky",
|
|
58
|
+
"description": "About page for GEO discoverability"
|
|
54
59
|
}
|
|
55
60
|
]
|
|
56
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.6",
|
|
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": {
|
|
@@ -222,6 +222,8 @@
|
|
|
222
222
|
"scripts/workspace-evolver.js",
|
|
223
223
|
"scripts/xmemory-lite.js",
|
|
224
224
|
".claude-plugin/plugin.json",
|
|
225
|
+
".claude/commands/",
|
|
226
|
+
"commands/",
|
|
225
227
|
".well-known/",
|
|
226
228
|
"LICENSE",
|
|
227
229
|
"README.md",
|
|
@@ -359,7 +361,7 @@
|
|
|
359
361
|
"social:prospect:bluesky:dry": "node scripts/social-bluesky-prospecting.js --dry-run",
|
|
360
362
|
"social:reply-publish:bluesky:dry": "node scripts/social-reply-monitor-bluesky.js --publish-approved --dry-run",
|
|
361
363
|
"test:python": "python3 -m pytest tests/*.py",
|
|
362
|
-
"test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:repeat-metric && npm run test:noop-detect && npm run test:action-receipts && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:mcp-tool-annotations && npm run test:mcp-oauth && npm run test:mcp-oauth-flow && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:lesson-semantic-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:predictive-credible-range && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:
|
|
364
|
+
"test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:repeat-metric && npm run test:noop-detect && npm run test:action-receipts && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:mcp-tool-annotations && npm run test:mcp-oauth && npm run test:mcp-oauth-flow && npm run test:plan-gate && npm run test:ai-component-inventory && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:lesson-semantic-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:predictive-credible-range && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:numbers-page && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:competitive-positioning-marketing && npm run test:medium-weekly && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:agent-reasoning-traces && npm run test:judge-reward && npm run test:llm-behavior-monitor && npm run test:prompting-os && npm run test:single-use-credential-gate && npm run test:structured-prompt-driven && npm run test:require-evidence-gate && npm run test:rule-validator && npm run test:bluesky-atproto && npm run test:social-reply-monitor-bluesky && npm run test:bluesky-delete-replies && npm run test:architect-kit-memory-bridge && npm run test:sonar-review-hotspots && npm run test:actionable-remediations && npm run test:gemini-embedding-policy && npm run test:agent-design-governance && npm run test:public-core-boundary && npm run test:hook-stop-verify-deploy && npm run test:hook-stop-anti-claim && npm run test:plausible-server-events && npm run test:activation-tracker && npm run test:unified-revenue-rollup && npm run test:conversion-rate-stats && npm run test:external-customer-audit && npm run test:telemetry-export && npm run test:stripe-checkout-diagnostic && npm run test:stripe-business-identity-probe && npm run test:revenue-observability-doctor && npm run test:public-bundle-ratchet && npm run test:stripe-payment-link-update && npm run test:ci-cd-hygiene-audit && npm run test:verify-marketing-pages-deployed && npm run test:install-email-capture && npm run test:install-shim && npm run test:hook-runtime-subcommands && npm run test:implementation-notes && npm run test:daily-block-cap && npm run test:free-to-paid-conversion-units && npm run test:metrics-real-endpoint && npm run test:cli-trial-and-help && npm run test:cost-cli && npm run test:silent-failure-cluster && npm run test:proof:truth && node --test tests/adaptive-reliability.test.js && npm run test:mcp-oauth-reviewer && npm run test:dfcx-gate && npm run test:dfcx-gate-server && npm run test:vertex-scorer && npm run test:dashboard-chat && npm run test:gitar-integration",
|
|
363
365
|
"test:hook-stop-verify-deploy": "node --test tests/hook-stop-verify-deploy.test.js",
|
|
364
366
|
"test:hook-stop-anti-claim": "node --test tests/hook-stop-anti-claim.test.js",
|
|
365
367
|
"test:plausible-server-events": "node --test tests/plausible-server-events.test.js tests/plausible-poller.test.js tests/plausible-domain-config.test.js",
|
package/public/dashboard.html
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<link rel="icon" type="image/png" href="/thumbgate-icon.png">
|
|
10
10
|
<link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
|
|
11
11
|
<meta name="robots" content="noindex">
|
|
12
|
+
<meta name="referrer" content="same-origin">
|
|
12
13
|
<!-- Privacy-friendly analytics by Plausible -->
|
|
13
14
|
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
14
15
|
<script type="application/ld+json">
|
|
@@ -872,7 +873,10 @@ async function connect(options) {
|
|
|
872
873
|
tierName = data.tier;
|
|
873
874
|
}
|
|
874
875
|
|
|
875
|
-
if (data.
|
|
876
|
+
if (data.localLlmConfigured) {
|
|
877
|
+
var modelLabel = data.localLlmModel ? ' (' + data.localLlmModel + ')' : '';
|
|
878
|
+
document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">✓ Local LLM' + modelLabel + ' ready. Chat answers are generated locally — no cloud calls.</span>';
|
|
879
|
+
} else if (data.geminiKeyStatus === 'validated' || data.geminiValidatedAt) {
|
|
876
880
|
var when = data.geminiValidatedAt ? ' (validated ' + new Date(data.geminiValidatedAt).toLocaleTimeString() + ')' : '';
|
|
877
881
|
document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">✓ Gemini API key validated' + when + '. You can now chat with your data.</span>';
|
|
878
882
|
} else if (data.perplexityConfigured) {
|
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.27.
|
|
23
|
+
<meta name="thumbgate-version" content="1.27.6">
|
|
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">
|
|
@@ -1676,7 +1676,7 @@ __GA_BOOTSTRAP__
|
|
|
1676
1676
|
<a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
|
|
1677
1677
|
<a href="/blog">Blog</a>
|
|
1678
1678
|
</div>
|
|
1679
|
-
<span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.27.
|
|
1679
|
+
<span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.27.6</span>
|
|
1680
1680
|
</div>
|
|
1681
1681
|
</footer>
|
|
1682
1682
|
|
package/public/learn.html
CHANGED
|
@@ -112,6 +112,12 @@
|
|
|
112
112
|
"url": "https://thumbgate.ai/learn/from-prototype-to-production",
|
|
113
113
|
"name": "From git init to v1.17.0 in 70 days: an honest ThumbGate build log"
|
|
114
114
|
},
|
|
115
|
+
{
|
|
116
|
+
"@type": "ListItem",
|
|
117
|
+
"position": 13,
|
|
118
|
+
"url": "https://thumbgate.ai/learn/pretix-stripe-connect-marketplaces",
|
|
119
|
+
"name": "Building a Pretix + Stripe Connect Plugin for Live-Music Venues"
|
|
120
|
+
},
|
|
115
121
|
{
|
|
116
122
|
"@type": "ListItem",
|
|
117
123
|
"position": 7,
|
|
@@ -404,6 +410,14 @@
|
|
|
404
410
|
<span class="article-tag">Indie SaaS</span>
|
|
405
411
|
<span class="article-tag">Shipping in Public</span>
|
|
406
412
|
</a>
|
|
413
|
+
|
|
414
|
+
<a href="/learn/pretix-stripe-connect-marketplaces" class="article-card">
|
|
415
|
+
<h3>Building a Pretix + Stripe Connect Plugin for Live-Music Venues</h3>
|
|
416
|
+
<p>How to design a multi-venue Stripe Connect ticketing plugin on Pretix, keeping venues as Merchant of Record without messy platform-fee tax reporting.</p>
|
|
417
|
+
<span class="article-tag">Stripe Connect</span>
|
|
418
|
+
<span class="article-tag">Pretix</span>
|
|
419
|
+
<span class="article-tag">Case Study</span>
|
|
420
|
+
</a>
|
|
407
421
|
</div>
|
|
408
422
|
|
|
409
423
|
<h2>Popular buyer questions</h2>
|
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.27.
|
|
28
|
+
"softwareVersion": "1.27.6",
|
|
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.27.
|
|
205
|
+
<div class="freshness">Updated: 2026-05-07 · Version 1.27.6</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>
|
package/scripts/billing.js
CHANGED
|
@@ -139,6 +139,10 @@ const IS_TEST = !!(
|
|
|
139
139
|
process.env.NODE_ENV === 'test'
|
|
140
140
|
);
|
|
141
141
|
|
|
142
|
+
function allowUnsignedStripeWebhooks() {
|
|
143
|
+
return IS_TEST && process.env.THUMBGATE_ALLOW_UNSIGNED_STRIPE_WEBHOOKS === '1';
|
|
144
|
+
}
|
|
145
|
+
|
|
142
146
|
function shouldMergeLegacyBillingData() {
|
|
143
147
|
return process.env._TEST_INCLUDE_LEGACY_BILLING_DATA === '1'
|
|
144
148
|
|| process.env.THUMBGATE_INCLUDE_LEGACY_BILLING_DATA === '1';
|
|
@@ -2901,7 +2905,7 @@ function disableCustomerKeys(customerId) {
|
|
|
2901
2905
|
}
|
|
2902
2906
|
|
|
2903
2907
|
function verifyWebhookSignature(rawBody, signature) {
|
|
2904
|
-
if (!CONFIG.STRIPE_WEBHOOK_SECRET) return
|
|
2908
|
+
if (!CONFIG.STRIPE_WEBHOOK_SECRET) return allowUnsignedStripeWebhooks();
|
|
2905
2909
|
if (!signature || !rawBody) return false;
|
|
2906
2910
|
|
|
2907
2911
|
// Stripe signature format: t=<timestamp>,v1=<hmac>,...
|
|
@@ -2931,6 +2935,13 @@ function verifyWebhookSignature(rawBody, signature) {
|
|
|
2931
2935
|
|
|
2932
2936
|
async function handleWebhook(rawBody, signature) {
|
|
2933
2937
|
if (LOCAL_MODE()) return { handled: false, reason: 'local_mode' };
|
|
2938
|
+
if (!CONFIG.STRIPE_WEBHOOK_SECRET && !allowUnsignedStripeWebhooks()) {
|
|
2939
|
+
return {
|
|
2940
|
+
handled: false,
|
|
2941
|
+
reason: 'invalid_signature',
|
|
2942
|
+
error: 'STRIPE_WEBHOOK_SECRET is required before Stripe webhooks can be processed.',
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2934
2945
|
let event;
|
|
2935
2946
|
try {
|
|
2936
2947
|
if (CONFIG.STRIPE_WEBHOOK_SECRET) {
|
package/scripts/cli-schema.js
CHANGED
|
@@ -94,6 +94,25 @@ const CLI_COMMANDS = [
|
|
|
94
94
|
{ name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
|
|
95
95
|
],
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: 'brain',
|
|
99
|
+
aliases: ['customer-brain', 'repo-brain'],
|
|
100
|
+
description: 'Scaffold, query, or build the governed customer/repo context brain',
|
|
101
|
+
group: 'discovery',
|
|
102
|
+
flags: [
|
|
103
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
104
|
+
{ name: 'task', type: 'string', description: 'Task description for routed context loading' },
|
|
105
|
+
{ name: 'type', type: 'string', description: 'Memory type for remember: decision | pattern | feedback | log' },
|
|
106
|
+
{ name: 'title', type: 'string', description: 'Title for a sourced memory entry' },
|
|
107
|
+
{ name: 'content', type: 'string', description: 'Body for a sourced memory entry' },
|
|
108
|
+
{ name: 'source', type: 'string', description: 'Required provenance for factual memory writes' },
|
|
109
|
+
{ name: 'tags', type: 'string', description: 'Comma-separated memory tags' },
|
|
110
|
+
{ name: 'text', type: 'string', description: 'Text/action to check against never-do rules' },
|
|
111
|
+
{ name: 'stale-days', type: 'number', description: 'Age threshold for cleanup report (default 60)' },
|
|
112
|
+
{ name: 'write', type: 'boolean', description: 'Save to .thumbgate/BRAIN.md (versioned, deterministic)' },
|
|
113
|
+
{ name: 'limit', type: 'number', description: 'Max lessons to include (default 15)' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
97
116
|
{
|
|
98
117
|
name: 'stats',
|
|
99
118
|
description: 'Feedback analytics — approval rate, Revenue-at-Risk, recent trend',
|
|
@@ -617,16 +636,7 @@ const CLI_COMMANDS = [
|
|
|
617
636
|
{ name: 'info', type: 'boolean', description: 'Show Pro feature list' },
|
|
618
637
|
],
|
|
619
638
|
},
|
|
620
|
-
|
|
621
|
-
name: 'brain',
|
|
622
|
-
description: 'Build the agent-readable context brain (lessons + rules + gates + project context)',
|
|
623
|
-
group: 'ops',
|
|
624
|
-
flags: [
|
|
625
|
-
{ name: 'write', type: 'boolean', description: 'Save to .thumbgate/BRAIN.md (versioned, deterministic)' },
|
|
626
|
-
{ name: 'limit', type: 'number', description: 'Max lessons to include (default 15)' },
|
|
627
|
-
{ name: 'json', type: 'boolean', description: 'Output the structured model as JSON' },
|
|
628
|
-
],
|
|
629
|
-
},
|
|
639
|
+
|
|
630
640
|
{
|
|
631
641
|
name: 'workflow',
|
|
632
642
|
aliases: ['swarm'],
|
|
@@ -129,6 +129,45 @@ async function retrieveVectorContext(question, opts = {}) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Live numeric snapshot from gate-stats + feedback analyzer. Always injected
|
|
133
|
+
// (~120 tokens) so the LLM can answer count/quantity questions like
|
|
134
|
+
// "how many were blocked today" without hallucinating.
|
|
135
|
+
function retrieveMetricsContext() {
|
|
136
|
+
const snapshot = {};
|
|
137
|
+
try {
|
|
138
|
+
const gs = require(path.join(__dirname, 'gate-stats'));
|
|
139
|
+
const s = gs.calculateStats();
|
|
140
|
+
snapshot.gates = {
|
|
141
|
+
total: s.totalGates,
|
|
142
|
+
blockRules: s.blockGates,
|
|
143
|
+
warnRules: s.warnGates,
|
|
144
|
+
totalBlockedEvents: s.totalBlocked,
|
|
145
|
+
totalWarnedEvents: s.totalWarned,
|
|
146
|
+
estimatedHoursSaved: s.estimatedHoursSaved,
|
|
147
|
+
topBlockedTrigger: s.topBlocked?.trigger || null,
|
|
148
|
+
topBlockedOccurrences: s.topBlocked?.occurrences || 0,
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
debugChatFallback('gate-stats unavailable', err);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const fl = require(path.join(__dirname, 'feedback-loop'));
|
|
155
|
+
const a = fl.analyzeFeedback();
|
|
156
|
+
snapshot.feedback = {
|
|
157
|
+
total: a.total,
|
|
158
|
+
positive: a.totalPositive,
|
|
159
|
+
negative: a.totalNegative,
|
|
160
|
+
approvalRate: a.approvalRate,
|
|
161
|
+
last7d: a.windows?.['7d'],
|
|
162
|
+
last30d: a.windows?.['30d'],
|
|
163
|
+
trend: a.trend,
|
|
164
|
+
};
|
|
165
|
+
} catch (err) {
|
|
166
|
+
debugChatFallback('feedback-loop analyze unavailable', err);
|
|
167
|
+
}
|
|
168
|
+
return snapshot;
|
|
169
|
+
}
|
|
170
|
+
|
|
132
171
|
// Retrieve relevant stored lessons and optional raw feedback vector matches.
|
|
133
172
|
async function retrieveContext(question, opts = {}) {
|
|
134
173
|
const lessons = retrieveLessonContext(question, opts);
|
|
@@ -137,7 +176,7 @@ async function retrieveContext(question, opts = {}) {
|
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
// Build a grounded RAG prompt. Pure function (testable).
|
|
140
|
-
function buildChatPrompt(question, lessons) {
|
|
179
|
+
function buildChatPrompt(question, lessons, metrics) {
|
|
141
180
|
const q = String(question || '').slice(0, MAX_QUESTION_CHARS).trim();
|
|
142
181
|
const context = (lessons || []).map((l, i) => {
|
|
143
182
|
const mark = /pos|up/i.test(l.signal) ? 'WORKED' : (/neg|down/i.test(l.signal) ? 'MISTAKE' : 'NOTE');
|
|
@@ -145,14 +184,19 @@ function buildChatPrompt(question, lessons) {
|
|
|
145
184
|
return `(${i + 1}) [${mark}] ${l.title || ''}${tags}\n ${l.content}`;
|
|
146
185
|
}).join('\n');
|
|
147
186
|
|
|
187
|
+
const metricsBlock = metrics && Object.keys(metrics).length
|
|
188
|
+
? `\n=== Live numeric snapshot (your data, current) ===\n${JSON.stringify(metrics, null, 2)}\n`
|
|
189
|
+
: '';
|
|
190
|
+
|
|
148
191
|
const system = [
|
|
149
192
|
'You are ThumbGate\'s "chat with your data" assistant. Answer the user\'s question',
|
|
150
|
-
'using ONLY the captured lessons below (this team\'s real
|
|
151
|
-
'
|
|
152
|
-
'
|
|
193
|
+
'using ONLY the captured lessons and the live numeric snapshot below (this team\'s real data).',
|
|
194
|
+
'For count/quantity questions ("how many X", "what\'s our rate"), use the numeric snapshot.',
|
|
195
|
+
'For pattern/why questions, cite the lesson numbers like [1], [3].',
|
|
196
|
+
'If neither source contains the answer, say so plainly — do not invent facts.',
|
|
153
197
|
].join(' ');
|
|
154
198
|
|
|
155
|
-
return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n\n=== Question ===\n${q}`;
|
|
199
|
+
return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n${metricsBlock}\n=== Question ===\n${q}`;
|
|
156
200
|
}
|
|
157
201
|
|
|
158
202
|
// Parse the Gemini generateContent response into plain text. Pure (testable).
|
|
@@ -263,7 +307,8 @@ async function answerDataQuestion(question, opts = {}) {
|
|
|
263
307
|
}
|
|
264
308
|
|
|
265
309
|
const model = resolveModel(opts.model);
|
|
266
|
-
const
|
|
310
|
+
const metrics = retrieveMetricsContext();
|
|
311
|
+
const prompt = buildChatPrompt(q, lessons, metrics);
|
|
267
312
|
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
268
313
|
const isPerplexity = apiKey && (apiKey.startsWith('pplx-') || apiKey.includes('perplexity'));
|
|
269
314
|
|
package/scripts/dashboard.js
CHANGED
package/scripts/feedback-loop.js
CHANGED
|
@@ -56,6 +56,90 @@ const {
|
|
|
56
56
|
|
|
57
57
|
const AUDIT_TRAIL_TAG = 'audit-trail';
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Anonymous fire-and-forget CLI feedback telemetry.
|
|
61
|
+
*
|
|
62
|
+
* Pings the hosted /v1/telemetry/ping endpoint exactly once per successful
|
|
63
|
+
* local feedback capture so the dashboard can measure CLI-side lesson volume.
|
|
64
|
+
*
|
|
65
|
+
* Hard contract (do NOT widen without explicit approval):
|
|
66
|
+
* - ONE event type: `feedback_captured`
|
|
67
|
+
* - Payload: { installId, signal: 'up'|'down', tier, ts } only.
|
|
68
|
+
* No context strings, tags, file paths, or content of any kind.
|
|
69
|
+
* - Opt-out: THUMBGATE_DISABLE_TELEMETRY=1 (or 'true') short-circuits
|
|
70
|
+
* immediately. Legacy THUMBGATE_NO_TELEMETRY=1 / DO_NOT_TRACK=1 are
|
|
71
|
+
* also honored for parity with cli-telemetry.js.
|
|
72
|
+
* - Fire-and-forget: NEVER await this call. Errors are swallowed.
|
|
73
|
+
* - 2-second timeout via AbortSignal.timeout.
|
|
74
|
+
*/
|
|
75
|
+
function emitAnonymousFeedbackPing(signal) {
|
|
76
|
+
try {
|
|
77
|
+
const env = process.env || {};
|
|
78
|
+
if (
|
|
79
|
+
env.THUMBGATE_DISABLE_TELEMETRY === '1' ||
|
|
80
|
+
env.THUMBGATE_DISABLE_TELEMETRY === 'true' ||
|
|
81
|
+
env.THUMBGATE_NO_TELEMETRY === '1' ||
|
|
82
|
+
env.DO_NOT_TRACK === '1'
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalizedSignal = signal === 'positive' ? 'up' : signal === 'negative' ? 'down' : null;
|
|
88
|
+
if (!normalizedSignal) return;
|
|
89
|
+
|
|
90
|
+
// Reuse the canonical installId from cli-telemetry.js (persisted at
|
|
91
|
+
// ~/.thumbgate/install-id). Falls back to a fresh UUID if that module
|
|
92
|
+
// is unavailable — better to ship an event we can dedup on the server
|
|
93
|
+
// than to drop the ping entirely.
|
|
94
|
+
let installId = null;
|
|
95
|
+
try {
|
|
96
|
+
const { getInstallId } = require('./cli-telemetry');
|
|
97
|
+
installId = getInstallId();
|
|
98
|
+
} catch (_) { /* fall through */ }
|
|
99
|
+
if (!installId) {
|
|
100
|
+
try {
|
|
101
|
+
installId = require('crypto').randomUUID();
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return; // no crypto, no install id → drop silently
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let tier = 'free';
|
|
108
|
+
try {
|
|
109
|
+
const { getStatuslineMeta } = require('./statusline-meta');
|
|
110
|
+
const meta = getStatuslineMeta({ env });
|
|
111
|
+
const rawTier = String(meta && meta.tier ? meta.tier : 'free').toLowerCase();
|
|
112
|
+
if (rawTier === 'pro' || rawTier === 'enterprise' || rawTier === 'free') {
|
|
113
|
+
tier = rawTier;
|
|
114
|
+
}
|
|
115
|
+
} catch (_) { /* default to 'free' */ }
|
|
116
|
+
|
|
117
|
+
const base = env.THUMBGATE_PUBLIC_APP_ORIGIN
|
|
118
|
+
|| env.THUMBGATE_API_URL
|
|
119
|
+
|| 'https://thumbgate-production.up.railway.app';
|
|
120
|
+
|
|
121
|
+
const body = JSON.stringify({
|
|
122
|
+
eventType: 'feedback_captured',
|
|
123
|
+
clientType: 'cli',
|
|
124
|
+
installId,
|
|
125
|
+
signal: normalizedSignal,
|
|
126
|
+
tier,
|
|
127
|
+
ts: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Fire-and-forget. No await. AbortSignal.timeout enforces the 2s cap.
|
|
131
|
+
if (typeof fetch !== 'function' || typeof AbortSignal === 'undefined' || typeof AbortSignal.timeout !== 'function') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
fetch(`${base.replace(/\/+$/, '')}/v1/telemetry/ping`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body,
|
|
138
|
+
signal: AbortSignal.timeout(2000),
|
|
139
|
+
}).catch(() => { /* fire-and-forget */ });
|
|
140
|
+
} catch (_) { /* telemetry must never disrupt CLI */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
59
143
|
function isAuditTrailEntry(entry = {}) {
|
|
60
144
|
return Array.isArray(entry.tags) && entry.tags.includes(AUDIT_TRAIL_TAG);
|
|
61
145
|
}
|
|
@@ -1113,6 +1197,7 @@ function captureFeedback(params) {
|
|
|
1113
1197
|
summary.lastUpdated = now;
|
|
1114
1198
|
saveSummary(summary);
|
|
1115
1199
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1200
|
+
emitAnonymousFeedbackPing(signal);
|
|
1116
1201
|
try { appendRejectionLedger(feedbackEvent, action.reason); } catch { /* non-critical */ }
|
|
1117
1202
|
try {
|
|
1118
1203
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1154,6 +1239,7 @@ function captureFeedback(params) {
|
|
|
1154
1239
|
...feedbackEvent,
|
|
1155
1240
|
validationIssues: prepared.issues,
|
|
1156
1241
|
});
|
|
1242
|
+
emitAnonymousFeedbackPing(signal);
|
|
1157
1243
|
try { appendRejectionLedger(feedbackEvent, `Schema validation failed: ${prepared.issues.join('; ')}`); } catch { /* non-critical */ }
|
|
1158
1244
|
try {
|
|
1159
1245
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1228,6 +1314,7 @@ function captureFeedback(params) {
|
|
|
1228
1314
|
}
|
|
1229
1315
|
|
|
1230
1316
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1317
|
+
emitAnonymousFeedbackPing(signal);
|
|
1231
1318
|
|
|
1232
1319
|
// Synthesis: merge similar lessons instead of creating duplicates
|
|
1233
1320
|
let synthesisResult = null;
|
package/scripts/gates-engine.js
CHANGED
|
@@ -1463,7 +1463,42 @@ function checkWhenClause(when, constraints) {
|
|
|
1463
1463
|
}
|
|
1464
1464
|
|
|
1465
1465
|
function matchGate(gate, toolName, toolInput = {}) {
|
|
1466
|
-
|
|
1466
|
+
let matchText = toolInput.command || toolInput.file_path || toolInput.path || '';
|
|
1467
|
+
|
|
1468
|
+
// Claw/hybrid support: enrich matchText with claw metadata (for EnterpriseClaw/OpenShell/Perplexity hybrid agents)
|
|
1469
|
+
const clawCtx = toolInput.clawContext || toolInput._claw || (toolInput.agentId ? {
|
|
1470
|
+
actionType: toolInput.actionType || 'unknown',
|
|
1471
|
+
agentId: toolInput.agentId || 'unknown',
|
|
1472
|
+
hybridRoute: toolInput.hybridRoute || 'unknown',
|
|
1473
|
+
screenInteraction: !!toolInput.screenInteraction,
|
|
1474
|
+
fileAccess: !!toolInput.fileAccess,
|
|
1475
|
+
} : null);
|
|
1476
|
+
|
|
1477
|
+
if (clawCtx) {
|
|
1478
|
+
const actionType = clawCtx.actionType || clawCtx.claw_action_type || 'unknown';
|
|
1479
|
+
const parts = [
|
|
1480
|
+
matchText,
|
|
1481
|
+
`claw_style: true`,
|
|
1482
|
+
`agent_identity: ${clawCtx.agentId || 'unknown'}`,
|
|
1483
|
+
`claw_action_type: ${actionType}`,
|
|
1484
|
+
`hybrid_route: ${clawCtx.hybridRoute || 'unknown'}`,
|
|
1485
|
+
];
|
|
1486
|
+
|
|
1487
|
+
if (clawCtx.screenInteraction || actionType.includes('screen')) {
|
|
1488
|
+
parts.push('screen_interaction');
|
|
1489
|
+
parts.push('interact screen');
|
|
1490
|
+
}
|
|
1491
|
+
if (clawCtx.fileAccess || actionType.includes('file') || actionType.includes('fs')) {
|
|
1492
|
+
parts.push('file_system_access');
|
|
1493
|
+
parts.push('local device file system access');
|
|
1494
|
+
}
|
|
1495
|
+
if (actionType === 'dynamic-tool-creation' || actionType.includes('create-tool') || actionType.includes('define-tool')) {
|
|
1496
|
+
parts.push('create tool');
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
matchText = parts.filter(Boolean).join(' | ');
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1467
1502
|
const affected = extractAffectedFiles(toolName, toolInput);
|
|
1468
1503
|
const affectedFiles = affected.files;
|
|
1469
1504
|
const repoRoot = affected.repoRoot;
|
|
@@ -730,6 +730,7 @@ function evaluatePretool(toolName, toolInput, opts) {
|
|
|
730
730
|
|
|
731
731
|
// Slow path: build live state (also used when compiled guards are stale)
|
|
732
732
|
const state = buildHybridState({
|
|
733
|
+
feedbackDir: o.feedbackDir,
|
|
733
734
|
feedbackLogPath: o.feedbackLogPath,
|
|
734
735
|
attributedFeedbackPath: o.attributedFeedbackPath,
|
|
735
736
|
});
|
|
@@ -49,6 +49,17 @@ function resolvePlausibleDataDomain({ host = '', env = process.env } = {}) {
|
|
|
49
49
|
return normalizedHost;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Prevent local/loopback traffic from mapping to the production Plausible site domain
|
|
53
|
+
const isLocal = normalizedHost === 'localhost' ||
|
|
54
|
+
normalizedHost === '127.0.0.1' ||
|
|
55
|
+
normalizedHost === '::1' ||
|
|
56
|
+
normalizedHost.endsWith('.local') ||
|
|
57
|
+
normalizedHost.startsWith('192.168.') ||
|
|
58
|
+
normalizedHost.startsWith('10.');
|
|
59
|
+
if (isLocal) {
|
|
60
|
+
return 'localhost';
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
return FALLBACK_REGISTERED_PLAUSIBLE_DOMAIN;
|
|
53
64
|
}
|
|
54
65
|
|
|
@@ -34,10 +34,10 @@ const REQUEST_TIMEOUT_MS = 2_000;
|
|
|
34
34
|
function isPlausibleDisabled() {
|
|
35
35
|
if (process.env.THUMBGATE_PLAUSIBLE_DISABLE === '1') return true;
|
|
36
36
|
if (process.env.DO_NOT_TRACK === '1') return true;
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Automatically disable Plausible events in local development unless explicitly overridden in tests
|
|
38
|
+
if (process.env.THUMBGATE_PLAUSIBLE_DISABLE !== '0' && process.env.NODE_ENV !== 'production') {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
|
package/src/api/server.js
CHANGED
|
@@ -1550,14 +1550,93 @@ function normalizeEnterpriseChatPrompt(value) {
|
|
|
1550
1550
|
|
|
1551
1551
|
function classifyEnterpriseChatTopic(prompt) {
|
|
1552
1552
|
const lower = String(prompt || '').toLowerCase();
|
|
1553
|
-
|
|
1554
|
-
|
|
1553
|
+
// Feedback-specific words run FIRST: "what mistakes were blocked today" is a
|
|
1554
|
+
// feedback question, not a gates question — must not be hijacked by /block/.
|
|
1555
|
+
if (/mistake|lesson|memory|feedback|thumb|negative|positive|what (?:went )?wrong|fail|win|success|worked|good/.test(lower)) return 'feedback';
|
|
1556
|
+
if (/gate|block|deny|prevent|guard|enforce/.test(lower)) return 'gates';
|
|
1555
1557
|
if (/team|agent|org|enterprise|rollout/.test(lower)) return 'team';
|
|
1556
1558
|
if (/token|cost|saving|budget|spend/.test(lower)) return 'cost';
|
|
1557
1559
|
if (/vertex|gcp|google|dialogflow|dfcx|cloud/.test(lower)) return 'cloud';
|
|
1558
1560
|
return 'overview';
|
|
1559
1561
|
}
|
|
1560
1562
|
|
|
1563
|
+
// Parse intent: LIST vs COUNT, and time window. Lets us answer "what mistakes
|
|
1564
|
+
// today?" with an actual filtered list instead of a canned total.
|
|
1565
|
+
function parseChatIntent(prompt) {
|
|
1566
|
+
const lower = String(prompt || '').toLowerCase();
|
|
1567
|
+
const wantsList = /\bwhat\b|\bwhich\b|\blist\b|\bshow me\b|\bexamples?\b|\btell me about\b/.test(lower);
|
|
1568
|
+
let windowMs = null;
|
|
1569
|
+
let windowLabel = 'across all time';
|
|
1570
|
+
if (/\btoday\b/.test(lower)) { windowMs = 24 * 60 * 60 * 1000; windowLabel = 'today'; }
|
|
1571
|
+
else if (/\byesterday\b/.test(lower)) { windowMs = 48 * 60 * 60 * 1000; windowLabel = 'yesterday'; }
|
|
1572
|
+
else if (/\bthis week\b|\b7 ?d(ay)?s?\b|\blast week\b/.test(lower)) { windowMs = 7 * 24 * 60 * 60 * 1000; windowLabel = 'the last 7 days'; }
|
|
1573
|
+
else if (/\bthis month\b|\b30 ?d(ay)?s?\b|\blast month\b/.test(lower)) { windowMs = 30 * 24 * 60 * 60 * 1000; windowLabel = 'the last 30 days'; }
|
|
1574
|
+
return { wantsList, windowMs, windowLabel };
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Read recent feedback entries (signal-filtered, time-filtered) directly from
|
|
1578
|
+
// the feedback log. Bounded + best-effort — never throws into the chat handler.
|
|
1579
|
+
// Treat short/placeholder context values as "no real description" so we don't
|
|
1580
|
+
// surface `"thumbs down"` × 3 as a useful list. Real feedback always has a
|
|
1581
|
+
// concrete sentence somewhere (whatWentWrong, distillation, whatToChange) —
|
|
1582
|
+
// pick the longest informative one.
|
|
1583
|
+
// Short tokens that mean "no real description" — kept as a plain Set, not a
|
|
1584
|
+
// big regex, to keep complexity low and easy to extend.
|
|
1585
|
+
const PLACEHOLDER_TOKENS = new Set([
|
|
1586
|
+
'thumbs down', 'thumbs up', 'thumb down', 'thumb up',
|
|
1587
|
+
'good', 'bad', 'ok', 'nice', 'verify', 'verifies', 'verification', 'test', 'testing',
|
|
1588
|
+
]);
|
|
1589
|
+
|
|
1590
|
+
function isPlaceholder(text) {
|
|
1591
|
+
const t = String(text || '').trim();
|
|
1592
|
+
if (!t || t.length < 20) return true;
|
|
1593
|
+
return PLACEHOLDER_TOKENS.has(t.toLowerCase().replace(/\.$/, ''));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function bestFeedbackDescription(row) {
|
|
1597
|
+
const candidates = [row.whatWentWrong, row.distillation, row.context, row.whatToChange, row.whatWorked, row.reasoning]
|
|
1598
|
+
.map((c) => String(c || '').trim());
|
|
1599
|
+
// Prefer the longest non-placeholder candidate; fall back to any non-empty.
|
|
1600
|
+
const informative = candidates.filter((c) => c && !isPlaceholder(c));
|
|
1601
|
+
const fallback = candidates.find(Boolean) || '';
|
|
1602
|
+
const best = informative.reduce((a, b) => (b.length > a.length ? b : a), '') || fallback;
|
|
1603
|
+
return best.slice(0, 220);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function readRecentFeedbackEntries(feedbackDir, signal, windowMs, limit = 5, opts = {}) {
|
|
1607
|
+
try {
|
|
1608
|
+
if (!feedbackDir) return [];
|
|
1609
|
+
const fsLocal = require('node:fs');
|
|
1610
|
+
const pathLocal = require('node:path');
|
|
1611
|
+
const logPath = pathLocal.join(feedbackDir, 'feedback-log.jsonl');
|
|
1612
|
+
if (!fsLocal.existsSync(logPath)) return [];
|
|
1613
|
+
const { readJsonl } = require('../../scripts/fs-utils');
|
|
1614
|
+
const rows = readJsonl(logPath) || [];
|
|
1615
|
+
const cutoff = windowMs ? Date.now() - windowMs : 0;
|
|
1616
|
+
const filtered = rows
|
|
1617
|
+
.filter((r) => !signal || r.signal === signal)
|
|
1618
|
+
.filter((r) => {
|
|
1619
|
+
if (!cutoff) return true;
|
|
1620
|
+
const t = r.timestamp ? Date.parse(r.timestamp) : Number.NaN;
|
|
1621
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
1622
|
+
})
|
|
1623
|
+
.reverse()
|
|
1624
|
+
.map((r) => {
|
|
1625
|
+
const out = { timestamp: r.timestamp, context: bestFeedbackDescription(r) };
|
|
1626
|
+
if (opts.includeSignal) out.signal = r.signal;
|
|
1627
|
+
return out;
|
|
1628
|
+
});
|
|
1629
|
+
// For list display, drop entries with no real description so the list is useful,
|
|
1630
|
+
// not three "thumbs down" placeholders. For count-only (high limit), keep all.
|
|
1631
|
+
const useful = opts.skipPlaceholders === false || limit > 50
|
|
1632
|
+
? filtered
|
|
1633
|
+
: filtered.filter((e) => e.context && !isPlaceholder(e.context));
|
|
1634
|
+
return useful.slice(0, limit);
|
|
1635
|
+
} catch {
|
|
1636
|
+
return [];
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1561
1640
|
function containsUnsafeEnterpriseChatInput(prompt) {
|
|
1562
1641
|
return /[;&|`$<>\\]/.test(String(prompt || ''));
|
|
1563
1642
|
}
|
|
@@ -1567,32 +1646,99 @@ function compactNumber(value) {
|
|
|
1567
1646
|
return Number.isFinite(n) ? n : 0;
|
|
1568
1647
|
}
|
|
1569
1648
|
|
|
1570
|
-
|
|
1649
|
+
// Pick a signal preference from the prompt. Returns 'negative' | 'positive' | null.
|
|
1650
|
+
function detectFeedbackSignalFromPrompt(prompt) {
|
|
1651
|
+
const lower = String(prompt || '').toLowerCase();
|
|
1652
|
+
const wantsNeg = /mistake|wrong|fail|negative|thumbs? *down|block/.test(lower);
|
|
1653
|
+
const wantsPos = /positive|thumbs? *up|worked|success|wins?\b|good/.test(lower);
|
|
1654
|
+
if (wantsNeg && !wantsPos) return 'negative';
|
|
1655
|
+
if (wantsPos && !wantsNeg) return 'positive';
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const FEEDBACK_LIST_LABELS = Object.freeze({
|
|
1660
|
+
negative: 'Recent mistakes',
|
|
1661
|
+
positive: 'Recent wins',
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
function buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeline }) {
|
|
1665
|
+
const signal = detectFeedbackSignalFromPrompt(ctx.prompt);
|
|
1666
|
+
|
|
1667
|
+
// One read of the time-windowed log, then in-memory counts + (signal-filtered,
|
|
1668
|
+
// placeholder-stripped) list. Counts include ALL entries (so "Feedback today: 5"
|
|
1669
|
+
// matches the dashboard tile); the list drops vague entries like literal "thumbs
|
|
1670
|
+
// down" / "good" so it surfaces only entries with real, actionable description.
|
|
1671
|
+
const windowed = readRecentFeedbackEntries(feedbackDir, null, intent.windowMs, 10000, { includeSignal: true });
|
|
1672
|
+
const windowPos = windowed.filter((r) => r.signal === 'positive').length;
|
|
1673
|
+
const windowNeg = windowed.filter((r) => r.signal === 'negative').length;
|
|
1674
|
+
const entries = (signal ? windowed.filter((r) => r.signal === signal) : windowed)
|
|
1675
|
+
.filter((r) => r.context && !isPlaceholder(r.context))
|
|
1676
|
+
.slice(0, 5);
|
|
1677
|
+
|
|
1678
|
+
const lines = [];
|
|
1679
|
+
lines.push(intent.windowMs
|
|
1680
|
+
? `Feedback ${intent.windowLabel}: ${windowed.length} (${windowPos} positive, ${windowNeg} negative).`
|
|
1681
|
+
: `Feedback total: ${compactNumber(approval.total)} (${compactNumber(approval.positive)} positive, ${compactNumber(approval.negative)} negative).`);
|
|
1682
|
+
|
|
1683
|
+
if (intent.wantsList) {
|
|
1684
|
+
if (entries.length) {
|
|
1685
|
+
lines.push(`${FEEDBACK_LIST_LABELS[signal] || 'Recent feedback'} (${intent.windowLabel}):`);
|
|
1686
|
+
for (const e of entries) {
|
|
1687
|
+
lines.push(` • ${(e.timestamp || '').slice(0, 10)} — ${e.context}`);
|
|
1688
|
+
}
|
|
1689
|
+
} else {
|
|
1690
|
+
lines.push(`No ${signal || 'feedback'} entries found ${intent.windowLabel}.`);
|
|
1691
|
+
}
|
|
1692
|
+
} else {
|
|
1693
|
+
lines.push(`Lesson pipeline: ${compactNumber(lessonPipeline.lessons || lessonPipeline.generated || 0)} lessons visible in the current dashboard snapshot.`);
|
|
1694
|
+
}
|
|
1695
|
+
return { lines, sources: ['feedback log', intent.wantsList ? 'feedback contexts' : 'lesson pipeline'] };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const GATE_EVENTS_REGEX = /activat|fired|trigger|block|denied|prevent|enforce|hit/;
|
|
1699
|
+
|
|
1700
|
+
function describeGate(g) {
|
|
1701
|
+
return `${g.name || g.id || 'unnamed'}${g.severity ? ' [' + g.severity + ']' : ''}`;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function buildGatesSection({ ctx, intent, gates, gateStats }) {
|
|
1705
|
+
const asksEvents = GATE_EVENTS_REGEX.test(String(ctx.prompt || '').toLowerCase());
|
|
1706
|
+
const totalActive = gates.length || compactNumber(gateStats.totalGates);
|
|
1707
|
+
const totalBlocked = compactNumber(gateStats.blocked || gateStats.denied || gateStats.totalBlocked);
|
|
1708
|
+
|
|
1709
|
+
const lines = [];
|
|
1710
|
+
if (asksEvents) {
|
|
1711
|
+
lines.push(`Blocked actions recorded (all time): ${totalBlocked}.`);
|
|
1712
|
+
if (intent.windowMs) {
|
|
1713
|
+
lines.push(`(Per-${intent.windowLabel} block-event breakdown isn't tracked in the local dashboard snapshot — only the running total. Filter the gate-events log directly for a precise window.)`);
|
|
1714
|
+
}
|
|
1715
|
+
} else {
|
|
1716
|
+
lines.push(`Active gates: ${totalActive}.`);
|
|
1717
|
+
}
|
|
1718
|
+
if (intent.wantsList && gates.length) {
|
|
1719
|
+
lines.push('Active gates:');
|
|
1720
|
+
for (const g of gates.slice(0, 8)) lines.push(` • ${describeGate(g)}`);
|
|
1721
|
+
} else if (gates[0] && !intent.wantsList) {
|
|
1722
|
+
lines.push(`Example gate: ${describeGate(gates[0])}.`);
|
|
1723
|
+
}
|
|
1724
|
+
return { lines, sources: ['gate stats'] };
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function buildEnterpriseChatSection(topic, dashboardData, status, ctx = {}) {
|
|
1571
1728
|
const approval = dashboardData.approval || {};
|
|
1572
1729
|
const gates = Array.isArray(dashboardData.gates) ? dashboardData.gates : [];
|
|
1573
1730
|
const gateStats = dashboardData.gateStats || {};
|
|
1574
1731
|
const team = dashboardData.team || {};
|
|
1575
1732
|
const tokenSavings = dashboardData.tokenSavings || {};
|
|
1576
1733
|
const lessonPipeline = dashboardData.lessonPipeline || {};
|
|
1734
|
+
const intent = ctx.intent || { wantsList: false, windowMs: null, windowLabel: 'across all time' };
|
|
1735
|
+
const feedbackDir = ctx.feedbackDir || null;
|
|
1577
1736
|
|
|
1578
1737
|
if (topic === 'feedback') {
|
|
1579
|
-
return {
|
|
1580
|
-
lines: [
|
|
1581
|
-
`Feedback total: ${compactNumber(approval.total)} (${compactNumber(approval.positive)} positive, ${compactNumber(approval.negative)} negative).`,
|
|
1582
|
-
`Lesson pipeline: ${compactNumber(lessonPipeline.lessons || lessonPipeline.generated || 0)} lessons visible in the current dashboard snapshot.`,
|
|
1583
|
-
],
|
|
1584
|
-
sources: ['feedback log', 'lesson pipeline'],
|
|
1585
|
-
};
|
|
1738
|
+
return buildFeedbackSection({ ctx, intent, feedbackDir, approval, lessonPipeline });
|
|
1586
1739
|
}
|
|
1587
1740
|
if (topic === 'gates') {
|
|
1588
|
-
return {
|
|
1589
|
-
lines: [
|
|
1590
|
-
`Active gates: ${gates.length || compactNumber(gateStats.totalGates)}.`,
|
|
1591
|
-
`Blocked actions recorded: ${compactNumber(gateStats.blocked || gateStats.denied || gateStats.totalBlocked)}.`,
|
|
1592
|
-
gates[0] ? `Example gate: ${gates[0].name || gates[0].id || 'unnamed gate'}.` : '',
|
|
1593
|
-
].filter(Boolean),
|
|
1594
|
-
sources: ['gate stats'],
|
|
1595
|
-
};
|
|
1741
|
+
return buildGatesSection({ ctx, intent, gates, gateStats });
|
|
1596
1742
|
}
|
|
1597
1743
|
if (topic === 'team') {
|
|
1598
1744
|
return {
|
|
@@ -1633,13 +1779,22 @@ function buildEnterpriseChatSection(topic, dashboardData, status) {
|
|
|
1633
1779
|
};
|
|
1634
1780
|
}
|
|
1635
1781
|
|
|
1636
|
-
function buildEnterpriseChatAnswer(prompt, dashboardData, status) {
|
|
1782
|
+
function buildEnterpriseChatAnswer(prompt, dashboardData, status, opts = {}) {
|
|
1637
1783
|
const topic = classifyEnterpriseChatTopic(prompt);
|
|
1638
|
-
const
|
|
1784
|
+
const intent = parseChatIntent(prompt);
|
|
1785
|
+
const section = buildEnterpriseChatSection(topic, dashboardData, status, {
|
|
1786
|
+
intent,
|
|
1787
|
+
feedbackDir: opts.feedbackDir,
|
|
1788
|
+
prompt,
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// List-style answers want newlines; single-line answers join with space.
|
|
1792
|
+
const hasList = section.lines.some((l) => /^\s*•/.test(l));
|
|
1793
|
+
const answer = hasList ? section.lines.join('\n') : section.lines.join(' ');
|
|
1639
1794
|
|
|
1640
1795
|
return {
|
|
1641
1796
|
topic,
|
|
1642
|
-
answer
|
|
1797
|
+
answer,
|
|
1643
1798
|
sources: ['local dashboard data', ...section.sources],
|
|
1644
1799
|
};
|
|
1645
1800
|
}
|
|
@@ -1655,6 +1810,7 @@ async function trySendLocalDashboardChat(res, parsed, feedbackDir, prompt, suffi
|
|
|
1655
1810
|
prompt,
|
|
1656
1811
|
dashboardResult.data,
|
|
1657
1812
|
buildEnterpriseDataChatStatus(),
|
|
1813
|
+
{ feedbackDir },
|
|
1658
1814
|
);
|
|
1659
1815
|
sendJson(res, 200, {
|
|
1660
1816
|
ok: true,
|
|
@@ -1666,7 +1822,7 @@ async function trySendLocalDashboardChat(res, parsed, feedbackDir, prompt, suffi
|
|
|
1666
1822
|
grounded: true,
|
|
1667
1823
|
});
|
|
1668
1824
|
return true;
|
|
1669
|
-
} catch
|
|
1825
|
+
} catch {
|
|
1670
1826
|
return false;
|
|
1671
1827
|
}
|
|
1672
1828
|
}
|
|
@@ -1702,7 +1858,7 @@ async function answerEnterpriseDataChat({ prompt, feedbackDir, parsed }) {
|
|
|
1702
1858
|
// The dashboard's real local/open-source chatbot turn goes through /v1/chat,
|
|
1703
1859
|
// which uses lesson retrieval + optional LanceDB vector search + the user's
|
|
1704
1860
|
// configured local or BYO model.
|
|
1705
|
-
const chat = buildEnterpriseChatAnswer(normalizedPrompt, dashboardData, status);
|
|
1861
|
+
const chat = buildEnterpriseChatAnswer(normalizedPrompt, dashboardData, status, { feedbackDir });
|
|
1706
1862
|
const dfcxRequest = {
|
|
1707
1863
|
fulfillmentInfo: { tag: 'chat-with-data' },
|
|
1708
1864
|
sessionInfo: {
|
|
@@ -2142,7 +2298,7 @@ function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
|
|
|
2142
2298
|
evidence: [err?.message ? err.message : 'unknown_error'],
|
|
2143
2299
|
},
|
|
2144
2300
|
});
|
|
2145
|
-
} catch
|
|
2301
|
+
} catch {}
|
|
2146
2302
|
return false;
|
|
2147
2303
|
}
|
|
2148
2304
|
}
|
|
@@ -3354,7 +3510,7 @@ function renderCheckoutSuccessPage(runtimeConfig) {
|
|
|
3354
3510
|
if (window.sessionStorage) {
|
|
3355
3511
|
window.sessionStorage.setItem(marker, '1');
|
|
3356
3512
|
}
|
|
3357
|
-
} catch
|
|
3513
|
+
} catch {
|
|
3358
3514
|
sendTelemetry(eventType, extra);
|
|
3359
3515
|
}
|
|
3360
3516
|
}
|
|
@@ -6013,7 +6169,7 @@ ${hidden}
|
|
|
6013
6169
|
evidence: [err?.message ? err.message : 'unknown_error'],
|
|
6014
6170
|
},
|
|
6015
6171
|
});
|
|
6016
|
-
} catch
|
|
6172
|
+
} catch {
|
|
6017
6173
|
// Telemetry is best-effort and must never fail the caller.
|
|
6018
6174
|
}
|
|
6019
6175
|
}
|
|
@@ -6380,7 +6536,7 @@ ${hidden}
|
|
|
6380
6536
|
context: 'failed to persist install-email capture to ledger',
|
|
6381
6537
|
metadata: { error: err?.message || 'unknown' },
|
|
6382
6538
|
});
|
|
6383
|
-
} catch
|
|
6539
|
+
} catch {}
|
|
6384
6540
|
}
|
|
6385
6541
|
|
|
6386
6542
|
// Privacy-clean telemetry ping for funnel attribution (no email).
|
|
@@ -7857,7 +8013,7 @@ ${hidden}
|
|
|
7857
8013
|
feedbackDir: getSafeDataDir(),
|
|
7858
8014
|
limit: 10,
|
|
7859
8015
|
});
|
|
7860
|
-
} catch
|
|
8016
|
+
} catch { /* best-effort — conversation window is optional */ }
|
|
7861
8017
|
}
|
|
7862
8018
|
const result = captureFeedback({
|
|
7863
8019
|
signal: body.signal,
|