thumbgate 1.26.7 → 1.27.2

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 (50) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +2 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +20 -9
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  9. package/adapters/mcp/server-stdio.js +28 -1
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +147 -10
  13. package/bin/dashboard-cli.js +7 -0
  14. package/config/gate-classifier-routing.json +98 -0
  15. package/config/gate-templates.json +60 -0
  16. package/config/mcp-allowlists.json +8 -7
  17. package/config/model-candidates.json +71 -6
  18. package/package.json +26 -10
  19. package/public/chatgpt-app.html +330 -0
  20. package/public/codex-plugin.html +66 -14
  21. package/public/dashboard.html +203 -17
  22. package/public/index.html +79 -4
  23. package/public/learn.html +70 -0
  24. package/public/lessons.html +129 -6
  25. package/public/numbers.html +2 -2
  26. package/public/pricing.html +20 -2
  27. package/scripts/agent-operations-planner.js +621 -0
  28. package/scripts/agent-reward-model.js +53 -1
  29. package/scripts/ai-component-inventory.js +367 -0
  30. package/scripts/classifier-routing.js +130 -0
  31. package/scripts/cli-schema.js +26 -0
  32. package/scripts/dashboard-chat.js +64 -17
  33. package/scripts/feedback-sanitizer.js +105 -0
  34. package/scripts/gates-engine.js +258 -61
  35. package/scripts/hybrid-feedback-context.js +141 -7
  36. package/scripts/memory-scope-readiness.js +159 -0
  37. package/scripts/parallel-workflow-orchestrator.js +293 -0
  38. package/scripts/plausible-domain-config.js +86 -0
  39. package/scripts/plausible-server-events.js +4 -2
  40. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  41. package/scripts/qa-scenario-planner.js +136 -0
  42. package/scripts/repeat-metric.js +28 -12
  43. package/scripts/secret-fixture-tokens.js +61 -0
  44. package/scripts/secret-scanner.js +44 -5
  45. package/scripts/security-scanner.js +80 -0
  46. package/scripts/seo-gsd.js +53 -0
  47. package/scripts/thumbgate-bench.js +16 -1
  48. package/scripts/tool-registry.js +37 -0
  49. package/scripts/workflow-sentinel.js +189 -4
  50. package/src/api/server.js +276 -10
@@ -16,12 +16,37 @@
16
16
  const path = require('path');
17
17
 
18
18
  const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models';
19
+ const PERPLEXITY_ENDPOINT = 'https://api.perplexity.ai/chat/completions';
19
20
  const DEFAULT_MODEL = 'gemini-2.5-flash';
20
21
  const MAX_QUESTION_CHARS = 2000;
21
22
  const MAX_CONTEXT_LESSONS = 8;
22
23
 
