thumbgate 1.30.0 → 1.31.0

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 (77) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +54 -16
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/forge/forge.yaml +3 -3
  6. package/adapters/mcp/server-stdio.js +66 -8
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +147 -31
  10. package/config/evals/generation-quality-golden.json +95 -0
  11. package/config/evals/rag-answer-quality-golden.json +91 -0
  12. package/config/evals/retrieval-hybrid-ablation.json +66 -0
  13. package/config/evals/retrieval-ranking-golden.json +522 -0
  14. package/config/gates/default.json +217 -50
  15. package/config/mcp-allowlists.json +212 -206
  16. package/config/model-tiers.json +7 -2
  17. package/glama.json +6 -0
  18. package/package.json +60 -8
  19. package/public/assets/diagrams/before-after.svg +17 -16
  20. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  21. package/public/assets/diagrams/loop.svg +19 -13
  22. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  23. package/public/compare.html +1 -0
  24. package/public/dashboard.html +126 -28
  25. package/public/evaluations.html +1 -1
  26. package/public/index.html +142 -13
  27. package/public/numbers.html +3 -2
  28. package/public/pricing.html +143 -30
  29. package/scripts/a-plus-evidence-scorecard.js +303 -0
  30. package/scripts/async-eval-observability.js +36 -11
  31. package/scripts/audit-trail.js +37 -1
  32. package/scripts/auto-promote-gates.js +149 -34
  33. package/scripts/colbert-style-maxsim.js +236 -0
  34. package/scripts/cross-encoder-reranker.js +356 -126
  35. package/scripts/dashboard-chat.js +350 -17
  36. package/scripts/document-intake.js +283 -7
  37. package/scripts/eval-quality-suite.js +204 -0
  38. package/scripts/feedback-loop.js +115 -7
  39. package/scripts/feedback-paths.js +32 -13
  40. package/scripts/feedback-quality.js +53 -0
  41. package/scripts/filesystem-search.js +17 -7
  42. package/scripts/gates-engine.js +17 -0
  43. package/scripts/harness-tool-names.js +70 -0
  44. package/scripts/hook-runtime.js +10 -3
  45. package/scripts/lesson-db.js +16 -5
  46. package/scripts/lesson-embedding-index.js +67 -20
  47. package/scripts/lesson-embedding-maintenance.js +177 -0
  48. package/scripts/lesson-reranker.js +55 -9
  49. package/scripts/lesson-retrieval.js +305 -29
  50. package/scripts/lesson-search.js +22 -8
  51. package/scripts/llm-client.js +304 -15
  52. package/scripts/model-tier-router.js +593 -0
  53. package/scripts/pragmatic-hybrid-search.js +379 -0
  54. package/scripts/rag-document-pipeline.js +461 -0
  55. package/scripts/rag-structured-output.js +441 -0
  56. package/scripts/ragas-style-metrics.js +351 -0
  57. package/scripts/request-envelope.js +178 -0
  58. package/scripts/rerank-pipeline.js +370 -0
  59. package/scripts/rerank-quality-eval.js +155 -0
  60. package/scripts/retrieval-hybrid-ablation.js +120 -0
  61. package/scripts/retrieval-quality-tier.js +118 -0
  62. package/scripts/secret-scanner.js +395 -4
  63. package/scripts/self-distill-agent.js +7 -1
  64. package/scripts/self-healing-check.js +25 -0
  65. package/scripts/skill-packs.js +183 -0
  66. package/scripts/slow-loop.js +72 -0
  67. package/scripts/statusline-links.js +1 -1
  68. package/scripts/statusline.sh +8 -1
  69. package/scripts/telemetry-analytics.js +13 -1
  70. package/scripts/thumbgate-search.js +98 -6
  71. package/scripts/tier-budget-guard.js +186 -0
  72. package/scripts/tool-registry.js +46 -0
  73. package/scripts/vector-store.js +108 -4
  74. package/scripts/verify-marketing-pages-deployed.js +85 -3
  75. package/server.json +44 -0
  76. package/smithery.yaml +17 -0
  77. package/src/api/server.js +194 -13
