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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +2 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +20 -9
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/mcp/server-stdio.js +28 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +147 -10
- package/bin/dashboard-cli.js +7 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +60 -0
- package/config/mcp-allowlists.json +8 -7
- package/config/model-candidates.json +71 -6
- package/package.json +26 -10
- package/public/chatgpt-app.html +330 -0
- package/public/codex-plugin.html +66 -14
- package/public/dashboard.html +203 -17
- package/public/index.html +79 -4
- package/public/learn.html +70 -0
- package/public/lessons.html +129 -6
- package/public/numbers.html +2 -2
- package/public/pricing.html +20 -2
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-schema.js +26 -0
- package/scripts/dashboard-chat.js +64 -17
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/gates-engine.js +258 -61
- package/scripts/hybrid-feedback-context.js +141 -7
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plausible-domain-config.js +86 -0
- package/scripts/plausible-server-events.js +4 -2
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +28 -12
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +80 -0
- package/scripts/seo-gsd.js +53 -0
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/tool-registry.js +37 -0
- package/scripts/workflow-sentinel.js +189 -4
- 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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
};
|