24
+ // Allowlist the model so a user-supplied `model` cannot route the call to an
25
+ // arbitrary / unexpected (or more expensive) endpoint. Anything not on the list
26
+ // falls back to the default.
27
+ const ALLOWED_MODELS = new Set([
28
+ 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro',
29
+ 'gemini-2.0-flash', 'gemini-2.0-flash-lite',
30
+ 'gemini-flash-latest', 'gemini-flash-lite-latest', 'gemini-pro-latest',
31
+ ]);
32
+
33
+ function resolveModel(requested) {
34
+ const r = String(requested || '').trim();
35
+ if (r && ALLOWED_MODELS.has(r)) return r;
36
+ const envModel = String(process.env.THUMBGATE_GEMINI_MODEL || '').trim();
37
+ if (envModel && ALLOWED_MODELS.has(envModel)) return envModel;
38
+ return DEFAULT_MODEL;
39
+ }
40
+
23
41
  function resolveApiKey(opts = {}) {
24
- return opts.apiKey || process.env.GEMINI_API_KEY || process.env.THUMBGATE_GEMINI_API_KEY || '';
42
+ let key = '';
43
+ if (Object.prototype.hasOwnProperty.call(opts, 'apiKey')) {
44
+ key = opts.apiKey || '';
45
+ } else {
46
+ key = opts.apiKey || process.env.GEMINI_API_KEY || process.env.THUMBGATE_GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.PERPLEXITY_API_KEY || process.env.THUMBGATE_PERPLEXITY_API_KEY || '';
47
+ }
48
+ if (!key) return '';
49
+ return key.trim().replace(/^["']|["']$/g, '');
25
50
  }
26
51
 
27
52
  // Retrieve the most relevant stored lessons for the question.
@@ -98,31 +123,53 @@ async function answerDataQuestion(question, opts = {}) {
98
123
  return {
99
124
  ok: false,
100
125
  error: 'no_api_key',
101
- message: 'Chat is not configured. Set GEMINI_API_KEY (e.g. `npx thumbgate setup-vertex --write`) to enable "chat with your data".',
126
+ message: 'Chat is not configured. Set a valid GEMINI_API_KEY or PERPLEXITY_API_KEY (for hybrid local-cloud) in the project .env or via dashboard Save. See adapters/perplexity/HYBRID.md.',
102
127
  sources,
103
128
  };
104
129
  }
105
130
 
106
- const model = opts.model || process.env.THUMBGATE_GEMINI_MODEL || DEFAULT_MODEL;
131
+ const model = resolveModel(opts.model);
107
132
  const prompt = buildChatPrompt(q, lessons);
108
133
  const fetchImpl = opts.fetch || globalThis.fetch;
134
+ const isPerplexity = apiKey.startsWith('pplx-') || apiKey.includes('perplexity');
109
135
 
110
136
  try {
111
- const res = await fetchImpl(`${GEMINI_ENDPOINT}/${encodeURIComponent(model)}:generateContent`, {
112
- method: 'POST',
113
- headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
114
- body: JSON.stringify({
115
- contents: [{ role: 'user', parts: [{ text: prompt }] }],
116
- generationConfig: { temperature: 0.2, maxOutputTokens: 1024 },
117
- }),
118
- });
119
- const json = await res.json().catch(() => ({}));
120
- if (!res.ok) {
121
- const msg = (json && json.error && json.error.message) ? String(json.error.message).split('\n')[0] : `HTTP ${res.status}`;
122
- return { ok: false, error: 'gemini_error', status: res.status, message: msg, sources };
137
+ if (isPerplexity) {
138
+ // Use Perplexity hybrid-capable API (OpenAI compatible) for RAG chat with your data
139
+ const res = await fetchImpl(PERPLEXITY_ENDPOINT, {
140
+ method: 'POST',
141
+ headers: { 'content-type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
142
+ body: JSON.stringify({
143
+ model: 'sonar', // or llama-3.1 etc for hybrid
144
+ messages: [{ role: 'user', content: prompt }],
145
+ temperature: 0.2,
146
+ max_tokens: 1024,
147
+ }),
148
+ });
149
+ const json = await res.json().catch(() => ({}));
150
+ if (!res.ok) {
151
+ const msg = (json && json.error && json.error.message) ? String(json.error.message).split('\n')[0] : `HTTP ${res.status}`;
152
+ return { ok: false, error: 'perplexity_error', status: res.status, message: msg, sources };
153
+ }
154
+ const answer = (json.choices && json.choices[0] && json.choices[0].message && json.choices[0].message.content) || '';
155
+ return { ok: true, answer: answer.trim() || '(no answer returned)', sources, model: json.model || 'perplexity-hybrid' };
156
+ } else {
157
+ const res = await fetchImpl(`${GEMINI_ENDPOINT}/${encodeURIComponent(model)}:generateContent`, {
158
+ method: 'POST',
159
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
160
+ body: JSON.stringify({
161
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
162
+ generationConfig: { temperature: 0.2, maxOutputTokens: 1024 },
163
+ }),
164
+ });
165
+ const json = await res.json().catch(() => ({}));
166
+ if (!res.ok) {
167
+ const msg = (json && json.error && json.error.message) ? String(json.error.message).split('\n')[0] : `HTTP ${res.status}`;
168
+ return { ok: false, error: 'gemini_error', status: res.status, message: msg, sources };
169
+ }
170
+ const answer = parseGeminiAnswer(json);
171
+ return { ok: true, answer: answer || '(no answer returned)', sources, model: json.modelVersion || model };
123
172
  }
124
- const answer = parseGeminiAnswer(json);
125
- return { ok: true, answer: answer || '(no answer returned)', sources, model: json.modelVersion || model };
126
173
  } catch (err) {
127
174
  return { ok: false, error: 'network', message: err && err.message ? err.message : String(err), sources };
128
175
  }
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const TRANSPORT_KEYS = new Set([
6
+ 'hookeventname',
7
+ 'hook_event_name',
8
+ 'sessionid',
9
+ 'session_id',
10
+ 'transcriptpath',
11
+ 'transcript_path',
12
+ 'timestamp',
13
+ 'createdat',
14
+ 'created_at',
15
+ 'updatedat',
16
+ 'updated_at',
17
+ 'cwd',
18
+ 'pid',
19
+ 'processid',
20
+ 'process_id',
21
+ 'promptid',
22
+ 'prompt_id',
23
+ 'traceid',
24
+ 'trace_id',
25
+ 'requestid',
26
+ 'request_id',
27
+ 'installid',
28
+ 'install_id',
29
+ 'visitorsessionid',
30
+ 'visitor_session_id',
31
+ 'toolinput',
32
+ 'tool_input',
33
+ ]);
34
+
35
+ const TRANSPORT_WORDS = new Set([
36
+ ...TRANSPORT_KEYS,
37
+ 'hook',
38
+ 'event',
39
+ 'userpromptsubmit',
40
+ 'user_prompt_submit',
41
+ 'pretooluse',
42
+ 'pre_tool_use',
43
+ 'posttooluse',
44
+ 'post_tool_use',
45
+ 'claude',
46
+ 'codex',
47
+ 'projects',
48
+ 'redacted',
49
+ 'tmp',
50
+ 'private',
51
+ 'folders',
52
+ 'json',
53
+ ]);
54
+
55
+ function stripEphemeralText(text) {
56
+ if (!text || typeof text !== 'string') return '';
57
+ return String(text)
58
+ .replace(/["']?(?:hook_?event_?name|session_?id|transcript_?path|timestamp|created_?at|updated_?at|cwd|pid|process_?id|prompt_?id|trace_?id|request_?id|install_?id|visitor_?session_?id)["']?\s*[:=]\s*["']?[^"',}\]\s]+["']?/gi, ' ')
59
+ .replace(/\/(?:private\/)?tmp\/[^\s"',}\]]+/gi, ' ')
60
+ .replace(/\/var\/folders\/[^\s"',}\]]+/gi, ' ')
61
+ .replace(/\/Users\/[^/\s]+\/\.(?:claude|codex|thumbgate)\/[^\s"',}\]]+/gi, ' ')
62
+ .replace(/\/Users\/[^/\s]+\/\.config\/thumbgate\/[^\s"',}\]]+/gi, ' ')
63
+ .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ' ')
64
+ .replace(/\b[0-9a-f]{24,}\b/gi, ' ')
65
+ .replace(/\b\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}(?:\.\d+)?z?\b/gi, ' ')
66
+ .replace(/\b\d{10,13}\b/g, ' ')
67
+ .replace(/:\d{4,5}\b/g, ':PORT');
68
+ }
69
+
70
+ function transportWordsOnly(text) {
71
+ const tokens = String(text || '')
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9_ -]/g, ' ')
74
+ .split(/\s+/)
75
+ .filter((token) => token.length >= 3);
76
+ if (tokens.length === 0) return true;
77
+ return tokens.every((token) => TRANSPORT_WORDS.has(token));
78
+ }
79
+
80
+ function sanitizeFeedbackText(text) {
81
+ const stripped = stripEphemeralText(text)
82
+ .replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
83
+ .replace(/\s+/g, ' ')
84
+ .trim();
85
+ if (transportWordsOnly(stripped)) return '';
86
+ return stripped;
87
+ }
88
+
89
+ function actionFingerprint(parts) {
90
+ const raw = Array.isArray(parts) ? parts.join(' ') : String(parts || '');
91
+ const stable = sanitizeFeedbackText(raw)
92
+ .toLowerCase()
93
+ .replace(/[^a-z0-9._:/ -]/g, ' ')
94
+ .replace(/\s+/g, ' ')
95
+ .trim();
96
+ if (!stable || transportWordsOnly(stable)) return null;
97
+ return crypto.createHash('sha256').update(stable).digest('hex').slice(0, 16);
98
+ }
99
+
100
+ module.exports = {
101
+ TRANSPORT_WORDS,
102
+ sanitizeFeedbackText,
103
+ actionFingerprint,
104
+ transportWordsOnly,
105
+ };