thumbgate 1.27.4 → 1.27.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +2 -1
  9. package/.well-known/llms.txt +6 -2
  10. package/.well-known/mcp/server-card.json +1 -1
  11. package/README.md +49 -5
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/letta/README.md +41 -0
  14. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  15. package/adapters/mcp/server-stdio.js +16 -1
  16. package/adapters/opencode/opencode.json +1 -1
  17. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  18. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  19. package/bench/observability-eval-suite.json +26 -0
  20. package/bin/cli.js +230 -6
  21. package/bin/postinstall.js +1 -1
  22. package/commands/dashboard.md +15 -0
  23. package/commands/thumbgate-dashboard.md +15 -0
  24. package/config/gate-templates.json +84 -0
  25. package/config/gates/claim-verification.json +12 -0
  26. package/config/gates/default.json +20 -0
  27. package/config/github-about.json +1 -1
  28. package/config/model-candidates.json +50 -0
  29. package/config/post-deploy-marketing-pages.json +5 -0
  30. package/package.json +67 -25
  31. package/public/agent-manager.html +41 -1
  32. package/public/agents-cost-savings.html +1 -1
  33. package/public/ai-malpractice-prevention.html +2 -1
  34. package/public/assets/brand/github-social-preview.png +0 -0
  35. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  36. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  37. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  38. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  39. package/public/assets/brand/thumbgate-mark-inline.svg +15 -0
  40. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  41. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  42. package/public/assets/brand/thumbgate-mark.svg +15 -0
  43. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  44. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  45. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  46. package/public/assets/legal-intake-control-flow.svg +66 -0
  47. package/public/blog.html +1 -1
  48. package/public/brand/thumbgate-mark.svg +15 -0
  49. package/public/brand/thumbgate-og.svg +16 -0
  50. package/public/codex-enterprise.html +1 -1
  51. package/public/codex-plugin.html +1 -1
  52. package/public/compare.html +23 -3
  53. package/public/dashboard.html +316 -30
  54. package/public/federal.html +1 -1
  55. package/public/guide.html +5 -4
  56. package/public/index.html +167 -49
  57. package/public/js/buyer-intent.js +672 -0
  58. package/public/learn.html +88 -7
  59. package/public/lessons.html +2 -1
  60. package/public/numbers.html +3 -3
  61. package/public/pricing.html +63 -15
  62. package/public/pro.html +7 -7
  63. package/scripts/activation-quickstart.js +187 -0
  64. package/scripts/agent-memory-lifecycle.js +211 -0
  65. package/scripts/async-eval-observability.js +236 -0
  66. package/scripts/auto-promote-gates.js +75 -4
  67. package/scripts/billing.js +12 -1
  68. package/scripts/build-metadata.js +24 -3
  69. package/scripts/cli-schema.js +42 -10
  70. package/scripts/dashboard-chat.js +53 -7
  71. package/scripts/dashboard.js +12 -17
  72. package/scripts/export-databricks-bundle.js +5 -1
  73. package/scripts/export-dpo-pairs.js +7 -2
  74. package/scripts/feedback-aggregate.js +281 -0
  75. package/scripts/feedback-loop.js +121 -0
  76. package/scripts/filesystem-search.js +35 -10
  77. package/scripts/gates-engine.js +234 -7
  78. package/scripts/gemini-embedding-policy.js +2 -1
  79. package/scripts/hook-stop-anti-claim.js +227 -0
  80. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  81. package/scripts/hybrid-feedback-context.js +1 -0
  82. package/scripts/lesson-inference.js +8 -3
  83. package/scripts/lesson-search.js +17 -1
  84. package/scripts/operational-integrity.js +39 -5
  85. package/scripts/plausible-domain-config.js +15 -2
  86. package/scripts/plausible-server-events.js +4 -4
  87. package/scripts/rate-limiter.js +12 -6
  88. package/scripts/secret-redaction.js +166 -0
  89. package/scripts/security-scanner.js +100 -0
  90. package/scripts/self-distill-agent.js +3 -1
  91. package/scripts/self-harness-optimizer.js +141 -0
  92. package/scripts/seo-gsd.js +635 -0
  93. package/scripts/statusline-cache-path.js +17 -2
  94. package/scripts/statusline-cache-read.js +57 -0
  95. package/scripts/statusline-local-stats.js +9 -1
  96. package/scripts/statusline-meta.js +5 -2
  97. package/scripts/statusline.sh +13 -1
  98. package/scripts/sync-telemetry-from-prod.js +374 -0
  99. package/scripts/telemetry-analytics.js +9 -0
  100. package/scripts/thumbgate-search.js +85 -19
  101. package/scripts/tool-contract-validator.js +76 -0
  102. package/scripts/vector-store.js +44 -0
  103. package/scripts/workspace-evolver.js +62 -2
  104. package/src/api/server.js +862 -146
