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
@@ -151,8 +151,67 @@ function sortResults(results) {
151
151
  });
152
152
  }
153
153
 
154
- function getFeedbackResults(query, limit, signal) {
155
- const results = searchFeedbackLog(query, Math.max(limit * 3, limit));
154
+ function extractFeedbackId(str) {
155
+ if (!str) return null;
156
+ const match = str.match(/fb[_-]\d+[_-][a-z0-9]+/i);
157
+ return match ? match[0].replace(/-/g, '_').toLowerCase() : null;
158
+ }
159
+
160
+ function deduplicateResults(results) {
161
+ const bestByFeedbackId = new Map();
162
+
163
+ for (const r of results) {
164
+ const feedId = extractFeedbackId(r.id || r.title || r.file || '');
165
+ if (feedId) {
166
+ const existing = bestByFeedbackId.get(feedId);
167
+ if (!existing) {
168
+ bestByFeedbackId.set(feedId, r);
169
+ } else {
170
+ const sourceOrder = { feedback: 4, contextfs: 3, prevention_rule: 2, document: 1 };
171
+ const existingOrder = sourceOrder[existing.source] || 0;
172
+ const currentOrder = sourceOrder[r.source] || 0;
173
+ if (currentOrder > existingOrder) {
174
+ bestByFeedbackId.set(feedId, r);
175
+ } else if (currentOrder === existingOrder && (r.score || 0) > (existing.score || 0)) {
176
+ bestByFeedbackId.set(feedId, r);
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ const finalResults = [];
183
+ const seenContent = new Set();
184
+ const seenIds = new Set();
185
+
186
+ for (const r of results) {
187
+ const feedId = extractFeedbackId(r.id || r.title || r.file || '');
188
+ let recordToUse = r;
189
+ if (feedId) {
190
+ recordToUse = bestByFeedbackId.get(feedId);
191
+ if (seenIds.has(recordToUse.id)) continue;
192
+ } else {
193
+ if (r.id && r.id !== 'null' && String(r.id).trim() !== '') {
194
+ if (seenIds.has(r.id)) continue;
195
+ }
196
+ }
197
+
198
+ const normTitle = String(recordToUse.title || '').trim().toLowerCase();
199
+ const normContext = String(recordToUse.context || '').trim().toLowerCase();
200
+ const contentKey = `${normTitle}|${normContext}`;
201
+ if (seenContent.has(contentKey)) continue;
202
+
203
+ if (recordToUse.id && recordToUse.id !== 'null' && String(recordToUse.id).trim() !== '') {
204
+ seenIds.add(recordToUse.id);
205
+ }
206
+ seenContent.add(contentKey);
207
+ finalResults.push(recordToUse);
208
+ }
209
+
210
+ return finalResults;
211
+ }
212
+
213
+ function getFeedbackResults(query, limit, signal, feedbackDir) {
214
+ const results = searchFeedbackLog(query, Math.max(limit * 3, limit), { feedbackDir });
156
215
  const normalizedSignal = normalizeSignal(signal);
157
216
  const filtered = normalizedSignal
158
217
  ? results.filter((record) => normalizeRecordSignal(record.signal) === normalizedSignal)
@@ -160,19 +219,19 @@ function getFeedbackResults(query, limit, signal) {
160
219
  return filtered.slice(0, limit).map(mapFeedbackResult);
161
220
  }
162
221
 
163
- function getContextResults(query, limit) {
164
- return searchContextFs(query, limit).map(mapContextResult);
222
+ function getContextResults(query, limit, feedbackDir) {
223
+ return searchContextFs(query, limit, { feedbackDir }).map(mapContextResult);
165
224
  }
166
225
 
167
- function getRuleResults(query, limit) {
168
- return searchPreventionRulesSync(query, limit).map(mapRuleResult);
226
+ function getRuleResults(query, limit, feedbackDir) {
227
+ return searchPreventionRulesSync(query, limit, { feedbackDir }).map(mapRuleResult);
169
228
  }
170
229
 
171
- function getDocumentResults(query, limit) {
172
- return searchImportedDocuments({ query, limit }).map(mapDocumentResult);
230
+ function getDocumentResults(query, limit, feedbackDir) {
231
+ return searchImportedDocuments({ query, limit, feedbackDir }).map(mapDocumentResult);
173
232
  }
174
233
 
175
- function searchThumbgate({ query, source = 'all', limit = 10, signal = null } = {}) {
234
+ function searchThumbgate({ query, source = 'all', limit = 10, signal = null, feedbackDir = null } = {}) {
176
235
  const trimmedQuery = String(query || '').trim();
177
236
  if (!trimmedQuery) {
178
237
  throw new Error('query is required');
@@ -182,22 +241,29 @@ function searchThumbgate({ query, source = 'all', limit = 10, signal = null } =
182
241
  const normalizedSignal = normalizeSignal(signal);
183
242
  const normalizedLimit = normalizeLimit(limit);
184
243
 
244
+ const fetchLimit = Math.max(100, normalizedLimit * 5);
245
+
185
246
  let results = [];
186
247
  if (normalizedSource === 'feedback') {
187
- results = getFeedbackResults(trimmedQuery, normalizedLimit, normalizedSignal);
248
+ const raw = getFeedbackResults(trimmedQuery, fetchLimit, normalizedSignal, feedbackDir);
249
+ results = deduplicateResults(raw).slice(0, normalizedLimit);
188
250
  } else if (normalizedSource === 'context') {
189
- results = getContextResults(trimmedQuery, normalizedLimit);
251
+ const raw = getContextResults(trimmedQuery, fetchLimit, feedbackDir);
252
+ results = deduplicateResults(raw).slice(0, normalizedLimit);
190
253
  } else if (normalizedSource === 'rules') {
191
- results = getRuleResults(trimmedQuery, normalizedLimit);
254
+ const raw = getRuleResults(trimmedQuery, fetchLimit, feedbackDir);
255
+ results = deduplicateResults(raw).slice(0, normalizedLimit);
192
256
  } else if (normalizedSource === 'documents') {
193
- results = getDocumentResults(trimmedQuery, normalizedLimit);
257
+ const raw = getDocumentResults(trimmedQuery, fetchLimit, feedbackDir);
258
+ results = deduplicateResults(raw).slice(0, normalizedLimit);
194
259
  } else {
195
- results = sortResults([
196
- ...getFeedbackResults(trimmedQuery, normalizedLimit, normalizedSignal),
197
- ...getContextResults(trimmedQuery, normalizedLimit),
198
- ...getRuleResults(trimmedQuery, normalizedLimit),
199
- ...getDocumentResults(trimmedQuery, normalizedLimit),
200
- ]).slice(0, normalizedLimit);
260
+ const combined = [
261
+ ...getFeedbackResults(trimmedQuery, fetchLimit, normalizedSignal, feedbackDir),
262
+ ...getContextResults(trimmedQuery, fetchLimit, feedbackDir),
263
+ ...getRuleResults(trimmedQuery, fetchLimit, feedbackDir),
264
+ ...getDocumentResults(trimmedQuery, fetchLimit, feedbackDir),
265
+ ];
266
+ results = deduplicateResults(sortResults(combined)).slice(0, normalizedLimit);
201
267
  }
202
268
 
203
269
  return {
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Tool Contract Validator
5
+ * Validates tool arguments against the tool's inputSchema.
6
+ */
7
+ function validateToolContract(schema, args) {
8
+ const errors = [];
9
+ if (!schema) return { valid: true, errors };
10
+
11
+ if (schema.type === 'object') {
12
+ if (typeof args !== 'object' || args === null || Array.isArray(args)) {
13
+ errors.push(`Expected object, got ${args === null ? 'null' : Array.isArray(args) ? 'array' : typeof args}`);
14
+ return { valid: false, errors };
15
+ }
16
+
17
+ // Check required fields
18
+ if (Array.isArray(schema.required)) {
19
+ for (const req of schema.required) {
20
+ if (args[req] === undefined || args[req] === null || args[req] === '') {
21
+ errors.push(`Missing required parameter: '${req}'`);
22
+ }
23
+ }
24
+ }
25
+
26
+ // Check properties
27
+ if (schema.properties) {
28
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
29
+ const value = args[key];
30
+ if (value === undefined || value === null) continue; // Optional field not provided
31
+
32
+ const valType = typeof value;
33
+ if (propSchema.type === 'string') {
34
+ if (valType !== 'string') {
35
+ errors.push(`Parameter '${key}' must be a string (got ${valType})`);
36
+ } else if (Array.isArray(propSchema.enum)) {
37
+ if (!propSchema.enum.includes(value)) {
38
+ errors.push(`Parameter '${key}' must be one of [${propSchema.enum.join(', ')}] (got '${value}')`);
39
+ }
40
+ }
41
+ } else if (propSchema.type === 'number') {
42
+ if (valType !== 'number' || isNaN(value)) {
43
+ errors.push(`Parameter '${key}' must be a number (got ${valType})`);
44
+ }
45
+ } else if (propSchema.type === 'boolean') {
46
+ if (valType !== 'boolean') {
47
+ errors.push(`Parameter '${key}' must be a boolean (got ${valType})`);
48
+ }
49
+ } else if (propSchema.type === 'array') {
50
+ if (!Array.isArray(value)) {
51
+ errors.push(`Parameter '${key}' must be an array (got ${valType})`);
52
+ }
53
+ } else if (propSchema.type === 'object') {
54
+ if (valType !== 'object' || value === null || Array.isArray(value)) {
55
+ errors.push(`Parameter '${key}' must be an object (got ${valType})`);
56
+ } else {
57
+ // Recurse for nested objects
58
+ const subRes = validateToolContract(propSchema, value);
59
+ if (!subRes.valid) {
60
+ for (const err of subRes.errors) {
61
+ errors.push(`Parameter '${key}': ${err}`);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ return {
71
+ valid: errors.length === 0,
72
+ errors,
73
+ };
74
+ }
75
+
76
+ module.exports = { validateToolContract };
@@ -172,6 +172,30 @@ async function embedWithGemini(text, options = {}) {
172
172
  return values.map(Number);
173
173
  }
174
174
 
175
+ async function embedWithCoreAI(text, options = {}) {
176
+ if (process.platform !== 'darwin') {
177
+ throw new Error('Core AI is only supported on macOS');
178
+ }
179
+ const endpoint = process.env.THUMBGATE_COREAI_ENDPOINT || 'http://localhost:8088';
180
+ try {
181
+ const res = await fetch(`${endpoint}/embed`, {
182
+ method: 'POST',
183
+ headers: { 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ text, options }),
185
+ signal: AbortSignal.timeout(2000),
186
+ });
187
+ if (res.ok) {
188
+ const payload = await res.json();
189
+ if (Array.isArray(payload.embedding)) {
190
+ return payload.embedding.map(Number);
191
+ }
192
+ }
193
+ } catch (err) {
194
+ throw new Error(`Core AI local service unavailable: ${err.message}`);
195
+ }
196
+ throw new Error('Core AI local service did not return a valid embedding');
197
+ }
198
+
175
199
  async function embed(text, options = {}) {
176
200
  if (process.env.THUMBGATE_VECTOR_STUB_EMBED === 'true') {
177
201
  // Deterministic 384-dim unit vector: first element = 1.0, rest = 0.0
@@ -180,6 +204,26 @@ async function embed(text, options = {}) {
180
204
  return stub;
181
205
  }
182
206
  const geminiConfig = resolveGeminiEmbeddingConfig();
207
+ if (geminiConfig.provider === 'coreai') {
208
+ try {
209
+ const vector = await embedWithCoreAI(text, options);
210
+ _lastEmbeddingProfile = {
211
+ generatedAt: new Date().toISOString(),
212
+ source: 'local-coreai',
213
+ activeProfile: {
214
+ id: 'coreai',
215
+ model: 'Core AI local model',
216
+ outputDimensionality: vector.length,
217
+ task: options.task || 'code retrieval',
218
+ rationale: 'Local Core AI Apple Silicon accelerated path.',
219
+ },
220
+ fallbackUsed: false,
221
+ };
222
+ return vector;
223
+ } catch (coreaiError) {
224
+ console.warn(`Core AI embedding failed, falling back to local: ${coreaiError.message}`);
225
+ }
226
+ }
183
227
  if (geminiConfig.enabled) {
184
228
  try {
185
229
  const vector = await embedWithGemini(text, options);
@@ -108,14 +108,74 @@ function parseCommandScore(output = '', status = 0, approvalRate = 0.5) {
108
108
  };
109
109
  }
110
110
 
111
+ function parseCommandLine(cmdString) {
112
+ const args = [];
113
+ let current = '';
114
+ let inDoubleQuote = false;
115
+ let inSingleQuote = false;
116
+ let escaped = false;
117
+
118
+ for (let i = 0; i < cmdString.length; i++) {
119
+ const char = cmdString[i];
120
+
121
+ if (escaped) {
122
+ current += char;
123
+ escaped = false;
124
+ } else if (char === '\\') {
125
+ if (inSingleQuote) {
126
+ current += char;
127
+ } else {
128
+ escaped = true;
129
+ }
130
+ } else if (char === '"' && !inSingleQuote) {
131
+ inDoubleQuote = !inDoubleQuote;
132
+ } else if (char === "'" && !inDoubleQuote) {
133
+ inSingleQuote = !inSingleQuote;
134
+ } else if (char === ' ' && !inDoubleQuote && !inSingleQuote) {
135
+ if (current) {
136
+ args.push(current);
137
+ current = '';
138
+ }
139
+ } else {
140
+ current += char;
141
+ }
142
+ }
143
+ if (current) {
144
+ args.push(current);
145
+ }
146
+ return args;
147
+ }
148
+
111
149
  function runCommand(command, {
112
150
  cwd = process.cwd(),
113
151
  env = process.env,
114
152
  timeoutMs = DEFAULT_TIMEOUT_MS,
115
153
  } = {}) {
116
154
  const startedAt = Date.now();
117
- const result = spawnSync(command, [], {
118
- shell: true,
155
+ const args = parseCommandLine(command);
156
+ const exec = args.shift();
157
+
158
+ const execBase = require('node:path').basename(exec).toLowerCase();
159
+ let safeExec;
160
+ if (exec === process.execPath) {
161
+ safeExec = process.execPath;
162
+ } else if (execBase === 'node' || execBase === 'node.exe') {
163
+ safeExec = process.execPath;
164
+ } else if (execBase === 'npm') {
165
+ safeExec = 'npm';
166
+ } else if (execBase === 'npm.cmd') {
167
+ safeExec = 'npm.cmd';
168
+ } else if (execBase === 'python3') {
169
+ safeExec = 'python3';
170
+ } else if (execBase === 'python') {
171
+ safeExec = 'python';
172
+ } else if (execBase === 'pytest') {
173
+ safeExec = 'pytest';
174
+ } else {
175
+ throw new Error(`Binary ${exec} is not authorized for workspace evolution.`);
176
+ }
177
+
178
+ const result = spawnSync(safeExec, args, {
119
179
  cwd,
120
180
  env,
121
181
  encoding: 'utf8',