thumbgate 1.27.18 → 1.27.20

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 (170) 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 +4 -3
  9. package/.well-known/agentic-verify.txt +1 -0
  10. package/.well-known/llms.txt +33 -12
  11. package/.well-known/mcp/server-card.json +8 -8
  12. package/README.md +246 -30
  13. package/adapters/claude/.mcp.json +2 -2
  14. package/adapters/codex/config.toml +2 -2
  15. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  16. package/adapters/gemini/function-declarations.json +1 -0
  17. package/adapters/letta/README.md +41 -0
  18. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  19. package/adapters/mcp/server-stdio.js +263 -11
  20. package/adapters/opencode/opencode.json +1 -1
  21. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  22. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  23. package/bench/observability-eval-suite.json +26 -0
  24. package/bench/thumbgate-bench.json +2 -2
  25. package/bin/cli.js +1456 -122
  26. package/bin/dashboard-cli.js +7 -0
  27. package/bin/postinstall.js +11 -27
  28. package/commands/dashboard.md +15 -0
  29. package/commands/thumbgate-dashboard.md +15 -0
  30. package/config/gate-classifier-routing.json +98 -0
  31. package/config/gate-templates.json +216 -0
  32. package/config/gates/claim-verification.json +12 -0
  33. package/config/gates/default.json +31 -2
  34. package/config/github-about.json +2 -2
  35. package/config/mcp-allowlists.json +23 -13
  36. package/config/merge-quality-checks.json +0 -1
  37. package/config/model-candidates.json +121 -6
  38. package/config/post-deploy-marketing-pages.json +80 -0
  39. package/config/tessl-tiles.json +1 -3
  40. package/openapi/openapi.yaml +12 -0
  41. package/package.json +225 -100
  42. package/public/about.html +162 -0
  43. package/public/agent-manager.html +179 -0
  44. package/public/agents-cost-savings.html +153 -0
  45. package/public/ai-malpractice-prevention.html +818 -0
  46. package/public/assets/brand/github-social-preview.png +0 -0
  47. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  48. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  49. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  50. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  51. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  52. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  53. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  54. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  55. package/public/assets/brand/thumbgate-mark.svg +21 -0
  56. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  57. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  58. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  59. package/public/assets/legal-intake-control-flow.svg +66 -0
  60. package/public/blog.html +4 -4
  61. package/public/brand/thumbgate-mark.svg +19 -0
  62. package/public/brand/thumbgate-og.svg +16 -0
  63. package/public/chatgpt-app.html +330 -0
  64. package/public/codex-enterprise.html +123 -0
  65. package/public/codex-plugin.html +72 -20
  66. package/public/compare.html +31 -8
  67. package/public/dashboard.html +930 -166
  68. package/public/diagnostic.html +345 -0
  69. package/public/federal.html +2 -2
  70. package/public/guide.html +33 -13
  71. package/public/index.html +469 -111
  72. package/public/install.html +193 -0
  73. package/public/js/buyer-intent.js +672 -0
  74. package/public/learn.html +183 -18
  75. package/public/lessons.html +168 -10
  76. package/public/numbers.html +7 -7
  77. package/public/pricing.html +399 -0
  78. package/public/pro.html +34 -11
  79. package/scripts/action-receipts.js +324 -0
  80. package/scripts/activation-quickstart.js +187 -0
  81. package/scripts/agent-memory-lifecycle.js +211 -0
  82. package/scripts/agent-operations-planner.js +621 -0
  83. package/scripts/agent-readiness.js +20 -3
  84. package/scripts/agent-reward-model.js +53 -1
  85. package/scripts/ai-component-inventory.js +367 -0
  86. package/scripts/async-eval-observability.js +236 -0
  87. package/scripts/audit.js +65 -0
  88. package/scripts/auto-promote-gates.js +82 -10
  89. package/scripts/auto-wire-hooks.js +14 -0
  90. package/scripts/aws-blocks-guardrails.js +272 -0
  91. package/scripts/billing.js +93 -1
  92. package/scripts/bot-detection.js +61 -3
  93. package/scripts/build-metadata.js +50 -10
  94. package/scripts/classifier-routing.js +130 -0
  95. package/scripts/cli-feedback.js +4 -2
  96. package/scripts/cli-schema.js +97 -0
  97. package/scripts/cli-telemetry.js +6 -1
  98. package/scripts/commercial-offer.js +82 -2
  99. package/scripts/context-manager.js +74 -6
  100. package/scripts/dashboard-chat.js +332 -0
  101. package/scripts/dashboard.js +68 -2
  102. package/scripts/export-databricks-bundle.js +5 -1
  103. package/scripts/export-dpo-pairs.js +7 -2
  104. package/scripts/feedback-aggregate.js +281 -0
  105. package/scripts/feedback-loop.js +123 -1
  106. package/scripts/feedback-quality.js +87 -0
  107. package/scripts/feedback-sanitizer.js +105 -0
  108. package/scripts/filesystem-search.js +35 -10
  109. package/scripts/gate-stats.js +89 -0
  110. package/scripts/gates-engine.js +1176 -85
  111. package/scripts/gemini-embedding-policy.js +2 -1
  112. package/scripts/hook-runtime.js +20 -14
  113. package/scripts/hook-stop-anti-claim.js +301 -0
  114. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  115. package/scripts/hybrid-feedback-context.js +142 -7
  116. package/scripts/install-shim.js +87 -0
  117. package/scripts/lesson-inference.js +8 -3
  118. package/scripts/lesson-search.js +17 -1
  119. package/scripts/license.js +10 -10
  120. package/scripts/llm-client.js +169 -4
  121. package/scripts/local-model-profile.js +15 -8
  122. package/scripts/mcp-config.js +7 -1
  123. package/scripts/mcp-oauth.js +293 -0
  124. package/scripts/memory-scope-readiness.js +159 -0
  125. package/scripts/meta-agent-loop.js +36 -0
  126. package/scripts/noop-detect.js +285 -0
  127. package/scripts/operational-integrity.js +39 -5
  128. package/scripts/oss-pr-opportunity-scout.js +35 -5
  129. package/scripts/parallel-workflow-orchestrator.js +293 -0
  130. package/scripts/plan-gate.js +243 -0
  131. package/scripts/plausible-domain-config.js +99 -0
  132. package/scripts/plausible-server-events.js +9 -6
  133. package/scripts/pro-local-dashboard.js +4 -4
  134. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  135. package/scripts/published-cli.js +0 -8
  136. package/scripts/qa-scenario-planner.js +136 -0
  137. package/scripts/rate-limiter.js +64 -13
  138. package/scripts/repeat-metric.js +137 -0
  139. package/scripts/secret-fixture-tokens.js +61 -0
  140. package/scripts/secret-redaction.js +166 -0
  141. package/scripts/secret-scanner.js +44 -5
  142. package/scripts/security-scanner.js +260 -10
  143. package/scripts/self-distill-agent.js +3 -1
  144. package/scripts/self-harness-optimizer.js +141 -0
  145. package/scripts/self-healing-check.js +193 -0
  146. package/scripts/self-protection.js +90 -0
  147. package/scripts/seo-gsd.js +916 -7
  148. package/scripts/silent-failure-cluster.js +531 -0
  149. package/scripts/statusline-cache-path.js +17 -2
  150. package/scripts/statusline-cache-read.js +57 -0
  151. package/scripts/statusline-local-stats.js +9 -1
  152. package/scripts/statusline-meta.js +28 -2
  153. package/scripts/statusline.sh +20 -4
  154. package/scripts/sync-telemetry-from-prod.js +374 -0
  155. package/scripts/telemetry-analytics.js +357 -0
  156. package/scripts/thompson-sampling.js +31 -10
  157. package/scripts/thumbgate-bench.js +16 -1
  158. package/scripts/thumbgate-search.js +85 -19
  159. package/scripts/tool-contract-validator.js +76 -0
  160. package/scripts/tool-registry.js +169 -1
  161. package/scripts/trajectory-scorer.js +63 -0
  162. package/scripts/vector-store.js +45 -0
  163. package/scripts/verify-marketing-pages-deployed.js +212 -0
  164. package/scripts/visitor-journey.js +172 -0
  165. package/scripts/workflow-sentinel.js +286 -53
  166. package/scripts/workspace-evolver.js +62 -2
  167. package/src/api/server.js +2683 -319
  168. package/.claude-plugin/marketplace.json +0 -85
  169. package/adapters/chatgpt/openapi.yaml +0 -1695
  170. package/scripts/bot-detector.js +0 -50
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * install-shim.js — Install a stable shim at ~/.thumbgate/bin/thumbgate-hook
5
+ *
6
+ * The shim is a tiny shell script that resolves the cached ThumbGate runtime
7
+ * first, so hook commands in settings.local.json stay stable across projects
8
+ * and agent restarts.
9
+ *
10
+ * The shim checks for a cached runtime binary first (fast path), and falls
11
+ * back to `npx --yes thumbgate@latest` (slow path, first-time self-install).
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ const SHIM_DIR = path.join(os.homedir(), '.thumbgate', 'bin');
19
+ const SHIM_PATH = path.join(SHIM_DIR, 'thumbgate-hook');
20
+ const RUNTIME_BIN = path.join(os.homedir(), '.thumbgate', 'runtime', 'node_modules', '.bin', 'thumbgate');
21
+
22
+ /**
23
+ * The shim script. Key design choices:
24
+ * - Uses `exec` to replace the shell process (no zombie processes)
25
+ * - Fast path: if cached runtime binary exists, exec it directly
26
+ * - Slow path: npx --yes thumbgate@latest (first-time auto-installs)
27
+ * - No default self-mutation: background upgrades are opt-in via
28
+ * THUMBGATE_SHIM_AUTO_UPDATE=1 so source checkouts, enterprise pins, and
29
+ * dogfood runtimes cannot be overwritten by a hook side effect.
30
+ */
31
+ function shimContent() {
32
+ const escapedRuntimeBin = JSON.stringify(RUNTIME_BIN);
33
+ const escapedRuntimeDir = JSON.stringify(path.join(os.homedir(), '.thumbgate', 'runtime'));
34
+
35
+ return `#!/usr/bin/env bash
36
+ # ThumbGate hook shim — DO NOT EDIT
37
+ # Installed by: thumbgate init
38
+ # Purpose: stable hook entry point that runs the cached ThumbGate runtime
39
+ # Pattern: Volta-style stable shim (see https://volta.sh)
40
+
41
+ set -euo pipefail
42
+
43
+ RUNTIME_BIN=${escapedRuntimeBin}
44
+ RUNTIME_DIR=${escapedRuntimeDir}
45
+
46
+ # Fast path: cached runtime binary exists and is executable
47
+ if [ -x "$RUNTIME_BIN" ]; then
48
+ # Optional background upgrade. Disabled by default so hooks never mutate a
49
+ # source checkout, enterprise pin, or dogfood runtime behind the operator's back.
50
+ if [ "\${THUMBGATE_SHIM_AUTO_UPDATE:-0}" = "1" ]; then
51
+ ( nohup npm install --prefix "$RUNTIME_DIR" --no-save --omit=dev thumbgate@latest >/dev/null 2>&1 & ) 2>/dev/null || true
52
+ fi
53
+ exec "$RUNTIME_BIN" "$@"
54
+ fi
55
+
56
+ # Slow path: no cached binary — install + exec via npx
57
+ mkdir -p "$RUNTIME_DIR"
58
+ exec npx --yes --package thumbgate@latest -- thumbgate "$@"
59
+ `;
60
+ }
61
+
62
+ function installShim() {
63
+ fs.mkdirSync(SHIM_DIR, { recursive: true });
64
+ fs.writeFileSync(SHIM_PATH, shimContent(), { mode: 0o755 });
65
+ return SHIM_PATH;
66
+ }
67
+
68
+ function shimInstalled() {
69
+ try {
70
+ return fs.existsSync(SHIM_PATH) && (fs.statSync(SHIM_PATH).mode & 0o111) !== 0;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function shimPath() {
77
+ return SHIM_PATH;
78
+ }
79
+
80
+ module.exports = {
81
+ installShim,
82
+ shimInstalled,
83
+ shimPath,
84
+ shimContent,
85
+ SHIM_DIR,
86
+ SHIM_PATH,
87
+ };
@@ -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,
@@ -111,7 +111,8 @@ function isSparseAttentionFamily(modelFamily) {
111
111
 
112
112
  function resolveProviderMode(env = process.env) {
113
113
  const explicit = normalizeSlug(env.THUMBGATE_PROVIDER_MODE || env.THUMBGATE_MODEL_PROVIDER_MODE);
114
- if (explicit === 'local' || explicit === 'managed') return explicit;
114
+ if (explicit === 'local' || explicit === 'managed' || explicit === 'vertex') return explicit;
115
+ if (env.VERTEX_PROJECT_ID || env.VERTEX_API_ENDPOINT) return 'vertex';
115
116
  if (env.THUMBGATE_LOCAL_MODEL_FAMILY || env.THUMBGATE_LOCAL_MODEL_SERVER) return 'local';
116
117
  return 'managed';
117
118
  }
@@ -133,6 +134,7 @@ function resolveModelFamily(env = process.env) {
133
134
  }
134
135
 
135
136
  function buildBackendLabel(providerMode, modelFamily) {
137
+ if (providerMode === 'vertex') return 'Vertex AI secure cloud backend';
136
138
  if (providerMode === 'managed') return 'Managed API backend';
137
139
  if (modelFamily.startsWith('deepseek')) return 'Local DeepSeek sparse backend';
138
140
  if (modelFamily.startsWith('glm')) return 'Local GLM sparse backend';
@@ -148,14 +150,18 @@ function detectInferenceBackend(env = process.env) {
148
150
  && supportsSparseAttention
149
151
  && INDEXCACHE_SERVER_ENGINES.has(serverEngine);
150
152
  const indexCacheEnabled = indexCacheEligible && parseBoolean(env.THUMBGATE_INDEXCACHE_ENABLED, false);
151
- const id = providerMode === 'managed'
152
- ? 'managed-api'
153
- : supportsSparseAttention
154
- ? `local-${modelFamily}-sparse`
155
- : 'local-dense';
153
+ const id = providerMode === 'vertex'
154
+ ? 'vertex-api'
155
+ : providerMode === 'managed'
156
+ ? 'managed-api'
157
+ : supportsSparseAttention
158
+ ? `local-${modelFamily}-sparse`
159
+ : 'local-dense';
156
160
 
157
161
  let rationale = 'Baseline backend with no sparse-attention acceleration.';
158
- if (providerMode === 'managed') {
162
+ if (providerMode === 'vertex') {
163
+ rationale = 'Vertex AI secure cloud backend providing compliant enterprise Gemini models inside VPC boundary.';
164
+ } else if (providerMode === 'managed') {
159
165
  rationale = 'Managed API path does not expose sparse-attention kernel controls like IndexCache.';
160
166
  } else if (indexCacheEnabled) {
161
167
  rationale = `Local ${modelFamily} backend is sparse-attention capable and IndexCache-ready on ${serverEngine}.`;
@@ -336,7 +342,8 @@ function resolveModelRole(role, env) {
336
342
  const envKey = `THUMBGATE_MODEL_ROLE_${normalized.toUpperCase()}`;
337
343
  const modelFamily = resolveModelFamily(e);
338
344
  const isLocalGlm = modelFamily.startsWith('glm');
339
- const provider = isLocalGlm ? 'local' : 'gemini';
345
+ const providerMode = resolveProviderMode(e);
346
+ const provider = isLocalGlm ? 'local' : (providerMode === 'vertex' ? 'vertex' : 'gemini');
340
347
  const defaultModel = isLocalGlm ? (GLM_MODEL_ROLES[normalized] || MODEL_ROLES[normalized]) : MODEL_ROLES[normalized];
341
348
  const model = (e[envKey] && String(e[envKey]).trim()) || defaultModel;
342
349
  return { role: normalized, model, provider, envKey };
@@ -107,9 +107,15 @@ function portableMcpEntry(pkgVersion) {
107
107
  }
108
108
 
109
109
  function codexAutoUpdateCliEntry(commandArgs = []) {
110
+ // Fast-start from the installed runtime binary; only resolve @latest via npx
111
+ // when the runtime is absent (e.g. first launch). This matches the hook
112
+ // commands and must never block MCP server startup on a per-launch reinstall
113
+ // (a blocking `npm install thumbgate@latest` on every launch caused
114
+ // capture/serve timeouts on slow or offline networks — a failed install left
115
+ // the chained `exec` unreached, so the server never started).
110
116
  return {
111
117
  command: 'sh',
112
- args: ['-lc', publishedCliShellCommand('latest', commandArgs, { preferInstalled: false })],
118
+ args: ['-lc', publishedCliShellCommand('latest', commandArgs)],
113
119
  };
114
120
  }
115
121