@@ -9,8 +9,9 @@
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
- <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
14
+ <script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
14
15
  <script type="application/ld+json">
15
16
  {
16
17
  "@context": "https://schema.org",
@@ -102,7 +103,7 @@
102
103
  .result-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
103
104
  .tag { font-size: 11px; background: var(--cyan-dim); color: var(--cyan); padding: 2px 8px; border-radius: 4px; cursor: pointer; transition: background 0.15s, transform 0.1s; border: none; font-family: inherit; }
104
105
  .tag:hover { background: rgba(34,211,238,0.25); transform: translateY(-1px); }
105
- mark { background: rgba(34,211,238,0.25); color: var(--cyan); border-radius: 2px; padding: 0 2px; }
106
+ mark { background: #fbbf24; color: #000000; font-weight: bold; border-radius: 2px; padding: 0 3px; }
106
107
 
107
108
  /* GATES */
108
109
  .gates-section { margin-bottom: 32px; }
@@ -178,6 +179,74 @@
178
179
  .demo-banner a { color: var(--cyan); text-decoration: none; font-weight: 600; }
179
180
  .demo-banner a:hover { text-decoration: underline; }
180
181
 
182
+ /* Tooltips styling */
183
+ .tooltip-trigger {
184
+ display: inline-flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ width: 14px;
188
+ height: 14px;
189
+ border-radius: 50%;
190
+ background: var(--border);
191
+ color: var(--text-muted);
192
+ font-size: 9px;
193
+ font-weight: bold;
194
+ cursor: help;
195
+ margin-left: 6px;
196
+ position: relative;
197
+ vertical-align: middle;
198
+ user-select: none;
199
+ }
200
+ .tooltip-trigger:hover {
201
+ background: var(--cyan);
202
+ color: var(--bg);
203
+ }
204
+ .tooltip-trigger .tooltip-content {
205
+ visibility: hidden;
206
+ opacity: 0;
207
+ width: 220px;
208
+ background-color: #161618;
209
+ color: #e8e8ec;
210
+ text-align: left;
211
+ border: 1px solid var(--border);
212
+ border-radius: 6px;
213
+ padding: 8px 12px;
214
+ position: absolute;
215
+ z-index: 1000;
216
+ bottom: 130%;
217
+ left: 50%;
218
+ transform: translateX(-50%);
219
+ transition: opacity 0.15s, visibility 0.15s;
220
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
221
+ font-weight: normal;
222
+ font-size: 11px;
223
+ line-height: 1.4;
224
+ white-space: normal;
225
+ pointer-events: none;
226
+ font-family: var(--font);
227
+ text-transform: none;
228
+ letter-spacing: normal;
229
+ }
230
+ .tooltip-trigger .tooltip-content::after {
231
+ content: "";
232
+ position: absolute;
233
+ top: 100%;
234
+ left: 50%;
235
+ transform: translateX(-50%);
236
+ border-width: 5px;
237
+ border-style: solid;
238
+ border-color: #161618 transparent transparent transparent;
239
+ }
240
+ .tooltip-trigger:hover .tooltip-content {
241
+ visibility: visible;
242
+ opacity: 1;
243
+ }
244
+ #disconnectBtn:hover {
245
+ background: rgba(248,113,113,0.1) !important;
246
+ border-color: #f87171 !important;
247
+ color: #f87171 !important;
248
+ }
249
+
181
250
  .empty { text-align: center; padding: 48px; color: var(--text-muted); font-size: 15px; }
182
251
  .loading { text-align: center; padding: 24px; color: var(--text-muted); }
183
252
  h2 { font-size: 20px; font-weight: 700; margin-bottom: 16px; letter-spacing: -0.02em; }
@@ -197,6 +266,19 @@
197
266
  .enterprise-source-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
198
267
  .enterprise-source { font-size: 11px; border: 1px solid var(--border); border-radius: 999px; padding: 4px 9px; color: var(--text-muted); background: var(--bg-card); }
199
268
 
269
+ .quick-nav-link {
270
+ cursor: pointer;
271
+ transition: opacity 0.15s ease, color 0.15s ease;
272
+ }
273
+ .quick-nav-link:hover {
274
+ color: var(--cyan);
275
+ opacity: 0.95;
276
+ }
277
+ .quick-nav-link:hover strong {
278
+ color: var(--cyan) !important;
279
+ text-decoration: underline;
280
+ }
281
+
200
282
  @media (max-width: 700px) {
201
283
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
202
284
  .search-filters { flex-wrap: wrap; }
@@ -221,13 +303,14 @@
221
303
  <div class="auth-bar">
222
304
  <div class="container">
223
305
  <div style="margin-bottom:10px;">
224
- <span style="font-size:14px;font-weight:600;">Connect your API key</span>
306
+ <span id="authTitle" style="font-size:14px;font-weight:600;">Connect your API key</span>
225
307
  <span id="authHelp" style="font-size:13px;color:var(--text-muted);margin-left:8px;">Pro auto-connects locally when you launch with <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">npx thumbgate pro</code>. Otherwise paste your <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">THUMBGATE_API_KEY</code> manually.</span>
226
308
  </div>
227
309
  <div style="display:flex;gap:12px;align-items:center;">
228
310
  <input type="password" class="auth-input" id="apiKey" placeholder="Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)..." />
229
311
  <button class="btn" id="connectBtn" onclick="connect()">Connect</button>
230
312
  <button class="btn-outline" id="demoBtn" onclick="loadDemo()">Try Demo</button>
313
+ <button class="btn-outline" id="disconnectBtn" onclick="disconnectKey()" style="display:none; border-color:#f87171; color:#f87171;">Disconnect Key</button>
231
314
  <span class="auth-status" id="authStatus"></span>
232
315
  </div>
233
316
  </div>
@@ -240,10 +323,10 @@
240
323
  <p style="font-size:12px;color:var(--text-muted);margin-bottom:8px;">Updated: <time datetime="2026-04-20">2026-04-20</time> · by <a href="https://github.com/IgorGanapolsky" style="color:inherit;">Igor Ganapolsky</a></p>
241
324
  <p style="font-size:14px;color:var(--text-muted);line-height:1.6;max-width:700px;">What's happening right now? Search memories, inspect active checks, manage your team, and export training data. <span style="color:var(--cyan);font-weight:600;">This is your control plane for AI agent behavior.</span></p>
242
325
  <div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
243
- <span>🔍 <strong style="color:var(--text);">Search</strong> — find any memory</span>
244
- <span>🛡️ <strong style="color:var(--text);">Gates</strong> — what's blocking</span>
245
- <span>👥 <strong style="color:var(--text);">Team</strong> — org metrics</span>
246
- <span>📤 <strong style="color:var(--text);">Export</strong> — DPO training data</span>
326
+ <span class="quick-nav-link" onclick="switchTab('search')">🔍 <strong style="color:var(--text);">Search</strong> — find any memory</span>
327
+ <span class="quick-nav-link" onclick="switchTab('gates')">🛡️ <strong style="color:var(--text);">Gates</strong> — what's blocking</span>
328
+ <span class="quick-nav-link" onclick="switchTab('team')">👥 <strong style="color:var(--text);">Team</strong> — org metrics</span>
329
+ <span class="quick-nav-link" onclick="switchTab('export')">📤 <strong style="color:var(--text);">Export</strong> — DPO training data</span>
247
330
  </div>
248
331
  </div>
249
332
 
@@ -254,10 +337,10 @@
254
337
 
255
338
  <!-- STATS -->
256
339
  <div class="stats-grid" id="statsGrid">
257
- <a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons?signal=all" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback → Lessons page"><div class="stat-label">Total Feedback</div><div class="stat-value cyan" id="statTotal">—</div></a>
258
- <a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=up" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback → Lessons page"><div class="stat-label">👍 Positive</div><div class="stat-value green" id="statPositive">—</div></a>
259
- <a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=down" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback → Lessons page"><div class="stat-label">👎 Negative</div><div class="stat-value red" id="statNegative">—</div></a>
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>
340
+ <a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons?signal=all" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback → Lessons page"><div class="stat-label">Total Feedback <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Total thumbs-up and thumbs-down signals captured from your AI agent interactions.</span></span></div><div class="stat-value cyan" id="statTotal">—</div></a>
341
+ <a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=up" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback → Lessons page"><div class="stat-label">👍 Positive <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Successful outcomes where the agent's work met expectations (thumbs-up).</span></span></div><div class="stat-value green" id="statPositive">—</div></a>
342
+ <a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=down" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback → Lessons page"><div class="stat-label">👎 Negative <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Unsuccessful runs or errors that we need to learn from to prevent repeat failures (thumbs-down).</span></span></div><div class="stat-value red" id="statNegative">—</div></a>
343
+ <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 <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Pre-action checks actively monitoring and blocking known-bad tool calls before they run.</span></span></div><div class="stat-value cyan" id="statGates">—</div></a>
261
344
  </div>
262
345
 
263
346
  <div class="panel" id="chatPanel" style="margin-bottom:20px;">
@@ -280,7 +363,7 @@
280
363
  <div class="panel" id="reviewDeltaPanel" style="margin-bottom:20px;">
281
364
  <div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;">
282
365
  <div>
283
- <h3 style="margin:0 0 8px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted);">🆕 Since Last Review</h3>
366
+ <h3 style="margin:0 0 8px 0;font-size:14px;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-muted);display:inline-flex;align-items:center;">🆕 Since Last Review <span class="tooltip-trigger" onclick="event.stopPropagation(); event.preventDefault();">?<span class="tooltip-content">Shows new feedback, negative signals, lessons, and blocks registered since your last baseline checkpoint.</span></span></h3>
284
367
  <p class="template-summary" id="reviewDeltaHeadline" style="margin-bottom:0;">No review checkpoint yet. Mark the current dashboard as reviewed to start seeing only new changes.</p>
285
368
  </div>
286
369
  <button class="btn-outline" id="reviewCheckpointBtn" onclick="markReviewed()">Mark Current Dashboard Reviewed</button>
@@ -319,6 +402,12 @@
319
402
 
320
403
  <!-- SEARCH TAB -->
321
404
  <div class="tab-content active" id="tab-search">
405
+ <div class="panel" style="margin-bottom:24px;">
406
+ <h3 style="margin-top:0;">📈 Feedback Trend (Last 30 Days)</h3>
407
+ <div style="position:relative;height:180px;margin-top:12px;">
408
+ <canvas id="feedbackTrendChart"></canvas>
409
+ </div>
410
+ </div>
322
411
  <div class="search-section">
323
412
  <div class="search-bar">
324
413
  <input type="text" class="search-input" id="searchQuery" placeholder="Search memories... (try: git, test, deploy, secrets, database)" onkeydown="if(event.key==='Enter')search()" />
@@ -340,6 +429,74 @@
340
429
  <div class="tab-content" id="tab-gates">
341
430
  <div class="gates-section">
342
431
  <h2>Active Pre-Action Checks</h2>
432
+
433
+ <!-- Interactive Gating Workflow Diagram -->
434
+ <details class="flow-details" style="margin-bottom: 24px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); padding: 16px;" open>
435
+ <summary style="font-weight: 600; cursor: pointer; color: var(--cyan); display: flex; align-items: center; gap: 8px; user-select: none;">
436
+ <span>🛡️ How Pre-Action Gating Works (Interactive Flow Diagram)</span>
437
+ </summary>
438
+ <div style="margin-top:20px; text-align:center;">
439
+ <svg viewBox="0 0 800 220" width="100%" height="auto" style="max-width: 800px; font-family: var(--font); font-size: 11px;">
440
+ <style>
441
+ .flow-node { fill: #1c1c1f; stroke: var(--border); stroke-width: 1.5; rx: 8px; transition: stroke 0.2s, fill 0.2s; }
442
+ .flow-node:hover { fill: rgba(34,211,238,0.06); stroke: var(--cyan); cursor: help; }
443
+ .flow-text { fill: var(--text); font-weight: 600; pointer-events: none; }
444
+ .flow-subtext { fill: var(--text-muted); font-size: 10px; pointer-events: none; }
445
+ .flow-arrow { fill: none; stroke: var(--border); stroke-width: 1.5; marker-end: url(#arrow-head); }
446
+ </style>
447
+
448
+ <defs>
449
+ <marker id="arrow-head" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
450
+ <path d="M 0 1 L 10 5 L 0 9 z" fill="#8b8b96" />
451
+ </marker>
452
+ </defs>
453
+
454
+ <!-- Step 1: Tool Call -->
455
+ <g>
456
+ <rect x="10" y="80" width="130" height="60" class="flow-node" />
457
+ <text x="75" y="105" text-anchor="middle" class="flow-text">1. Tool Call</text>
458
+ <text x="75" y="125" text-anchor="middle" class="flow-subtext">Agent requests action</text>
459
+ </g>
460
+
461
+ <path d="M 140 110 L 190 110" class="flow-arrow" />
462
+
463
+ <!-- Step 2: Hook Intercept -->
464
+ <g>
465
+ <rect x="200" y="80" width="140" height="60" class="flow-node" />
466
+ <text x="270" y="105" text-anchor="middle" class="flow-text">2. PreToolUse Hook</text>
467
+ <text x="270" y="125" text-anchor="middle" class="flow-subtext">Intercepts tool execution</text>
468
+ </g>
469
+
470
+ <path d="M 340 110 L 390 110" class="flow-arrow" />
471
+
472
+ <!-- Step 3: Policy / DB Lookup -->
473
+ <g>
474
+ <rect x="400" y="80" width="150" height="60" class="flow-node" />
475
+ <text x="475" y="105" text-anchor="middle" class="flow-text">3. Gate Evaluation</text>
476
+ <text x="475" y="125" text-anchor="middle" class="flow-subtext">Validates matching rules</text>
477
+ </g>
478
+
479
+ <!-- Branch Arrows -->
480
+ <path d="M 550 100 L 640 55" class="flow-arrow" />
481
+ <path d="M 550 120 L 640 165" class="flow-arrow" />
482
+
483
+ <!-- Step 4a: Block -->
484
+ <g>
485
+ <rect x="650" y="20" width="130" height="60" class="flow-node" style="stroke:#ef4444;" />
486
+ <text x="715" y="45" text-anchor="middle" class="flow-text" style="fill:#ef4444;">❌ BLOCK</text>
487
+ <text x="715" y="65" text-anchor="middle" class="flow-subtext">Matches rule -> aborted</text>
488
+ </g>
489
+
490
+ <!-- Step 4b: Allow -->
491
+ <g>
492
+ <rect x="650" y="140" width="130" height="60" class="flow-node" style="stroke:#22c55e;" />
493
+ <text x="715" y="165" text-anchor="middle" class="flow-text" style="fill:#22c55e;">✅ ALLOW</text>
494
+ <text x="715" y="185" text-anchor="middle" class="flow-subtext">Runs safely & logs</text>
495
+ </g>
496
+ </svg>
497
+ </div>
498
+ </details>
499
+
343
500
  <div id="gatesList"><div class="loading">Loading gates...</div></div>
344
501
  </div>
345
502
  </div>
@@ -536,13 +693,7 @@
536
693
  </div>
537
694
 
538
695
  <!-- Charts -->
539
- <div class="team-columns" style="margin-bottom:24px;">
540
- <div class="panel">
541
- <h3>Feedback Trend (30 days)</h3>
542
- <div style="position:relative;height:260px;margin-top:12px;">
543
- <canvas id="feedbackTrendChart"></canvas>
544
- </div>
545
- </div>
696
+ <div style="margin-bottom:24px;">
546
697
  <div class="panel">
547
698
  <h3>Lessons Generated (30 days)</h3>
548
699
  <div style="position:relative;height:260px;margin-top:12px;">
@@ -848,6 +999,37 @@ function hasBootstrapKey() {
848
999
  return LOCAL_PRO_BOOTSTRAP && Boolean(BOOTSTRAP_API_KEY);
849
1000
  }
850
1001
 
1002
+ async function checkLocalLlmReachable(url) {
1003
+ try {
1004
+ var controller = new AbortController();
1005
+ var timeout = setTimeout(function() { controller.abort(); }, 400);
1006
+ // Use no-cors mode to bypass CORS block (only care if network request succeeds/fails)
1007
+ await fetch(url, { method: 'GET', signal: controller.signal, mode: 'no-cors' });
1008
+ clearTimeout(timeout);
1009
+ return true;
1010
+ } catch (e) {
1011
+ return false;
1012
+ }
1013
+ }
1014
+
1015
+ function disconnectKey() {
1016
+ API_KEY = '';
1017
+ isDemo = false;
1018
+ var input = document.getElementById('apiKey');
1019
+ input.value = '';
1020
+ input.disabled = false;
1021
+ input.placeholder = "Paste your API key here (THUMBGATE_API_KEY or THUMBGATE_PRO_KEY)...";
1022
+ var btn = document.getElementById('connectBtn');
1023
+ btn.disabled = false;
1024
+ document.getElementById('demoBtn').style.display = 'inline-block';
1025
+ document.getElementById('disconnectBtn').style.display = 'none';
1026
+ document.getElementById('authStatus').className = 'auth-status';
1027
+ document.getElementById('authStatus').textContent = 'Disconnected';
1028
+ document.getElementById('authTitle').textContent = 'Connect your API key';
1029
+ document.getElementById('authHelp').innerHTML = 'Pro auto-connects locally when you launch with <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">npx thumbgate pro</code>. Otherwise paste your <code style="font-family:var(--mono);font-size:12px;background:var(--cyan-dim);color:var(--cyan);padding:2px 6px;border-radius:4px;">THUMBGATE_API_KEY</code> manually.';
1030
+ document.getElementById('dashboardContent').style.display = 'none';
1031
+ }
1032
+
851
1033
  async function connect(options) {
852
1034
  var opts = options || {};
853
1035
  var input = document.getElementById('apiKey');
@@ -872,7 +1054,18 @@ async function connect(options) {
872
1054
  tierName = data.tier;
873
1055
  }
874
1056
 
875
- if (data.geminiKeyStatus === 'validated' || data.geminiValidatedAt) {
1057
+ if (data.localLlmConfigured) {
1058
+ var modelLabel = data.localLlmModel ? ' (' + data.localLlmModel + ')' : '';
1059
+ var llmUrl = data.localLlmEndpoint || 'http://localhost:11434/v1';
1060
+ document.getElementById('chatHint').innerHTML = '<span style="color:var(--text-muted)">Checking local LLM reachability...</span>';
1061
+ checkLocalLlmReachable(llmUrl.replace(/\/v1$/, '')).then(function(reachable) {
1062
+ if (reachable) {
1063
+ document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">✓ Local LLM' + modelLabel + ' ready. Chat answers are generated locally — no cloud calls.</span>';
1064
+ } else {
1065
+ document.getElementById('chatHint').innerHTML = '<span style="color:#f59e0b">⚠ Ollama is offline. Launch it to chat locally: <code style="font-family:var(--mono);font-size:12px;background:rgba(245,158,11,0.1);color:#f59e0b;padding:2px 6px;border-radius:4px;">ollama run qwen2.5-coder:14b</code></span>';
1066
+ }
1067
+ });
1068
+ } else if (data.geminiKeyStatus === 'validated' || data.geminiValidatedAt) {
876
1069
  var when = data.geminiValidatedAt ? ' (validated ' + new Date(data.geminiValidatedAt).toLocaleTimeString() + ')' : '';
877
1070
  document.getElementById('chatHint').innerHTML = '<span style="color:var(--green)">✓ Gemini API key validated' + when + '. You can now chat with your data.</span>';
878
1071
  } else if (data.perplexityConfigured) {
@@ -883,10 +1076,15 @@ async function connect(options) {
883
1076
 
884
1077
  status.className = 'auth-status ok';
885
1078
  status.textContent = opts.localPro ? `✓ Local ${tierName} connected` : '✓ Connected';
1079
+ document.getElementById('disconnectBtn').style.display = 'inline-block';
1080
+ const titleEl = document.getElementById('authTitle');
1081
+ if (titleEl) {
1082
+ titleEl.textContent = opts.localPro ? `${tierName} License Active` : 'API Key Connected';
1083
+ }
886
1084
  document.getElementById('dashboardContent').style.display = 'block';
887
1085
  if (opts.localPro) {
888
1086
  const localProActiveMessage = 'Local Pro is active on this machine. Your dashboard is using the saved license key automatically.';
889
- const localEnterpriseActiveMessage = 'Local Enterprise is active on this machine. Your dashboard is using the saved license key automatically.';
1087
+ const localEnterpriseActiveMessage = 'Local Enterprise is active on this machine. Your dashboard is using the saved license key automatically. Click Disconnect to switch keys.';
890
1088
  input.value = 'local-license';
891
1089
  input.disabled = true;
892
1090
  input.placeholder = `Local ${tierName} auto-connected`;
@@ -904,6 +1102,10 @@ async function connect(options) {
904
1102
  } catch (e) {
905
1103
  status.className = 'auth-status err';
906
1104
  status.textContent = '✗ ' + e.message;
1105
+ const titleEl = document.getElementById('authTitle');
1106
+ if (titleEl) {
1107
+ titleEl.textContent = 'Connect your API key';
1108
+ }
907
1109
  if (opts.localPro) {
908
1110
  document.getElementById('authHelp').textContent = `Local ${tierName} bootstrap failed. Paste your THUMBGATE_API_KEY manually or retry the local launcher.`;
909
1111
  }
@@ -969,24 +1171,88 @@ async function search() {
969
1171
  }
970
1172
 
971
1173
  function renderResult(r) {
972
- const signal = r.signal || r.type || '';
1174
+ let contextText = r.context || r.text || r.content || '';
1175
+ let signal = r.signal || r.type || '';
1176
+
1177
+ if (contextText && contextText.trim().startsWith('{')) {
1178
+ const originalText = contextText;
1179
+ try {
1180
+ let cleanText = contextText.trim();
1181
+ if (cleanText.endsWith('…')) {
1182
+ cleanText = cleanText.slice(0, -1);
1183
+ }
1184
+ if (cleanText.endsWith(',...')) {
1185
+ cleanText = cleanText.slice(0, -4);
1186
+ }
1187
+ let parsed = null;
1188
+ try {
1189
+ parsed = JSON.parse(cleanText);
1190
+ } catch (_) {
1191
+ try { parsed = JSON.parse(cleanText + '"}'); } catch (_) {}
1192
+ try { parsed = JSON.parse(cleanText + '}'); } catch (_) {}
1193
+ try { parsed = JSON.parse(cleanText + '"} }'); } catch (_) {}
1194
+ }
1195
+ if (parsed && (parsed.context || parsed.text || parsed.content || parsed.message)) {
1196
+ contextText = parsed.context || parsed.text || parsed.content || parsed.message;
1197
+ } else {
1198
+ let contextMatch = originalText.match(/"context"\s*:\s*"((?:[^"\\]|\\.)*?)"/);
1199
+ if (!contextMatch) {
1200
+ contextMatch = originalText.match(/"context"\s*:\s*"((?:[^"\\]|\\.)*)/);
1201
+ }
1202
+ if (!contextMatch || !contextMatch[1]) {
1203
+ let otherMatch = originalText.match(/"(?:text|content|message)"\s*:\s*"((?:[^"\\]|\\.)*?)"/);
1204
+ if (!otherMatch) {
1205
+ otherMatch = originalText.match(/"(?:text|content|message)"\s*:\s*"((?:[^"\\]|\\.)*)/);
1206
+ }
1207
+ if (otherMatch && otherMatch[1]) {
1208
+ contextMatch = otherMatch;
1209
+ }
1210
+ }
1211
+ if (contextMatch && contextMatch[1]) {
1212
+ let val = contextMatch[1];
1213
+ val = val.replace(/(?:"\s*,\s*"[^"]*"\s*:?|"\s*,\s*|"\s*\}?\s*|\s*,\s*|\s*\}?\s*)$/, '');
1214
+ val = val.replace(/(?:\.\.\.|…|",\.\.\.|",…|"\s*,\s*\.\.\.|"\s*,\s*…)$/, '');
1215
+ contextText = val.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
1216
+ }
1217
+ }
1218
+ if (parsed && parsed.signal) {
1219
+ signal = parsed.signal;
1220
+ } else {
1221
+ const signalMatch = originalText.match(/"signal"\s*:\s*"([^"]+)"/);
1222
+ if (signalMatch && !signal) {
1223
+ signal = signalMatch[1];
1224
+ }
1225
+ }
1226
+ } catch (_) {}
1227
+ }
1228
+
973
1229
  const isUp = signal === 'up' || signal === 'positive' || signal === 'thumbs_up';