@@ -248,6 +248,20 @@ const TOOLS = [
248
248
  scope: MEMORY_SCOPE_SCHEMA,
249
249
  requireScope: { type: 'boolean', description: 'Fail closed unless a complete four-field scope is supplied.' },
250
250
  includeShared: { type: 'boolean', description: 'Include explicitly shared memories with scoped results. Defaults true.' },
251
+ filters: {
252
+ type: 'object',
253
+ description: 'Pre-filter the candidate corpus before lexical or vector ranking.',
254
+ properties: {
255
+ domain: { type: ['string', 'array'], items: { type: 'string' } },
256
+ signal: { type: ['string', 'array'], items: { type: 'string' } },
257
+ source: { type: ['string', 'array'], items: { type: 'string' } },
258
+ toolsUsed: { type: ['string', 'array'], items: { type: 'string' } },
259
+ tags: { type: 'array', items: { type: 'string' } },
260
+ requireAllTags: { type: 'boolean' },
261
+ },
262
+ },
263
+ queryRewrite: { type: 'boolean', description: 'Enable bounded deterministic synonym expansion when the original lexical match is weak. Defaults true.' },
264
+ includeRetrievalMeta: { type: 'boolean', description: 'Include path provenance such as lexical/dense pools and query variants.' },
251
265
  },
252
266
  required: ['toolName'],
253
267
  },
@@ -276,6 +290,17 @@ const TOOLS = [
276
290
  limit: { type: 'number', description: 'Maximum results to return (default 10)' },
277
291
  source: { type: 'string', enum: ['all', 'feedback', 'context', 'rules', 'documents'], description: 'Restrict search to a single ThumbGate source.' },
278
292
  signal: { type: 'string', enum: ['up', 'down', 'positive', 'negative'], description: 'Optional feedback-signal filter when searching feedback data.' },
293
+ filters: {
294
+ type: 'object',
295
+ description: 'Pre-filter imported documents before chunk ranking.',
296
+ properties: {
297
+ tags: { type: 'array', items: { type: 'string' } },
298
+ requireAllTags: { type: 'boolean' },
299
+ sourceFormat: { type: 'string' },
300
+ sourceType: { type: 'string' },
301
+ },
302
+ },
303
+ queryRewrite: { type: 'boolean', description: 'Enable bounded deterministic synonym expansion. Defaults true.' },
279
304
  },
280
305
  },
281
306
  }),
@@ -1285,6 +1310,27 @@ const TOOLS = [
1285
1310
  },
1286
1311
  },
1287
1312
  }),
