thumbgate 1.27.18 → 1.27.19
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 +6 -6
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +249 -30
- package/adapters/chatgpt/openapi.yaml +12 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1429 -121
- package/bin/postinstall.js +1 -8
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +1 -1
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pro.html +34 -11
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/rate-limiter.js +64 -13
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/seo-gsd.js +916 -7
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-registry.js +169 -1
- package/scripts/vector-store.js +45 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/scripts/bot-detector.js +0 -50
|
@@ -122,7 +122,7 @@ function resolveGeminiEmbeddingConfig(env = process.env) {
|
|
|
122
122
|
|
|
123
123
|
return {
|
|
124
124
|
enabled,
|
|
125
|
-
provider: enabled ? 'gemini' : 'local',
|
|
125
|
+
provider: provider === 'coreai' ? 'coreai' : (enabled ? 'gemini' : 'local'),
|
|
126
126
|
model: String(env.THUMBGATE_GEMINI_EMBED_MODEL || GEMINI_EMBEDDING_2_MODEL).trim() || GEMINI_EMBEDDING_2_MODEL,
|
|
127
127
|
apiKey,
|
|
128
128
|
apiBaseUrl: trimTrailingSlashes(env.THUMBGATE_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'),
|
|
@@ -171,6 +171,7 @@ function buildGeminiEmbeddingRolloutPlan(args = {}) {
|
|
|
171
171
|
},
|
|
172
172
|
rolloutSteps: [
|
|
173
173
|
'Keep local embeddings as the default offline path.',
|
|
174
|
+
'For Apple Silicon developers, route local queries through Core AI (AOT compiled models) to bypass CPU overhead.',
|
|
174
175
|
'Enable Gemini Embedding 2 only when a Gemini API key is present.',
|
|
175
176
|
'Use task-specific query/document prefixes at index and retrieval time.',
|
|
176
177
|
'Start at 768 dimensions, then benchmark 1536 only if recall misses show up.',
|
package/scripts/hook-runtime.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
publishedCliAvailable,
|
|
8
8
|
} = require('./mcp-config');
|
|
9
9
|
const { publishedCliShellCommand } = require('./published-cli');
|
|
10
|
+
const { shimInstalled, shimPath } = require('./install-shim');
|
|
10
11
|
|
|
11
12
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
12
13
|
const featureSupportCache = new Map();
|
|
@@ -33,34 +34,39 @@ function publishedHookCommandsAvailable(version) {
|
|
|
33
34
|
return available;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function
|
|
37
|
+
function resolveCliCommand(subcommand) {
|
|
38
|
+
// Source checkout: always use direct node command for development
|
|
39
|
+
if (isSourceCheckout(PKG_ROOT)) {
|
|
40
|
+
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
|
|
41
|
+
}
|
|
42
|
+
// Prefer stable shim — always resolves @latest, survives version bumps
|
|
43
|
+
if (shimInstalled()) {
|
|
44
|
+
return `${shellQuote(shimPath())} ${subcommand}`;
|
|
45
|
+
}
|
|
37
46
|
const version = packageVersion();
|
|
38
47
|
if (publishedHookCommandsAvailable(version)) {
|
|
39
|
-
return publishedCliShellCommand(version);
|
|
40
|
-
}
|
|
41
|
-
if (isSourceCheckout(PKG_ROOT)) {
|
|
42
|
-
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))}`;
|
|
48
|
+
return publishedCliShellCommand(version, [subcommand]);
|
|
43
49
|
}
|
|
44
|
-
return publishedCliShellCommand(version);
|
|
50
|
+
return publishedCliShellCommand(version, [subcommand]);
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
function
|
|
53
|
+
function resolveCodexCliCommand(subcommand) {
|
|
48
54
|
const version = packageVersion();
|
|
49
55
|
if (publishedHookCommandsAvailable(version)) {
|
|
50
|
-
return publishedCliShellCommand('latest', []
|
|
56
|
+
return publishedCliShellCommand('latest', [subcommand]);
|
|
51
57
|
}
|
|
52
58
|
if (isSourceCheckout(PKG_ROOT)) {
|
|
53
|
-
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))}`;
|
|
59
|
+
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
|
|
54
60
|
}
|
|
55
|
-
return publishedCliShellCommand('latest', []
|
|
61
|
+
return publishedCliShellCommand('latest', [subcommand]);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
function buildPortableHookCommand(subcommand) {
|
|
59
|
-
return
|
|
65
|
+
return resolveCliCommand(subcommand);
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
function buildCodexPortableHookCommand(subcommand) {
|
|
63
|
-
return
|
|
69
|
+
return resolveCodexCliCommand(subcommand);
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
function preToolHookCommand() {
|
|
@@ -115,8 +121,8 @@ module.exports = {
|
|
|
115
121
|
packageVersion,
|
|
116
122
|
publishedHookCommandsAvailable,
|
|
117
123
|
preToolHookCommand,
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
resolveCodexCliCommand,
|
|
125
|
+
resolveCliCommand,
|
|
120
126
|
sessionStartHookCommand,
|
|
121
127
|
statuslineCommand,
|
|
122
128
|
userPromptHookCommand,
|
|
@@ -6,11 +6,27 @@
|
|
|
6
6
|
* Also used directly by the CLI to refresh statusline counters after feedback capture.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
11
12
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
13
|
+
const {
|
|
14
|
+
getAggregateStatuslineCachePath,
|
|
15
|
+
shouldAggregateFeedback,
|
|
16
|
+
} = require('./feedback-aggregate');
|
|
12
17
|
|
|
13
18
|
function getCachePath() {
|
|
19
|
+
const explicitFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
|
|
20
|
+
if (shouldAggregateFeedback() && explicitFeedbackDir) {
|
|
21
|
+
try {
|
|
22
|
+
if (path.resolve(explicitFeedbackDir).startsWith(path.resolve(os.tmpdir()) + path.sep)) {
|
|
23
|
+
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (shouldAggregateFeedback()) return getAggregateStatuslineCachePath();
|
|
14
30
|
return path.join(resolveFeedbackDir(), 'statusline_cache.json');
|
|
15
31
|
}
|
|
16
32
|
|
|
@@ -18,6 +18,11 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
20
20
|
const { readJsonl } = require('./fs-utils');
|
|
21
|
+
const {
|
|
22
|
+
TRANSPORT_WORDS,
|
|
23
|
+
sanitizeFeedbackText,
|
|
24
|
+
transportWordsOnly,
|
|
25
|
+
} = require('./feedback-sanitizer');
|
|
21
26
|
|
|
22
27
|
// ---------------------------------------------------------------------------
|
|
23
28
|
// Paths
|
|
@@ -51,6 +56,7 @@ const STOPWORDS = new Set([
|
|
|
51
56
|
'has', 'had', 'not', 'but', 'they', 'you', 'can', 'will', 'all', 'any',
|
|
52
57
|
'one', 'its', 'our', 'also', 'more', 'very', 'just', 'into', 'been',
|
|
53
58
|
'bash', 'edit', 'write', 'tool', 'hook', 'clear',
|
|
59
|
+
...TRANSPORT_WORDS,
|
|
54
60
|
]);
|
|
55
61
|
|
|
56
62
|
const NEG = new Set([
|
|
@@ -74,7 +80,7 @@ const HYBRID_JSONL_READ_LIMIT = 400;
|
|
|
74
80
|
*/
|
|
75
81
|
function normalize(text) {
|
|
76
82
|
if (!text || typeof text !== 'string') return '';
|
|
77
|
-
return text
|
|
83
|
+
return sanitizeFeedbackText(text)
|
|
78
84
|
.replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
|
|
79
85
|
.replace(/:\d{4,5}\b/g, ':PORT')
|
|
80
86
|
.toLowerCase()
|
|
@@ -97,7 +103,9 @@ function stripFeedbackPrefix(text) {
|
|
|
97
103
|
* Compose normalize + stripFeedbackPrefix.
|
|
98
104
|
*/
|
|
99
105
|
function normalizePatternText(text) {
|
|
100
|
-
|
|
106
|
+
const normalized = normalize(stripFeedbackPrefix(text));
|
|
107
|
+
if (transportWordsOnly(normalized)) return '';
|
|
108
|
+
return normalized;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/**
|
|
@@ -125,6 +133,104 @@ function classify(entry) {
|
|
|
125
133
|
return 'neutral';
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
function isHookPromptEnvelope(context) {
|
|
137
|
+
if (!context || typeof context !== 'string') return false;
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(context);
|
|
140
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
|
|
141
|
+
return Boolean(
|
|
142
|
+
parsed.prompt &&
|
|
143
|
+
(
|
|
144
|
+
parsed.hookEventName ||
|
|
145
|
+
parsed.hook_event_name ||
|
|
146
|
+
parsed.workspaceRoot ||
|
|
147
|
+
parsed.workspace_root ||
|
|
148
|
+
parsed.session_id ||
|
|
149
|
+
parsed.sessionId ||
|
|
150
|
+
parsed.transcript_path ||
|
|
151
|
+
parsed.transcriptPath
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
} catch (_) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function patternContext(entry) {
|
|
160
|
+
const context = entry && entry.context ? String(entry.context) : '';
|
|
161
|
+
if (!context) return '';
|
|
162
|
+
const hasExplicitFeedback = Boolean(
|
|
163
|
+
entry.whatWentWrong ||
|
|
164
|
+
entry.what_went_wrong ||
|
|
165
|
+
entry.whatToChange ||
|
|
166
|
+
entry.what_to_change ||
|
|
167
|
+
entry.failureType ||
|
|
168
|
+
(Array.isArray(entry.tags) && entry.tags.length > 0) ||
|
|
169
|
+
entry.structuredRule
|
|
170
|
+
);
|
|
171
|
+
if (isHookPromptEnvelope(context) && !hasExplicitFeedback) return '';
|
|
172
|
+
if (isHookPromptEnvelope(context) && hasExplicitFeedback) {
|
|
173
|
+
return '';
|
|
174
|
+
}
|
|
175
|
+
return context;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if the feedback entry is an automated enforcement log (e.g. from gates engine)
|
|
180
|
+
* rather than real developer/user feedback.
|
|
181
|
+
*/
|
|
182
|
+
function isAutomatedFeedback(entry) {
|
|
183
|
+
const tags = entry.tags || [];
|
|
184
|
+
if (tags.includes('auto-capture') || tags.includes('gates-engine') || tags.includes('audit-trail')) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const context = String(entry.context || entry.whatWentWrong || '').toLowerCase();
|
|
188
|
+
return context.includes('gate "') || context.includes('blocked tool') || context.includes('warned tool');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
function isHookPromptEnvelope(context) {
|
|
193
|
+
if (!context || typeof context !== 'string') return false;
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(context);
|
|
196
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
|
|
197
|
+
return Boolean(
|
|
198
|
+
parsed.prompt &&
|
|
199
|
+
(
|
|
200
|
+
parsed.hookEventName ||
|
|
201
|
+
parsed.hook_event_name ||
|
|
202
|
+
parsed.workspaceRoot ||
|
|
203
|
+
parsed.workspace_root ||
|
|
204
|
+
parsed.session_id ||
|
|
205
|
+
parsed.sessionId ||
|
|
206
|
+
parsed.transcript_path ||
|
|
207
|
+
parsed.transcriptPath
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
} catch (_) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function patternContext(entry) {
|
|
216
|
+
const context = entry && entry.context ? String(entry.context) : '';
|
|
217
|
+
if (!context) return '';
|
|
218
|
+
const hasExplicitFeedback = Boolean(
|
|
219
|
+
entry.whatWentWrong ||
|
|
220
|
+
entry.what_went_wrong ||
|
|
221
|
+
entry.whatToChange ||
|
|
222
|
+
entry.what_to_change ||
|
|
223
|
+
entry.failureType ||
|
|
224
|
+
(Array.isArray(entry.tags) && entry.tags.length > 0) ||
|
|
225
|
+
entry.structuredRule
|
|
226
|
+
);
|
|
227
|
+
if (isHookPromptEnvelope(context) && !hasExplicitFeedback) return '';
|
|
228
|
+
if (isHookPromptEnvelope(context) && hasExplicitFeedback) {
|
|
229
|
+
return '';
|
|
230
|
+
}
|
|
231
|
+
return context;
|
|
232
|
+
}
|
|
233
|
+
|
|
128
234
|
/**
|
|
129
235
|
* Extract ms from a timestamp value. Returns 0 on failure.
|
|
130
236
|
*/
|
|
@@ -212,13 +318,15 @@ function buildHybridState(opts) {
|
|
|
212
318
|
if (cls === 'positive') positive++;
|
|
213
319
|
if (cls === 'negative') {
|
|
214
320
|
negative++;
|
|
215
|
-
// Track tool-level negative counts
|
|
216
|
-
|
|
217
|
-
|
|
321
|
+
// Track tool-level negative counts (exclude automated gate logs)
|
|
322
|
+
if (!isAutomatedFeedback(entry)) {
|
|
323
|
+
const toolName = inferToolName(entry.toolName || entry.tool_name || 'unknown', entry.context || '');
|
|
324
|
+
toolNegatives[toolName] = (toolNegatives[toolName] || 0) + 1;
|
|
325
|
+
}
|
|
218
326
|
|
|
219
327
|
// Build pattern from context / whatWentWrong / what_went_wrong
|
|
220
328
|
const rawText = [
|
|
221
|
-
entry
|
|
329
|
+
patternContext(entry),
|
|
222
330
|
entry.whatWentWrong || entry.what_went_wrong || '',
|
|
223
331
|
entry.whatToChange || entry.what_to_change || '',
|
|
224
332
|
entry.failureType || '',
|
|
@@ -254,11 +362,13 @@ function buildHybridState(opts) {
|
|
|
254
362
|
|
|
255
363
|
// Process attributed feedback separately to track attributed tool counts
|
|
256
364
|
for (const entry of attributedEntries) {
|
|
365
|
+
if (classify(entry) !== 'negative') continue; // skip pruned/positive
|
|
366
|
+
if (isAutomatedFeedback(entry)) continue; // skip automated gate blocks
|
|
257
367
|
const toolName = inferToolName(entry.toolName || entry.tool_name || entry.attributed_tool || 'unknown', entry.context || '');
|
|
258
368
|
toolNegativesAttributed[toolName] = (toolNegativesAttributed[toolName] || 0) + 1;
|
|
259
369
|
|
|
260
370
|
const rawText = [
|
|
261
|
-
entry
|
|
371
|
+
patternContext(entry),
|
|
262
372
|
entry.whatWentWrong || entry.what_went_wrong || '',
|
|
263
373
|
...(Array.isArray(entry.tags) ? entry.tags : []),
|
|
264
374
|
...(entry.richContext && Array.isArray(entry.richContext.filePaths) ? entry.richContext.filePaths : []),
|
|
@@ -620,12 +730,36 @@ function evaluatePretool(toolName, toolInput, opts) {
|
|
|
620
730
|
|
|
621
731
|
// Slow path: build live state (also used when compiled guards are stale)
|
|
622
732
|
const state = buildHybridState({
|
|
733
|
+
feedbackDir: o.feedbackDir,
|
|
623
734
|
feedbackLogPath: o.feedbackLogPath,
|
|
624
735
|
attributedFeedbackPath: o.attributedFeedbackPath,
|
|
625
736
|
});
|
|
626
737
|
return evaluatePretoolFromState(state, toolName, toolInput);
|
|
627
738
|
}
|
|
628
739
|
|
|
740
|
+
// Claw-style agent support (high-ROI for EnterpriseClaw / OpenShell agents from Automation Anywhere / Nvidia)
|
|
741
|
+
// Extends hybrid context for claw_action_type (file, screen, dynamic-tool, orchestration), agent_identity, hybrid_route.
|
|
742
|
+
// Use in evaluatePretool calls from claw-aware MCP/hooks: pass {clawContext: {actionType: 'dynamic-tool-creation', agentId: '...', route: 'local/cloud'}} in opts.
|
|
743
|
+
function evaluateClawPretool(toolName, toolInput, clawContext, opts) {
|
|
744
|
+
const o = opts || {};
|
|
745
|
+
const claw = clawContext || {};
|
|
746
|
+
// Merge claw metadata into toolInput for gate evaluation (so templates like block-dynamic-tool-creation can match)
|
|
747
|
+
const enrichedInput = {
|
|
748
|
+
...(typeof toolInput === 'object' ? toolInput : { raw: toolInput }),
|
|
749
|
+
_claw: {
|
|
750
|
+
actionType: claw.actionType || 'unknown',
|
|
751
|
+
agentId: claw.agentId || 'unknown',
|
|
752
|
+
hybridRoute: claw.hybridRoute || 'unknown',
|
|
753
|
+
screenInteraction: !!claw.screenInteraction,
|
|
754
|
+
fileAccess: !!claw.fileAccess,
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
const result = evaluatePretool(toolName, JSON.stringify(enrichedInput), o);
|
|
758
|
+
// Tag result with claw metadata for logging/feedback
|
|
759
|
+
result.clawContext = claw;
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
|
|
629
763
|
// ---------------------------------------------------------------------------
|
|
630
764
|
// CLI main()
|
|
631
765
|
// ---------------------------------------------------------------------------
|
|
@@ -674,6 +808,7 @@ function main() {
|
|
|
674
808
|
module.exports = {
|
|
675
809
|
buildHybridState,
|
|
676
810
|
evaluatePretool,
|
|
811
|
+
evaluateClawPretool,
|
|
677
812
|
compileGuardArtifact,
|
|
678
813
|
writeGuardArtifact,
|
|
679
814
|
readGuardArtifact,
|
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
20
20
|
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
21
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
21
22
|
const {
|
|
22
23
|
buildStableId,
|
|
23
24
|
extractFilePaths,
|
|
@@ -137,15 +138,19 @@ function createLesson({ feedbackId, signal, inferredLesson, triggerMessage, prio
|
|
|
137
138
|
// Stable link: dashboard deep-link to this lesson
|
|
138
139
|
lesson.link = `${getLessonBaseUrl()}/lessons#${lesson.id}`;
|
|
139
140
|
|
|
141
|
+
// Redact secrets before persisting — lesson text is derived from conversation content that may
|
|
142
|
+
// have captured a pasted credential. See scripts/secret-redaction.js.
|
|
143
|
+
const redactedLesson = redactSecretsDeep(lesson);
|
|
144
|
+
|
|
140
145
|
const lessonsPath = getLessonsPath();
|
|
141
146
|
ensureParentDir(lessonsPath);
|
|
142
|
-
fs.appendFileSync(lessonsPath, JSON.stringify(
|
|
147
|
+
fs.appendFileSync(lessonsPath, JSON.stringify(redactedLesson) + '\n');
|
|
143
148
|
|
|
144
149
|
// Update recent lesson for statusbar
|
|
145
150
|
const recentPath = getRecentLessonPath();
|
|
146
|
-
fs.writeFileSync(recentPath, JSON.stringify(
|
|
151
|
+
fs.writeFileSync(recentPath, JSON.stringify(redactedLesson, null, 2) + '\n');
|
|
147
152
|
|
|
148
|
-
return
|
|
153
|
+
return redactedLesson;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
/**
|
package/scripts/lesson-search.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
|
|
5
|
+
const { buildMemoryLifecycleView, scoreHybridMemoryMatch } = require('./agent-memory-lifecycle');
|
|
5
6
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
6
7
|
|
|
7
8
|
const HIGH_RISK_TAGS = new Set([
|
|
@@ -411,7 +412,8 @@ function scoreLesson(queryText, memory, parsed, sourceFeedback) {
|
|
|
411
412
|
const score = jaccardSimilarity(queryTokens, lessonTokens)
|
|
412
413
|
+ substringBoost(queryText, lessonText)
|
|
413
414
|
+ recencyScore(memory.timestamp)
|
|
414
|
-
+ (memory.category === 'error' ? 0.05 : 0)
|
|
415
|
+
+ (memory.category === 'error' ? 0.05 : 0)
|
|
416
|
+
+ Math.min(0.2, scoreHybridMemoryMatch(queryText, memory).score * 0.1);
|
|
415
417
|
|
|
416
418
|
return {
|
|
417
419
|
score,
|
|
@@ -427,6 +429,7 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
|
|
|
427
429
|
const { score, matchedTokens } = scoreLesson(options.query || '', memory, parsed, sourceFeedback);
|
|
428
430
|
const harnessRecommendations = buildHarnessRecommendations(memory, parsed, sourceFeedback, ruleMatches, gateMatches);
|
|
429
431
|
const lifecycle = buildLifecycle(memory, parsed, sourceFeedback, ruleMatches, gateMatches, harnessRecommendations);
|
|
432
|
+
const memoryLifecycle = buildMemoryLifecycleView(memory, { query: options.query || '' });
|
|
430
433
|
|
|
431
434
|
return {
|
|
432
435
|
id: memory.id,
|
|
@@ -452,6 +455,7 @@ function buildLessonResult(memory, sourceFeedback, options = {}) {
|
|
|
452
455
|
systemResponse: {
|
|
453
456
|
promotedToMemory: true,
|
|
454
457
|
lifecycle,
|
|
458
|
+
memoryLifecycle,
|
|
455
459
|
diagnosis: memory.diagnosis || null,
|
|
456
460
|
sourceFeedback: sourceFeedback
|
|
457
461
|
? {
|
|
@@ -589,6 +593,14 @@ function tryFts5Search(query, options) {
|
|
|
589
593
|
tags: row.tags,
|
|
590
594
|
importance: row.importance,
|
|
591
595
|
timestamp: row.timestamp,
|
|
596
|
+
memoryLifecycle: buildMemoryLifecycleView({
|
|
597
|
+
title: row.context,
|
|
598
|
+
content: [row.whatWentWrong, row.whatToChange, row.whatWorked].filter(Boolean).join('\n'),
|
|
599
|
+
domain: row.domain,
|
|
600
|
+
tags: row.tags,
|
|
601
|
+
importance: row.importance,
|
|
602
|
+
timestamp: row.timestamp,
|
|
603
|
+
}, { query: query || '' }),
|
|
592
604
|
})),
|
|
593
605
|
backend: 'sqlite-fts5',
|
|
594
606
|
};
|
|
@@ -618,6 +630,10 @@ function formatLessonSearchResults(payload) {
|
|
|
618
630
|
payload.results.forEach((result, index) => {
|
|
619
631
|
lines.push(`${index + 1}. ${result.title}`);
|
|
620
632
|
lines.push(` Category: ${result.category} | Tags: ${result.tags.join(', ') || 'none'} | Score: ${result.score}`);
|
|
633
|
+
if (result.systemResponse.memoryLifecycle) {
|
|
634
|
+
const memoryLifecycle = result.systemResponse.memoryLifecycle;
|
|
635
|
+
lines.push(` Memory: scope=${memoryLifecycle.scope} | decay=${memoryLifecycle.decay.state} | hybrid=${memoryLifecycle.retrievalHints.hybridScore}`);
|
|
636
|
+
}
|
|
621
637
|
if (result.lesson.summary) {
|
|
622
638
|
lines.push(` Lesson: ${result.lesson.summary}`);
|
|
623
639
|
}
|
package/scripts/license.js
CHANGED
|
@@ -23,15 +23,17 @@ function isValidKey(key) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function verifyLicense(options = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Only ThumbGate's own env vars are license candidates — scanning foreign
|
|
27
|
+
// *_API_KEY / *_PRO_KEY vars would treat another vendor's secret as a
|
|
28
|
+
// license key. The result object never carries the raw key value.
|
|
29
|
+
const envEntry = [
|
|
30
|
+
['THUMBGATE_API_KEY', process.env.THUMBGATE_API_KEY],
|
|
31
|
+
['THUMBGATE_PRO_KEY', process.env.THUMBGATE_PRO_KEY],
|
|
29
32
|
...Object.entries(process.env)
|
|
30
|
-
.filter(([name]) => /(?:_API_KEY|_PRO_KEY)$/.test(name))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { valid: true, source: 'env', key: envKey };
|
|
33
|
+
.filter(([name]) => name.startsWith('THUMBGATE_') && /(?:_API_KEY|_PRO_KEY)$/.test(name)),
|
|
34
|
+
].find(([, value]) => isValidKey(value));
|
|
35
|
+
if (envEntry) {
|
|
36
|
+
return { valid: true, source: 'env', envVar: envEntry[0] };
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const licensePath = getLicensePath(options.homeDir);
|
|
@@ -42,7 +44,6 @@ function verifyLicense(options = {}) {
|
|
|
42
44
|
return {
|
|
43
45
|
valid: true,
|
|
44
46
|
source: 'file',
|
|
45
|
-
key: data.key,
|
|
46
47
|
activatedAt: data.activatedAt,
|
|
47
48
|
path: licensePath,
|
|
48
49
|
};
|
|
@@ -80,7 +81,6 @@ module.exports = {
|
|
|
80
81
|
verifyLicense,
|
|
81
82
|
isProLicensed,
|
|
82
83
|
activateLicense,
|
|
83
|
-
generateLicenseKey,
|
|
84
84
|
isValidKey,
|
|
85
85
|
VALID_PREFIXES,
|
|
86
86
|
LICENSE_PATH,
|
package/scripts/llm-client.js
CHANGED
|
@@ -11,20 +11,23 @@ const MODELS = {
|
|
|
11
11
|
const DEFAULT_MODEL = MODELS.FAST;
|
|
12
12
|
const DEFAULT_MAX_TOKENS = 1024;
|
|
13
13
|
const DEFAULT_CACHE_TTL = '5m';
|
|
14
|
+
const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/paas/v4';
|
|
15
|
+
const DEFAULT_ZAI_MODEL = 'glm-5.2-flash';
|
|
14
16
|
|
|
15
|
-
let
|
|
17
|
+
let _anthropicClient = null;
|
|
18
|
+
let _geminiClient = null;
|
|
16
19
|
|
|
17
20
|
function isAvailable() {
|
|
18
21
|
return Boolean(process.env.ANTHROPIC_API_KEY);
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function getClient() {
|
|
22
|
-
if (
|
|
25
|
+
if (_anthropicClient) return _anthropicClient;
|
|
23
26
|
if (!isAvailable()) return null;
|
|
24
27
|
try {
|
|
25
28
|
const Anthropic = require('@anthropic-ai/sdk');
|
|
26
|
-
|
|
27
|
-
return
|
|
29
|
+
_anthropicClient = new Anthropic();
|
|
30
|
+
return _anthropicClient;
|
|
28
31
|
} catch {
|
|
29
32
|
return null;
|
|
30
33
|
}
|
|
@@ -138,7 +141,144 @@ function parseClaudeJson(text) {
|
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
function getZaiApiKey(env = process.env) {
|
|
145
|
+
return env.ZAI_API_KEY || env.THUMBGATE_ZAI_API_KEY || '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getZaiBaseUrl(env = process.env) {
|
|
149
|
+
return env.ZAI_BASE_URL || env.THUMBGATE_ZAI_BASE_URL || DEFAULT_ZAI_BASE_URL;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getZaiModel(env = process.env) {
|
|
153
|
+
return env.ZAI_API_MODEL || env.THUMBGATE_ZAI_MODEL || DEFAULT_ZAI_MODEL;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function callZaiInternal(options = {}, env = process.env) {
|
|
157
|
+
const apiKey = getZaiApiKey(env);
|
|
158
|
+
if (!apiKey || typeof fetch !== 'function') return null;
|
|
159
|
+
|
|
160
|
+
const messages = Array.isArray(options.messages) && options.messages.length > 0
|
|
161
|
+
? options.messages
|
|
162
|
+
: [
|
|
163
|
+
...(options.systemPrompt ? [{ role: 'system', content: options.systemPrompt }] : []),
|
|
164
|
+
{ role: 'user', content: options.userPrompt || '' },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(`${getZaiBaseUrl(env).replace(/\/$/, '')}/chat/completions`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: `Bearer ${apiKey}`,
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
model: options.model || getZaiModel(env),
|
|
176
|
+
messages,
|
|
177
|
+
max_tokens: options.maxTokens || DEFAULT_MAX_TOKENS,
|
|
178
|
+
temperature: Number.isFinite(options.temperature) ? options.temperature : 0,
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!response.ok) return null;
|
|
183
|
+
const json = await response.json();
|
|
184
|
+
return {
|
|
185
|
+
text: stripCodeFences(json?.choices?.[0]?.message?.content || ''),
|
|
186
|
+
usage: json?.usage || null,
|
|
187
|
+
stopReason: json?.choices?.[0]?.finish_reason || null,
|
|
188
|
+
id: json?.id || null,
|
|
189
|
+
model: json?.model || options.model || getZaiModel(env),
|
|
190
|
+
};
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function callGeminiInternal(options = {}) {
|
|
197
|
+
const env = process.env;
|
|
198
|
+
const { detectInferenceBackend } = require('./local-model-profile');
|
|
199
|
+
const providerMode = detectInferenceBackend(env).providerMode;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const { GoogleGenAI } = require('@google/genai');
|
|
203
|
+
if (!_geminiClient) {
|
|
204
|
+
if (providerMode === 'vertex') {
|
|
205
|
+
_geminiClient = new GoogleGenAI({
|
|
206
|
+
enterprise: true,
|
|
207
|
+
project: env.VERTEX_PROJECT_ID || 'ai-revenue28-webhook',
|
|
208
|
+
location: env.VERTEX_LOCATION || 'us-central1',
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
_geminiClient = new GoogleGenAI({
|
|
212
|
+
apiKey: env.GEMINI_API_KEY,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const contents = convertMessagesToGemini(options.messages, options.userPrompt);
|
|
218
|
+
const config = {};
|
|
219
|
+
if (options.systemPrompt) {
|
|
220
|
+
config.systemInstruction = options.systemPrompt;
|
|
221
|
+
}
|
|
222
|
+
if (Number.isFinite(options.temperature)) {
|
|
223
|
+
config.temperature = options.temperature;
|
|
224
|
+
}
|
|
225
|
+
if (options.maxTokens) {
|
|
226
|
+
config.maxOutputTokens = options.maxTokens;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const response = await runStep('llm.callGemini', {
|
|
230
|
+
retries: 2,
|
|
231
|
+
logger: (msg) => console.warn(msg),
|
|
232
|
+
}, async () => _geminiClient.models.generateContent({
|
|
233
|
+
model: options.model,
|
|
234
|
+
contents,
|
|
235
|
+
config,
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
text: response.text || '',
|
|
240
|
+
usage: response.usageMetadata ? {
|
|
241
|
+
input_tokens: response.usageMetadata.promptTokenCount,
|
|
242
|
+
output_tokens: response.usageMetadata.candidatesTokenCount,
|
|
243
|
+
} : null,
|
|
244
|
+
stopReason: response.candidates?.[0]?.finishReason || null,
|
|
245
|
+
id: null,
|
|
246
|
+
model: options.model,
|
|
247
|
+
};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('Gemini/Vertex AI execution error:', err);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function convertMessagesToGemini(messages, userPrompt) {
|
|
255
|
+
const list = Array.isArray(messages) && messages.length > 0
|
|
256
|
+
? messages
|
|
257
|
+
: [{ role: 'user', content: userPrompt }];
|
|
258
|
+
|
|
259
|
+
return list.map((msg) => {
|
|
260
|
+
const role = msg.role === 'assistant' ? 'model' : 'user';
|
|
261
|
+
let text = '';
|
|
262
|
+
if (typeof msg.content === 'string') {
|
|
263
|
+
text = msg.content;
|
|
264
|
+
} else if (Array.isArray(msg.content)) {
|
|
265
|
+
text = msg.content.map((c) => c.text || '').join('');
|
|
266
|
+
} else if (msg.content && typeof msg.content === 'object') {
|
|
267
|
+
text = msg.content.text || JSON.stringify(msg.content);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
role,
|
|
271
|
+
parts: [{ text }],
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
141
276
|
async function callClaudeInternal(options = {}) {
|
|
277
|
+
const modelName = options.model || '';
|
|
278
|
+
if (modelName.startsWith('gemini') || modelName.startsWith('vertex')) {
|
|
279
|
+
return callGeminiInternal(options);
|
|
280
|
+
}
|
|
281
|
+
|
|
142
282
|
const client = getClient();
|
|
143
283
|
if (!client) return null;
|
|
144
284
|
|
|
@@ -193,10 +333,35 @@ async function callClaudeJson(options = {}) {
|
|
|
193
333
|
return parsed;
|
|
194
334
|
}
|
|
195
335
|
|
|
336
|
+
async function callZaiJson(options = {}) {
|
|
337
|
+
const result = await callZaiInternal(options);
|
|
338
|
+
if (!result) return null;
|
|
339
|
+
|
|
340
|
+
const parsed = parseClaudeJson(result.text);
|
|
341
|
+
if (parsed === null) return null;
|
|
342
|
+
|
|
343
|
+
if (options.returnMetadata) {
|
|
344
|
+
return {
|
|
345
|
+
parsed,
|
|
346
|
+
text: result.text,
|
|
347
|
+
usage: result.usage,
|
|
348
|
+
stopReason: result.stopReason,
|
|
349
|
+
id: result.id,
|
|
350
|
+
model: result.model,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return parsed;
|
|
355
|
+
}
|
|
356
|
+
|
|
196
357
|
module.exports = {
|
|
197
358
|
isAvailable,
|
|
198
359
|
callClaude,
|
|
199
360
|
callClaudeJson,
|
|
361
|
+
callZaiJson,
|
|
362
|
+
getZaiApiKey,
|
|
363
|
+
getZaiBaseUrl,
|
|
364
|
+
getZaiModel,
|
|
200
365
|
stripCodeFences,
|
|
201
366
|
parseClaudeJson,
|
|
202
367
|
normalizeCacheOptions,
|