974
1230
  const isDown = signal === 'down' || signal === 'negative' || signal === 'thumbs_down';
975
1231
  const signalClass = isUp ? 'up' : isDown ? 'down' : '';
976
1232
  const signalLabel = isUp ? '👍 Positive' : isDown ? '👎 Negative' : signal;
977
- const title = r.title || r.context || r.text || '';
978
- const context = r.context || r.text || r.content || '';
1233
+
1234
+ let displayTitle = r.title || contextText || '';
1235
+ if (displayTitle && displayTitle.startsWith('feedback_')) {
1236
+ const parts = displayTitle.split('_');
1237
+ if (parts.length >= 3) {
1238
+ const type = parts[1];
1239
+ const id = parts.slice(2).join('_');
1240
+ displayTitle = (type === 'positive' ? '👍' : '👎') + ' Feedback (' + id + ')';
1241
+ }
1242
+ }
1243
+
979
1244
  const tags = r.tags || [];
980
1245
  const date = r.timestamp ? new Date(r.timestamp).toLocaleDateString() : '';
1246
+
981
1247
  return '<div class="result-card">' +
982
1248
  '<div class="result-header">' +
983
1249
  (signalLabel ? '<span class="result-signal ' + signalClass + '">' + signalLabel + '</span>' : '<span></span>') +