1313
+ readOnlyTool({
1314
+ name: 'gate_check',
1315
+ // Harnesses with a native pre-tool hook (Claude Code, Codex, Gemini, Forge) get hard
1316
+ // enforcement and never need this. MCP-only harnesses (Cline, Cursor, OpenCode) have no
1317
+ // interception point, so this exposes the SAME gates engine as a tool the agent calls
1318
+ // before acting. adapters/cline/.clinerules has instructed agents to call
1319
+ // `thumbgate.gate_check` since the adapter shipped — but the tool did not exist, so
1320
+ // that enforcement was inert. This makes the documented contract real.
1321
+ description: 'Evaluate a proposed tool call against ThumbGate policy BEFORE executing it. Returns decision "block" (do not run the action; surface the reason) or "allow". Use this when about to run a shell command, write/edit a file, or take any irreversible action.',
1322
+ inputSchema: {
1323
+ type: 'object',
1324
+ required: ['tool_name'],
1325
+ properties: {
1326
+ tool_name: { type: 'string', description: 'The tool about to be invoked (e.g. Bash, Write, Edit)' },
1327
+ tool_input: {
1328
+ type: 'object',
1329
+ description: 'The proposed arguments, e.g. { "command": "rm -rf /" } for Bash or { "file_path": "..." } for Write',
1330
+ },
1331
+ },
1332
+ },
1333
+ }),
1288
1334
  readOnlyTool({
1289
1335
  name: 'gate_stats',
1290
1336
  description: 'Get gate enforcement statistics -- blocked count, warned count, top gates',
@@ -80,6 +80,42 @@ function hasLocalTransformerProvider() {
80
80
  }
81
81
  }
82
82
 
83
+ function getOllamaEmbeddingConfig() {
84
+ const model = String(process.env.THUMBGATE_OLLAMA_EMBED_MODEL || '').trim();
85
+ let endpoint = String(
86
+ process.env.THUMBGATE_OLLAMA_ENDPOINT
87
+ || process.env.OLLAMA_HOST
88
+ || 'http://127.0.0.1:11434',
89
+ ).trim();
90
+ if (endpoint && !/^https?:\/\//i.test(endpoint)) endpoint = `http://${endpoint}`;
91
+ while (endpoint.endsWith('/')) endpoint = endpoint.slice(0, -1);
92
+ return {
93
+ enabled: Boolean(model),
94
+ model,
95
+ endpoint,
96
+ timeoutMs: Math.max(
97
+ 250,
98
+ Math.min(30_000, Number(process.env.THUMBGATE_OLLAMA_TIMEOUT_MS) || 10_000),
99
+ ),
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Synchronous capability check used by hot-path routing. This reports only
105
+ * semantic providers, never the deterministic feature-hash degradation.
106
+ */
107
+ function hasSemanticEmbeddingProvider() {
108
+ if (process.env.THUMBGATE_VECTOR_STUB_EMBED === 'true') return true;
109
+ if (getOllamaEmbeddingConfig().enabled) return true;
110
+ if (hasLocalTransformerProvider()) return true;
111
+ try {
112
+ const config = resolveGeminiEmbeddingConfig();
113
+ return config.provider === 'coreai' || Boolean(config.enabled && config.apiKey);
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
83
119
  function fnv1a32(value) {
84
120
  let hash = 0x811c9dc5;
85
121
  const bytes = Buffer.from(String(value), 'utf8');
@@ -98,7 +134,7 @@ function addHashedFeature(vector, feature, weight) {
98
134
  }
99
135
 
100
136
  function embedWithFeatureHash(text) {
101
- const vector = Array(FEATURE_HASH_DIMENSIONS).fill(0);
137
+ const vector = new Array(FEATURE_HASH_DIMENSIONS).fill(0);
102
138
  const tokens = String(text || '').toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [];
103
139
 
104
140
  for (let index = 0; index < tokens.length; index += 1) {
@@ -249,6 +285,43 @@ async function embedWithCoreAI(text, options = {}) {
249
285
  throw new Error('Core AI local service did not return a valid embedding');
250
286
  }
251
287
 
288
+ async function embedWithOllama(text, options = {}) {
289
+ const config = getOllamaEmbeddingConfig();
290
+ if (!config.enabled) {
291
+ throw new Error('Ollama embeddings require THUMBGATE_OLLAMA_EMBED_MODEL');
292
+ }
293
+ if (typeof fetch !== 'function') {
294
+ throw new Error('Ollama embeddings require global fetch. Use Node 18.18+.');
295
+ }
296
+
297
+ let response;
298
+ try {
299
+ response = await fetch(`${config.endpoint}/api/embed`, {
300
+ method: 'POST',
301
+ headers: { 'Content-Type': 'application/json' },
302
+ body: JSON.stringify({
303
+ model: config.model,
304
+ input: String(text || ''),
305
+ truncate: true,
306
+ dimensions: options.outputDimensionality || undefined,
307
+ }),
308
+ signal: AbortSignal.timeout(config.timeoutMs),
309
+ });
310
+ } catch (error) {
311
+ throw new Error(`Ollama embedding service unavailable: ${error.message}`);
312
+ }
313
+
314
+ if (!response.ok) {
315
+ throw new Error(`Ollama embedding request failed: ${response.status} ${response.statusText}`);
316
+ }
317
+ const payload = await response.json();
318
+ const vector = Array.isArray(payload.embeddings) ? payload.embeddings[0] : null;
319
+ if (!Array.isArray(vector) || vector.length === 0) {
320
+ throw new Error('Ollama embedding response did not include vector values');
321
+ }
322
+ return vector.map(Number);
323
+ }
324
+
252
325
  async function embed(text, options = {}) {
253
326
  if (process.env.THUMBGATE_VECTOR_STUB_EMBED === 'true') {
254
327
  // Deterministic 384-dim unit vector: first element = 1.0, rest = 0.0
@@ -277,6 +350,28 @@ async function embed(text, options = {}) {
277
350
  console.warn(`Core AI embedding failed, falling back to local: ${coreaiError.message}`);
278
351
  }
279
352
  }
353
+ const ollamaConfig = getOllamaEmbeddingConfig();
354
+ if (ollamaConfig.enabled) {
355
+ try {
356
+ const vector = await embedWithOllama(text, options);
357
+ _lastEmbeddingProfile = {
358
+ generatedAt: new Date().toISOString(),
359
+ source: 'local-ollama',
360
+ activeProfile: {
361
+ id: 'ollama',
362
+ model: ollamaConfig.model,
363
+ outputDimensionality: vector.length,
364
+ task: options.task || 'code retrieval',
365
+ rationale: 'Explicit local Ollama semantic embedding provider.',
366
+ qualityTier: 'production',
367
+ },
368
+ fallbackUsed: false,
369
+ };
370
+ return vector;
371
+ } catch (ollamaError) {
372
+ console.warn(`Ollama embedding failed, falling back: ${ollamaError.message}`);
373
+ }
374
+ }
280
375
  if (geminiConfig.enabled) {
281
376
  try {
282
377
  const vector = await embedWithGemini(text, options);
@@ -297,7 +392,9 @@ async function embed(text, options = {}) {
297
392
  if (!geminiConfig.fallbackToLocal) {
298
393
  throw geminiError;
299
394
  }
300
- console.warn(`Gemini embedding fallback: ${geminiError.message}`);
395
+ // Do not log raw provider/user-controlled error text (Sonar jssecurity:S5145).
396
+ const code = geminiError && (geminiError.code || geminiError.name || 'Error');
397
+ console.warn(`Gemini embedding fallback: ${code}`);
301
398
  }
302
399
  }
303
400
  if (hasLocalTransformerProvider()) {
@@ -314,6 +411,8 @@ async function embed(text, options = {}) {
314
411
  }
315
412
 
316
413
  const vector = embedWithFeatureHash(text);
414
+ // Feature-hash is a last-resort degrade, not production semantic quality.
415
+ // Callers (prove/eval/chat health) must treat quality_tier=degraded.
317
416
  _lastEmbeddingProfile = {
318
417
  generatedAt: new Date().toISOString(),
319
418
  source: 'built-in',
@@ -322,9 +421,11 @@ async function embed(text, options = {}) {
322
421
  model: 'ThumbGate feature hashing',
323
422
  outputDimensionality: FEATURE_HASH_DIMENSIONS,
324
423
  task: options.task || 'code retrieval',
325
- rationale: 'Deterministic zero-dependency local text embedding.',
424
+ rationale: 'DEGRADED: deterministic zero-dependency hash embedding — not semantic. Configure Gemini or local transformers for production retrieval quality.',
425
+ qualityTier: 'degraded',
326
426
  },
327
- fallbackUsed: false,
427
+ fallbackUsed: true,
428
+ fallbackReason: 'no_managed_or_transformer_embedder',
328
429
  };
329
430
  return vector;
330
431
  }
@@ -440,4 +541,7 @@ module.exports = {
440
541
  setGeminiEmbedderForTests,
441
542
  truncateForEmbedding,
442
543
  embedWithFeatureHash,
544
+ embedWithOllama,
545
+ getOllamaEmbeddingConfig,
546
+ hasSemanticEmbeddingProvider,
443
547
  };
@@ -22,6 +22,8 @@
22
22
  * --json machine-readable report on stdout, suitable for piping
23
23
  * into the GitHub Actions PR comment step.
24
24
  * --quiet suppress per-route lines; only print the final summary.
25
+ * --max-attempts=N retry only transient transport/5xx failures.
26
+ * --retry-delay-ms=N bounded delay between transient retries.
25
27
  *
26
28
  * Exit code is 0 when every page passes, 1 if any sentinel is missing
27
29
  * or any route returns non-200.
@@ -32,6 +34,8 @@ const path = require('node:path');
32
34
 
33
35
  const DEFAULT_PROD_URL = 'https://thumbgate-production.up.railway.app';
34
36
  const DEFAULT_TIMEOUT_MS = 12000;
37
+ const DEFAULT_MAX_ATTEMPTS = 2;
38
+ const DEFAULT_RETRY_DELAY_MS = 500;
35
39
  const DEFAULT_MANIFEST_PATH = path.resolve(__dirname, '..', 'config', 'post-deploy-marketing-pages.json');
36
40
 
37
41
  function parseArgs(argv = []) {
@@ -41,6 +45,8 @@ function parseArgs(argv = []) {
41
45
  json: false,
42
46
  quiet: false,
43
47
  timeoutMs: DEFAULT_TIMEOUT_MS,
48
+ maxAttempts: DEFAULT_MAX_ATTEMPTS,
49
+ retryDelayMs: DEFAULT_RETRY_DELAY_MS,
44
50
  };
45
51
  for (const arg of argv) {
46
52
  if (arg === '--json') out.json = true;
@@ -50,6 +56,12 @@ function parseArgs(argv = []) {
50
56
  else if (arg.startsWith('--timeout-ms=')) {
51
57
  const n = Number(arg.slice('--timeout-ms='.length));
52
58
  if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
59
+ } else if (arg.startsWith('--max-attempts=')) {
60
+ const n = Number(arg.slice('--max-attempts='.length));
61
+ if (Number.isInteger(n) && n > 0 && n <= 5) out.maxAttempts = n;
62
+ } else if (arg.startsWith('--retry-delay-ms=')) {
63
+ const n = Number(arg.slice('--retry-delay-ms='.length));
64
+ if (Number.isInteger(n) && n >= 0 && n <= 5000) out.retryDelayMs = n;
53
65
  }
54
66
  }
55
67
  return out;
@@ -80,7 +92,7 @@ function loadManifest(manifestPath) {
80
92
  return parsed;
81
93
  }
82
94
 
83
- async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAgent, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS }) {
95
+ async function probePageOnce({ prodUrl, route, sentinel, mustNotContain = [], userAgent, fetchImpl, timeoutMs }) {
84
96
  if (typeof fetchImpl !== 'function') {
85
97
  return { route, ok: false, error: 'fetch_unavailable' };
86
98
  }
@@ -97,7 +109,10 @@ async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAg
97
109
  Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
98
110
  },
99
111
  });
100
- const body = await res.text().catch(() => '');
112
+ // Keep body-read transport failures visible to the outer retry classifier.
113
+ // Treating a disconnected 2xx stream as an empty body would incorrectly
114
+ // turn an operational failure into a deterministic sentinel mismatch.
115
+ const body = await res.text();
101
116
  const sentinelPresent = body.includes(sentinel);
102
117
  const forbiddenPresent = Array.isArray(mustNotContain)
103
118
  ? mustNotContain.filter((value) => body.includes(value))
@@ -122,7 +137,67 @@ async function probePage({ prodUrl, route, sentinel, mustNotContain = [], userAg
122
137
  }
123
138
  }
124
139
 
125
- async function runVerification({ prodUrl, manifestPath, fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
140
+ function isTransientProbeFailure(result = {}) {
141
+ if (result.ok) return false;
142
+ if (result.error && result.error !== 'fetch_unavailable') return true;
143
+ return result.status === 429 || result.status >= 500;
144
+ }
145
+
146
+ function waitForRetry(delayMs) {
147
+ if (!delayMs) return Promise.resolve();
148
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
149
+ }
150
+
151
+ async function probePage({
152
+ prodUrl,
153
+ route,
154
+ sentinel,
155
+ mustNotContain = [],
156
+ userAgent,
157
+ fetchImpl = globalThis.fetch,
158
+ timeoutMs = DEFAULT_TIMEOUT_MS,
159
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
160
+ retryDelayMs = DEFAULT_RETRY_DELAY_MS,
161
+ }) {
162
+ const boundedAttempts = Number.isInteger(maxAttempts)
163
+ ? Math.min(Math.max(maxAttempts, 1), 5)
164
+ : DEFAULT_MAX_ATTEMPTS;
165
+ let result;
166
+ for (let attempt = 1; attempt <= boundedAttempts; attempt += 1) {
167
+ // eslint-disable-next-line no-await-in-loop
168
+ result = await probePageOnce({
169
+ prodUrl,
170
+ route,
171
+ sentinel,
172
+ mustNotContain,
173
+ userAgent,
174
+ fetchImpl,
175
+ timeoutMs,
176
+ });
177
+ if (!isTransientProbeFailure(result) || attempt === boundedAttempts) {
178
+ return {
179
+ ...result,
180
+ attempts: attempt,
181
+ recoveredAfterRetry: Boolean(result.ok && attempt > 1),
182
+ };
183
+ }
184
+ // A deploy can pass /health while the first buyer-page request still
185
+ // wakes a cold instance. Retry only transient failures; content contract
186
+ // mismatches remain immediate, deterministic failures.
187
+ // eslint-disable-next-line no-await-in-loop
188
+ await waitForRetry(retryDelayMs);
189
+ }
190
+ return { ...result, attempts: boundedAttempts, recoveredAfterRetry: false };
191
+ }
192
+
193
+ async function runVerification({
194
+ prodUrl,
195
+ manifestPath,
196
+ fetchImpl = globalThis.fetch,
197
+ timeoutMs = DEFAULT_TIMEOUT_MS,
198
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
199
+ retryDelayMs = DEFAULT_RETRY_DELAY_MS,
200
+ } = {}) {
126
201
  const manifest = loadManifest(manifestPath);
127
202
  const results = [];
128
203
  // Sequential is fine — manifest has <20 entries; parallelism would
@@ -137,6 +212,8 @@ async function runVerification({ prodUrl, manifestPath, fetchImpl = globalThis.f
137
212
  userAgent: entry.userAgent,
138
213
  fetchImpl,
139
214
  timeoutMs,
215
+ maxAttempts,
216
+ retryDelayMs,
140
217
  });
141
218
  results.push({ ...result, sentinel: entry.sentinel, description: entry.description });
142
219
  }
@@ -183,6 +260,8 @@ async function main(argv) {
183
260
  prodUrl: args.prodUrl,
184
261
  manifestPath: args.manifestPath,
185
262
  timeoutMs: args.timeoutMs,
263
+ maxAttempts: args.maxAttempts,
264
+ retryDelayMs: args.retryDelayMs,
186
265
  });
187
266
  } catch (error) {
188
267
  process.stderr.write(`verify-marketing-pages-deployed FAILED: ${error.message}\n`);
@@ -199,9 +278,12 @@ async function main(argv) {
199
278
  module.exports = {
200
279
  DEFAULT_PROD_URL,
201
280
  DEFAULT_TIMEOUT_MS,
281
+ DEFAULT_MAX_ATTEMPTS,
282
+ DEFAULT_RETRY_DELAY_MS,
202
283
  DEFAULT_MANIFEST_PATH,
203
284
  parseArgs,
204
285
  loadManifest,
286
+ isTransientProbeFailure,
205
287
  probePage,
206
288
  runVerification,
207
289
  renderHuman,
package/server.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.IgorGanapolsky/thumbgate",
4
+ "description": "ThumbGate pre-action gates for AI coding agents. Captures thumbs-up/down feedback as lessons, promotes repeated failures into prevention rules, and evaluates tool calls over stdio MCP before they run.",
5
+ "title": "ThumbGate",
6
+ "websiteUrl": "https://thumbgate.ai",
7
+ "repository": {
8
+ "source": "github",
9
+ "url": "https://github.com/IgorGanapolsky/ThumbGate"
10
+ },
11
+ "version": "1.31.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "registryBaseUrl": "https://registry.npmjs.org",
16
+ "identifier": "thumbgate",
17
+ "version": "1.31.0",
18
+ "runtimeHint": "npx",
19
+ "runtimeArguments": [
20
+ {
21
+ "type": "positional",
22
+ "value": "-y"
23
+ }
24
+ ],
25
+ "packageArguments": [
26
+ {
27
+ "type": "positional",
28
+ "value": "serve"
29
+ }
30
+ ],
31
+ "transport": {
32
+ "type": "stdio"
33
+ },
34
+ "environmentVariables": [
35
+ {
36
+ "name": "THUMBGATE_MCP_PROFILE",
37
+ "description": "Optional MCP tool profile override (essential | default | readonly | locked). Omit to keep the factory least-privilege essential profile; only set when expanded tools are intentional.",
38
+ "isRequired": false,
39
+ "format": "string"
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,17 @@
1
+ startCommand:
2
+ type: "stdio"
3
+ configSchema:
4
+ type: "object"
5
+ properties:
6
+ mcpProfile:
7
+ type: "string"
8
+ description: "MCP profile to use (default, readonly, locked)"
9
+ default: "default"
10
+ required: []
11
+ commandFunction:
12
+ command: "npx"
13
+ args:
14
+ - "-y"
15
+ - "thumbgate"
16
+ - "serve"
17
+ description: "ThumbGate — pre-action gates for AI coding agents. Turns thumbs-down feedback into prevention rules and evaluates tool calls over stdio MCP (npx -y thumbgate serve) before they run. Not the HTTP API (npm start). Works with Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode, and any MCP-compatible agent."