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.
Files changed (96) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +33 -12
  5. package/.well-known/mcp/server-card.json +8 -8
  6. package/README.md +249 -30
  7. package/adapters/chatgpt/openapi.yaml +12 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/gemini/function-declarations.json +1 -0
  11. package/adapters/mcp/server-stdio.js +263 -11
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bench/thumbgate-bench.json +2 -2
  14. package/bin/cli.js +1429 -121
  15. package/bin/postinstall.js +1 -8
  16. package/config/gate-classifier-routing.json +98 -0
  17. package/config/gate-templates.json +216 -0
  18. package/config/gates/claim-verification.json +12 -0
  19. package/config/gates/default.json +31 -2
  20. package/config/github-about.json +2 -2
  21. package/config/mcp-allowlists.json +23 -13
  22. package/config/merge-quality-checks.json +0 -1
  23. package/config/model-candidates.json +121 -6
  24. package/config/post-deploy-marketing-pages.json +80 -0
  25. package/config/tessl-tiles.json +1 -3
  26. package/openapi/openapi.yaml +12 -0
  27. package/package.json +1 -1
  28. package/public/blog.html +4 -4
  29. package/public/codex-plugin.html +72 -20
  30. package/public/compare.html +31 -8
  31. package/public/dashboard.html +930 -166
  32. package/public/federal.html +2 -2
  33. package/public/guide.html +33 -13
  34. package/public/index.html +469 -111
  35. package/public/learn.html +183 -18
  36. package/public/lessons.html +168 -10
  37. package/public/numbers.html +7 -7
  38. package/public/pro.html +34 -11
  39. package/scripts/agent-memory-lifecycle.js +211 -0
  40. package/scripts/agent-readiness.js +20 -3
  41. package/scripts/agent-reward-model.js +53 -1
  42. package/scripts/auto-promote-gates.js +82 -10
  43. package/scripts/auto-wire-hooks.js +14 -0
  44. package/scripts/billing.js +93 -1
  45. package/scripts/bot-detection.js +61 -3
  46. package/scripts/build-metadata.js +50 -10
  47. package/scripts/cli-feedback.js +4 -2
  48. package/scripts/cli-schema.js +97 -0
  49. package/scripts/cli-telemetry.js +6 -1
  50. package/scripts/commercial-offer.js +82 -2
  51. package/scripts/context-manager.js +74 -6
  52. package/scripts/dashboard.js +68 -2
  53. package/scripts/export-databricks-bundle.js +5 -1
  54. package/scripts/export-dpo-pairs.js +7 -2
  55. package/scripts/feedback-loop.js +123 -1
  56. package/scripts/feedback-quality.js +87 -0
  57. package/scripts/filesystem-search.js +35 -10
  58. package/scripts/gate-stats.js +89 -0
  59. package/scripts/gates-engine.js +1176 -85
  60. package/scripts/gemini-embedding-policy.js +2 -1
  61. package/scripts/hook-runtime.js +20 -14
  62. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  63. package/scripts/hybrid-feedback-context.js +142 -7
  64. package/scripts/lesson-inference.js +8 -3
  65. package/scripts/lesson-search.js +17 -1
  66. package/scripts/license.js +10 -10
  67. package/scripts/llm-client.js +169 -4
  68. package/scripts/local-model-profile.js +15 -8
  69. package/scripts/mcp-config.js +7 -1
  70. package/scripts/memory-scope-readiness.js +159 -0
  71. package/scripts/meta-agent-loop.js +36 -0
  72. package/scripts/operational-integrity.js +39 -5
  73. package/scripts/oss-pr-opportunity-scout.js +35 -5
  74. package/scripts/plausible-server-events.js +9 -6
  75. package/scripts/pro-local-dashboard.js +4 -4
  76. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  77. package/scripts/published-cli.js +0 -8
  78. package/scripts/rate-limiter.js +64 -13
  79. package/scripts/secret-scanner.js +44 -5
  80. package/scripts/security-scanner.js +260 -10
  81. package/scripts/self-distill-agent.js +3 -1
  82. package/scripts/seo-gsd.js +916 -7
  83. package/scripts/statusline-cache-path.js +17 -2
  84. package/scripts/statusline-local-stats.js +9 -1
  85. package/scripts/statusline-meta.js +28 -2
  86. package/scripts/statusline.sh +20 -4
  87. package/scripts/telemetry-analytics.js +357 -0
  88. package/scripts/thompson-sampling.js +31 -10
  89. package/scripts/thumbgate-bench.js +16 -1
  90. package/scripts/thumbgate-search.js +85 -19
  91. package/scripts/tool-registry.js +169 -1
  92. package/scripts/vector-store.js +45 -0
  93. package/scripts/workflow-sentinel.js +286 -53
  94. package/scripts/workspace-evolver.js +62 -2
  95. package/src/api/server.js +2683 -319
  96. 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.',
@@ -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 resolveCliBaseCommand() {
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 resolveCodexCliBaseCommand() {
53
+ function resolveCodexCliCommand(subcommand) {
48
54
  const version = packageVersion();
49
55
  if (publishedHookCommandsAvailable(version)) {
50
- return publishedCliShellCommand('latest', [], { preferInstalled: false });
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', [], { preferInstalled: false });
61
+ return publishedCliShellCommand('latest', [subcommand]);
56
62
  }
57
63
 
58
64
  function buildPortableHookCommand(subcommand) {
59
- return `${resolveCliBaseCommand()} ${subcommand}`;
65
+ return resolveCliCommand(subcommand);
60
66
  }
61
67
 
62
68
  function buildCodexPortableHookCommand(subcommand) {
63
- return `${resolveCodexCliBaseCommand()} ${subcommand}`;
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
- resolveCodexCliBaseCommand,
119
- resolveCliBaseCommand,
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 path = require('path');
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
- return normalize(stripFeedbackPrefix(text));
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
- const toolName = inferToolName(entry.toolName || entry.tool_name || 'unknown', entry.context || '');
217
- toolNegatives[toolName] = (toolNegatives[toolName] || 0) + 1;
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.context || '',
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.context || '',
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(lesson) + '\n');
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(lesson, null, 2) + '\n');
151
+ fs.writeFileSync(recentPath, JSON.stringify(redactedLesson, null, 2) + '\n');
147
152
 
148
- return lesson;
153
+ return redactedLesson;
149
154
  }
150
155
 
151
156
  /**
@@ -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
  }
@@ -23,15 +23,17 @@ function isValidKey(key) {
23
23
  }
24
24
 
25
25
  function verifyLicense(options = {}) {
26
- const envKey = [
27
- process.env.THUMBGATE_API_KEY,
28
- process.env.THUMBGATE_PRO_KEY,
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
- .map(([, value]) => value),
32
- ].find((value) => isValidKey(value));
33
- if (isValidKey(envKey)) {
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,
@@ -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 _client = null;
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 (_client) return _client;
25
+ if (_anthropicClient) return _anthropicClient;
23
26
  if (!isAvailable()) return null;
24
27
  try {
25
28
  const Anthropic = require('@anthropic-ai/sdk');
26
- _client = new Anthropic();
27
- return _client;
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,