984
1250
  '<span class="result-date">' + date + '</span>' +
985
1251
  '</div>' +
986
- (title ? '<div class="result-title">' + highlightText(title) + '</div>' : '') +
987
- (context && context !== title ? '<div class="result-context">' + highlightText(context.slice(0, 300)) + '</div>' : '') +
1252
+ (displayTitle ? '<div class="result-title">' + highlightText(displayTitle) + '</div>' : '') +
1253
+ (contextText && contextText !== displayTitle ? '<div class="result-context">' + highlightText(contextText.slice(0, 300)) + '</div>' : '') +
988
1254
  (tags.length ? '<div class="result-tags">' + tags.map(function(t) {
989
- return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + escHtml(t) + '</button>';
1255
+ return '<button type="button" class="tag" data-tag="' + encodeURIComponent(String(t)) + '" title="' + escAttr('Search for ' + t) + '">' + highlightText(t) + '</button>';
990
1256
  }).join('') + '</div>' : '') +
991
1257
  '</div>';
992
1258
  }
@@ -1824,9 +2090,28 @@ function renderEnterpriseStatus(status) {
1824
2090
  { name: 'Dialogflow guard adapter', value: dfcx.liveAgentConfigured ? 'Configured' : 'Optional', note: dfcx.verification || 'Only needed for customer-owned Dialogflow CX agent deployments.' },
1825
2091
  { name: 'Legacy gcloud CX command', value: 'Unsupported', note: 'Do not use the old alpha gcloud CX command group; use REST API or console for DFCX proof.' }
1826
2092
  ];
1827
- target.innerHTML = rows.map(function(row) {
1828
- return '<div class="inventory-row"><div><div class="inventory-name">' + escHtml(row.name) + '</div><div class="inventory-subtitle">' + escHtml(row.note) + '</div></div><span class="remediation-action">' + escHtml(row.value) + '</span></div>';
1829
- }).join('');
2093
+
2094
+ function draw() {
2095
+ target.innerHTML = rows.map(function(row) {
2096
+ 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>';
2097
+ }).join('');
2098
+ }
2099
+
2100
+ draw();
2101
+
2102
+ if (localLlm) {
2103
+ var llmUrl = 'http://localhost:11434/v1';
2104
+ checkLocalLlmReachable(llmUrl).then(function(reachable) {
2105
+ if (reachable) {
2106
+ rows[0].value = '✓ Local LLM Online';
2107
+ draw();
2108
+ } else {
2109
+ rows[0].value = '⚠ Local LLM Offline';
2110
+ rows[0].note = 'Ollama is offline. Launch it to chat locally: ollama run qwen2.5-coder:14b';
2111
+ draw();
2112
+ }
2113
+ });
2114
+ }
1830
2115
  }
1831
2116
 
1832
2117
  async function loadEnterpriseDataChatStatus() {
@@ -2478,5 +2763,6 @@ function renderGateAuditChartFromData(gateAudit) {
2478
2763
  }
2479
2764
 
2480
2765
  </script>
2766
+ <script src="/js/buyer-intent.js"></script>
2481
2767
  </body>
2482
2768
  </html>
@@ -16,7 +16,7 @@ __GOOGLE_SITE_VERIFICATION_META__
16
16
  <meta property="og:image" content="/og.png">
17
17
  <meta name="keywords" content="ThumbGate federal, AI agent governance federal, NIST 800-53 AI agent, OMB M-24-10, EO 14110, FedRAMP AI coding agent, federal AI use case inventory, agent audit log, agency AI policy enforcement, Bedrock GovCloud, Azure Government AI, SBIR AI governance">
18
18
 
19
- <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
19
+ <script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
20
20
  __GA_BOOTSTRAP__
21
21
 
22
22
  <script>
package/public/guide.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>How to Stop AI Coding Agents From Repeating Mistakes — ThumbGate Guide</title>
7
7
  <!-- Privacy-friendly analytics by Plausible -->
8
- <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
8
+ <script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
9
9
  <meta name="description" content="The complete guide to preventing AI coding agent mistakes with pre-action checks, history-aware lesson distillation, and automatic prevention rules.">
10
10
  <meta name="keywords" content="AI agent mistakes, Claude Code force push, AI coding agent memory, MCP server guardrails, pre-action checks, vibe coding safety, PreToolUse hooks, ThumbGate, SpecLock alternative, Mem0 alternative">
11
11
  <meta property="og:title" content="How to Stop AI Coding Agents From Repeating Mistakes">
@@ -376,11 +376,12 @@ npx thumbgate init --agent gemini</code></pre>
376
376
  <pre><code>npx thumbgate init</code></pre>
377
377
  <p>One command. Works with Claude Code, Cursor, Codex, Gemini, Amp, and OpenCode. Claude Code can also call Codex for review, adversarial review, and second-pass handoffs through the repo-local bridge plugin.</p>
378
378
  <a href="https://thumbgate.ai/checkout/pro?utm_source=guide&utm_medium=cta_button&utm_campaign=pro_pack" class="cta">Get Pro — $19/mo or $149/yr</a>
379
- <a rel="nofollow noopener noreferrer" target="_blank" href="https://buy.stripe.com/00w14neyUcXA5pL5e33sI0e" class="cta cta-secondary">Pay $499 diagnostic</a>
380
- <a rel="nofollow noopener noreferrer" target="_blank" href="https://buy.stripe.com/fZu9AT76saPsg4pbCr3sI0f" class="cta cta-secondary">Pay $1500 sprint</a>
379
+ <a href="__SPRINT_DIAGNOSTIC_CHECKOUT_URL__" class="cta cta-secondary">Pay $499 diagnostic</a>
380
+ <a href="__WORKFLOW_SPRINT_CHECKOUT_URL__" class="cta cta-secondary">Pay $1500 sprint</a>
381
381
  <a href="https://thumbgate.ai/#workflow-sprint-intake" class="cta cta-secondary">Send workflow first</a>
382
- <p style="color:var(--muted); font-size:0.85rem;">Free: 5 captures/day, 25 total captures, 3 active prevention rules, hook blocking. Pro: hosted sync, dashboard, recall, lesson search, unlimited captures/rules, DPO export. Enterprise: intake first, then custom pricing scoped to your rollout.</p>
382
+ <p style="color:var(--muted); font-size:0.85rem;">Free: 2 captures/day, 10 total captures, 3 active prevention rules, hook blocking. Pro: hosted sync, dashboard, recall, lesson search, unlimited captures/rules, DPO export. Enterprise: intake first, then custom pricing scoped to your rollout.</p>
383
383
 
384
384
  </div>
385
+ <script src="/js/buyer-intent.js"></script>
385
386
  </body>
386
387
  </html>