thumbgate 1.27.17 → 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 (107) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +33 -5
  5. package/adapters/chatgpt/openapi.yaml +1707 -0
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/codex/config.toml +2 -2
  8. package/adapters/mcp/server-stdio.js +1 -1
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bin/cli.js +63 -54
  11. package/bin/postinstall.js +23 -14
  12. package/config/post-deploy-marketing-pages.json +19 -0
  13. package/config/tessl-tiles.json +1 -3
  14. package/package.json +99 -229
  15. package/public/guide.html +0 -4
  16. package/public/index.html +28 -49
  17. package/public/learn.html +0 -8
  18. package/public/numbers.html +2 -2
  19. package/scripts/agent-readiness.js +0 -147
  20. package/scripts/bot-detection.js +61 -3
  21. package/scripts/cli-feedback.js +4 -14
  22. package/scripts/cli-schema.js +7 -0
  23. package/scripts/durability/step.js +3 -3
  24. package/scripts/feedback-loop.js +10 -32
  25. package/scripts/gates-engine.js +72 -1
  26. package/scripts/hook-runtime.js +2 -2
  27. package/scripts/license.js +10 -9
  28. package/scripts/llm-client.js +44 -260
  29. package/scripts/mcp-config.js +7 -1
  30. package/scripts/published-cli.js +0 -8
  31. package/scripts/seo-gsd.js +0 -5
  32. package/src/api/server.js +111 -35
  33. package/.claude/commands/dashboard.md +0 -15
  34. package/.claude/commands/thumbgate-blocked.md +0 -27
  35. package/.claude/commands/thumbgate-dashboard.md +0 -15
  36. package/.claude/commands/thumbgate-doctor.md +0 -30
  37. package/.claude/commands/thumbgate-guard.md +0 -36
  38. package/.claude/commands/thumbgate-protect.md +0 -30
  39. package/.claude/commands/thumbgate-rules.md +0 -30
  40. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  41. package/adapters/letta/README.md +0 -41
  42. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  43. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  44. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  45. package/bench/observability-eval-suite.json +0 -26
  46. package/bin/dashboard-cli.js +0 -7
  47. package/commands/dashboard.md +0 -15
  48. package/commands/thumbgate-dashboard.md +0 -15
  49. package/config/builtin-lessons.json +0 -23
  50. package/public/about.html +0 -162
  51. package/public/agent-manager.html +0 -179
  52. package/public/agents-cost-savings.html +0 -153
  53. package/public/ai-malpractice-prevention.html +0 -818
  54. package/public/assets/brand/github-social-preview.png +0 -0
  55. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  56. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  57. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  58. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  59. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  60. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  61. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  62. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  63. package/public/assets/brand/thumbgate-mark.svg +0 -21
  64. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  65. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  66. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  67. package/public/assets/legal-intake-control-flow.svg +0 -66
  68. package/public/brand/thumbgate-mark.svg +0 -19
  69. package/public/brand/thumbgate-og.svg +0 -16
  70. package/public/chatgpt-app.html +0 -330
  71. package/public/codex-enterprise.html +0 -123
  72. package/public/js/buyer-intent.js +0 -672
  73. package/public/pricing.html +0 -399
  74. package/scripts/action-receipts.js +0 -324
  75. package/scripts/activation-quickstart.js +0 -187
  76. package/scripts/agent-operations-planner.js +0 -621
  77. package/scripts/ai-component-inventory.js +0 -367
  78. package/scripts/async-eval-observability.js +0 -236
  79. package/scripts/audit.js +0 -65
  80. package/scripts/aws-blocks-guardrails.js +0 -228
  81. package/scripts/bot-detector.js +0 -50
  82. package/scripts/classifier-routing.js +0 -130
  83. package/scripts/dashboard-chat.js +0 -332
  84. package/scripts/feedback-aggregate.js +0 -281
  85. package/scripts/feedback-sanitizer.js +0 -105
  86. package/scripts/hob-pack.js +0 -591
  87. package/scripts/hook-stop-anti-claim.js +0 -301
  88. package/scripts/install-shim.js +0 -87
  89. package/scripts/mcp-oauth.js +0 -293
  90. package/scripts/noop-detect.js +0 -285
  91. package/scripts/omlx-smoke.js +0 -192
  92. package/scripts/parallel-workflow-orchestrator.js +0 -293
  93. package/scripts/plan-gate.js +0 -243
  94. package/scripts/plausible-domain-config.js +0 -99
  95. package/scripts/qa-scenario-planner.js +0 -136
  96. package/scripts/repeat-metric.js +0 -137
  97. package/scripts/secret-fixture-tokens.js +0 -61
  98. package/scripts/secret-redaction.js +0 -166
  99. package/scripts/self-harness-optimizer.js +0 -141
  100. package/scripts/self-healing-check.js +0 -193
  101. package/scripts/silent-failure-cluster.js +0 -531
  102. package/scripts/statusline-cache-read.js +0 -57
  103. package/scripts/sync-telemetry-from-prod.js +0 -374
  104. package/scripts/tool-contract-validator.js +0 -76
  105. package/scripts/trajectory-scorer.js +0 -63
  106. package/scripts/verify-marketing-pages-deployed.js +0 -195
  107. package/scripts/visitor-journey.js +0 -172
@@ -1163,9 +1163,47 @@ function isThreadResolutionEvidenceAction(toolName, toolInput = {}) {
1163
1163
  return /\b(?:gate-satisfy|satisfy_gate|track_action|gh\s+pr\s+(?:view|checks|status)|gh\s+api\b.*(?:reviewThreads|reviews|comments|threads)|git\s+(?:status|diff|show))\b/i.test(command);
1164
1164
  }
1165
1165
 
1166
+ // Read-only observability/metrics MCP tools must NEVER be blocked by the pending
1167
+ // PR-thread-resolution gate. Reading revenue, the dashboard, gate stats, or a
1168
+ // semantic entity cannot advance a "done" claim or mutate any state, so gating it
1169
+ // only blinds the operator to their own numbers. This is the exact failure the CEO
1170
+ // hit on 2026-06-30: `get_business_metrics` denied with "a git commit was made on a
1171
+ // PR branch" — a governance gate eating the observability path. The exempt set is
1172
+ // sourced from the canonical `readonly` MCP profile (config/mcp-allowlists.json) so
1173
+ // it cannot drift from the product's own definition of "safe to read"; the hard-coded
1174
+ // fallback guarantees the core observability tools stay readable even if that policy
1175
+ // file is unreadable at runtime.
1176
+ const READ_ONLY_TOOL_FALLBACK = new Set([
1177
+ 'get_business_metrics', 'describe_semantic_entity', 'describe_reliability_entity',
1178
+ 'get_reliability_rules', 'dashboard', 'org_dashboard', 'gate_stats', 'feedback_stats',
1179
+ 'feedback_summary', 'session_report', 'generate_operator_artifact', 'settings_status',
1180
+ 'get_scope_state', 'get_branch_governance', 'context_provenance', 'native_messaging_audit',
1181
+ 'list_harnesses', 'list_intents', 'list_imported_documents', 'get_imported_document',
1182
+ 'check_operational_integrity', 'workflow_sentinel', 'recall', 'search_lessons',
1183
+ 'retrieve_lessons', 'search_thumbgate', 'unified_context', 'verify_claim',
1184
+ ]);
1185
+ let readOnlyToolCache = null;
1186
+ function getReadOnlyToolNames() {
1187
+ if (readOnlyToolCache) return readOnlyToolCache;
1188
+ const names = new Set(READ_ONLY_TOOL_FALLBACK);
1189
+ try {
1190
+ const { getAllowedTools } = require('./mcp-policy');
1191
+ for (const tool of getAllowedTools('readonly')) names.add(tool);
1192
+ } catch (_) {
1193
+ // Policy file missing/malformed — the fallback set still covers the core
1194
+ // observability tools, so the operator can always read state.
1195
+ }
1196
+ readOnlyToolCache = names;
1197
+ return names;
1198
+ }
1199
+ function isReadOnlyObservabilityTool(toolName) {
1200
+ return Boolean(toolName) && getReadOnlyToolNames().has(toolName);
1201
+ }
1202
+
1166
1203
  function evaluatePendingPrThreadResolutionGate(toolName, toolInput = {}) {
1167
1204
  if (!hasAction(PR_THREAD_RESOLUTION_ACTION)) return null;
1168
1205
  if (isThreadResolutionSatisfied()) return null;
1206
+ if (isReadOnlyObservabilityTool(toolName)) return null;
1169
1207
  if (isThreadResolutionEvidenceAction(toolName, toolInput)) return null;
1170
1208
 
1171
1209
  const message = 'A git commit was made on a PR branch. Verify review threads are resolved before the next tool call.';
@@ -1991,6 +2029,19 @@ async function checkMetricCondition(metricCondition) {
1991
2029
  return true;
1992
2030
  }
1993
2031
 
2032
+ /**
2033
+ * Whether this run is autonomous — i.e. no human is present to resolve an
2034
+ * `approve` (human-in-the-loop) gate. Opt-in ONLY via THUMBGATE_AUTONOMOUS=1
2035
+ * (or "true"); interactive and existing CI behavior is unchanged unless an
2036
+ * operator explicitly sets it. In an autonomous agent loop an approval gate has
2037
+ * nobody to sign off, so it must fail CLOSED (deny) rather than defer forever or
2038
+ * slip through — a guardrail has to guard precisely when it is unattended.
2039
+ */
2040
+ function isAutonomousRun() {
2041
+ const raw = String(process.env.THUMBGATE_AUTONOMOUS || '').trim().toLowerCase();
2042
+ return raw === '1' || raw === 'true';
2043
+ }
2044
+
1994
2045
  async function evaluateGatesAsync(toolName, toolInput, configPath) {
1995
2046
  let config;
1996
2047
  try {
@@ -2145,6 +2196,15 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
2145
2196
  if (gate.action === 'approve') {
2146
2197
  const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
2147
2198
  if (approvalEnabled) {
2199
+ if (isAutonomousRun()) {
2200
+ // Autonomous run: no human to approve. Fail CLOSED so the actions that
2201
+ // most need sign-off cannot slip through unattended.
2202
+ const failClosedMessage = `[autonomous run — no approver present, failing closed] ${message}`;
2203
+ recordStat(gate.id, 'block', gate, { toolName, toolInput });
2204
+ const failClosedAudit = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message: failClosedMessage, severity: gate.severity, source: 'gates-engine', autonomousFailClosed: true });
2205
+ auditToFeedback(failClosedAudit);
2206
+ return { decision: 'deny', gate: gate.id, message: failClosedMessage, severity: gate.severity, reasoning, requiresApproval: true, failedClosed: true };
2207
+ }
2148
2208
  recordStat(gate.id, 'approve', gate, { toolName, toolInput });
2149
2209
  const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
2150
2210
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
@@ -2349,6 +2409,15 @@ function evaluateGates(toolName, toolInput, configPath) {
2349
2409
  if (gate.action === 'approve') {
2350
2410
  const approvalEnabled = process.env.THUMBGATE_APPROVAL_GATES !== '0';
2351
2411
  if (approvalEnabled) {
2412
+ if (isAutonomousRun()) {
2413
+ // Autonomous run: no human to approve. Fail CLOSED so the actions that
2414
+ // most need sign-off cannot slip through unattended.
2415
+ const failClosedMessage = `[autonomous run — no approver present, failing closed] ${message}`;
2416
+ recordStat(gate.id, 'block', gate, { toolName, toolInput });
2417
+ const failClosedAudit = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message: failClosedMessage, severity: gate.severity, source: 'gates-engine', autonomousFailClosed: true });
2418
+ auditToFeedback(failClosedAudit);
2419
+ return { decision: 'deny', gate: gate.id, message: failClosedMessage, severity: gate.severity, reasoning, requiresApproval: true, failedClosed: true };
2420
+ }
2352
2421
  recordStat(gate.id, 'approve', gate, { toolName, toolInput });
2353
2422
  const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
2354
2423
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
@@ -2785,7 +2854,7 @@ function buildRecentCorrectiveActionsContext(options = {}) {
2785
2854
  function buildRelevantLessonContext(toolName, toolInput) {
2786
2855
  if (!toolName) return null;
2787
2856
 
2788
- const { retrieveRelevantLessons, calculateRetrievalEntropy, filterTopP } = loadOptionalModule("./lesson-retrieval", () => ({ retrieveRelevantLessons: () => [], calculateRetrievalEntropy: () => 0, filterTopP: (l) => l }));
2857
+ const { retrieveRelevantLessons, calculateRetrievalEntropy } = loadOptionalModule("./lesson-retrieval", () => ({ retrieveRelevantLessons: () => [], calculateRetrievalEntropy: () => 0 }));
2789
2858
 
2790
2859
  // Extract a searchable action context from the tool input
2791
2860
  const actionContext = extractActionContext(toolName, toolInput);
@@ -3294,6 +3363,7 @@ module.exports = {
3294
3363
  matchesGate,
3295
3364
  evaluateGates,
3296
3365
  evaluateGatesAsync,
3366
+ isAutonomousRun,
3297
3367
  computeExecutableHash,
3298
3368
  formatOutput,
3299
3369
  isApprovalGatesEnabled,
@@ -3335,6 +3405,7 @@ module.exports = {
3335
3405
  evaluateBoostedRiskTagGuard,
3336
3406
  registerPrThreadResolutionClaimGate,
3337
3407
  evaluatePendingPrThreadResolutionGate,
3408
+ isReadOnlyObservabilityTool,
3338
3409
  getLocalOnlyScopeSources,
3339
3410
  isRemoteSideEffectCommand,
3340
3411
  evaluateLocalOnlyRemoteSideEffectGate,
@@ -53,12 +53,12 @@ function resolveCliCommand(subcommand) {
53
53
  function resolveCodexCliCommand(subcommand) {
54
54
  const version = packageVersion();
55
55
  if (publishedHookCommandsAvailable(version)) {
56
- return publishedCliShellCommand('latest', [subcommand], { preferInstalled: false });
56
+ return publishedCliShellCommand('latest', [subcommand]);
57
57
  }
58
58
  if (isSourceCheckout(PKG_ROOT)) {
59
59
  return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
60
60
  }
61
- return publishedCliShellCommand('latest', [subcommand], { preferInstalled: false });
61
+ return publishedCliShellCommand('latest', [subcommand]);
62
62
  }
63
63
 
64
64
  function buildPortableHookCommand(subcommand) {
@@ -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
  };
@@ -4,90 +4,26 @@
4
4
  const { runStep } = require('./durability/step');
5
5
 
6
6
  const MODELS = {
7
- FAST: 'glm-5.2',
8
- SMART: 'glm-5.2',
9
- ZAI_CODING: 'glm-5.2',
10
- LEGACY_CLAUDE_FAST: 'claude-haiku-4-5-20251001',
11
- LEGACY_CLAUDE_SMART: 'claude-sonnet-4-6',
7
+ FAST: 'claude-haiku-4-5-20251001',
8
+ SMART: 'claude-sonnet-4-6',
12
9
  };
13
10
 
14
11
  const DEFAULT_MODEL = MODELS.FAST;
15
12
  const DEFAULT_MAX_TOKENS = 1024;
16
13
  const DEFAULT_CACHE_TTL = '5m';
17
- const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
18
- const DEFAULT_ZAI_MODEL = MODELS.ZAI_CODING;
19
- const DEFAULT_OMLX_BASE_URL = 'http://127.0.0.1:8000/v1';
20
- const DEFAULT_OMLX_MODEL = 'qwen3-coder-next';
14
+ const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/paas/v4';
15
+ const DEFAULT_ZAI_MODEL = 'glm-5.2-flash';
21
16
 
22
17
  let _anthropicClient = null;
23
18
  let _geminiClient = null;
24
19
 
25
- function getZaiApiKey(env = process.env) {
26
- return env.ZAI_API_KEY || env.THUMBGATE_ZAI_API_KEY || env.GLM_API_KEY || '';
27
- }
28
-
29
- function getZaiBaseUrl(env = process.env) {
30
- return (env.ZAI_API_BASE_URL || env.THUMBGATE_ZAI_API_BASE_URL || DEFAULT_ZAI_BASE_URL).replace(/\/+$/, '');
31
- }
32
-
33
- function getZaiModel(env = process.env) {
34
- return env.ZAI_API_MODEL || env.THUMBGATE_ZAI_MODEL || DEFAULT_ZAI_MODEL;
35
- }
36
-
37
- function getOmlxApiKey(env = process.env) {
38
- return env.OMLX_API_KEY || env.THUMBGATE_OMLX_API_KEY || '';
39
- }
40
-
41
- function getOmlxBaseUrl(env = process.env) {
42
- return (env.OMLX_BASE_URL || env.THUMBGATE_OMLX_BASE_URL || DEFAULT_OMLX_BASE_URL)
43
- .replace(/\/chat\/completions\/?$/i, '')
44
- .replace(/\/+$/, '');
45
- }
46
-
47
- function getOmlxModel(env = process.env) {
48
- return env.OMLX_MODEL || env.THUMBGATE_OMLX_MODEL || DEFAULT_OMLX_MODEL;
49
- }
50
-
51
- function hasZaiApiKey(env = process.env) {
52
- return Boolean(getZaiApiKey(env));
53
- }
54
-
55
- function hasOmlxConfig(env = process.env) {
56
- return env.THUMBGATE_LLM_PROVIDER === 'omlx'
57
- || env.LLM_PROVIDER === 'omlx'
58
- || env.THUMBGATE_OMLX_ENABLED === '1'
59
- || env.OMLX_ENABLED === '1'
60
- || Boolean(env.OMLX_BASE_URL || env.THUMBGATE_OMLX_BASE_URL);
61
- }
62
-
63
- function isZaiProvider(provider = '') {
64
- return /^(zai|z\.ai|glm|glm-coding)$/i.test(String(provider || '').trim());
65
- }
66
-
67
- function isOmlxProvider(provider = '') {
68
- return /^(omlx|local-omlx|mlx-local)$/i.test(String(provider || '').trim());
69
- }
70
-
71
- function isZaiModel(model = '') {
72
- return /^(glm|zai|z\.ai)/i.test(String(model || '').trim());
73
- }
74
-
75
- function isAvailable(provider = '') {
76
- const normalizedProvider = String(provider || '').trim().toLowerCase();
77
- if (isZaiProvider(normalizedProvider)) return hasZaiApiKey();
78
- if (isOmlxProvider(normalizedProvider)) return hasOmlxConfig();
79
- if (normalizedProvider === 'anthropic' || normalizedProvider === 'claude') {
80
- return Boolean(process.env.ANTHROPIC_API_KEY);
81
- }
82
- if (normalizedProvider === 'gemini' || normalizedProvider === 'vertex') {
83
- return Boolean(process.env.GEMINI_API_KEY || process.env.VERTEX_PROJECT_ID);
84
- }
85
- return Boolean(hasOmlxConfig() || hasZaiApiKey() || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY || process.env.VERTEX_PROJECT_ID);
20
+ function isAvailable() {
21
+ return Boolean(process.env.ANTHROPIC_API_KEY);
86
22
  }
87
23
 
88
24
  function getClient() {
89
25
  if (_anthropicClient) return _anthropicClient;
90
- if (!isAvailable('anthropic')) return null;
26
+ if (!isAvailable()) return null;
91
27
  try {
92
28
  const Anthropic = require('@anthropic-ai/sdk');
93
29
  _anthropicClient = new Anthropic();
@@ -189,63 +125,6 @@ function buildClaudeRequest({
189
125
  return request;
190
126
  }
191
127
 
192
- function normalizeMessageText(content) {
193
- if (typeof content === 'string') return content;
194
- if (Array.isArray(content)) {
195
- return content.map((part) => {
196
- if (typeof part === 'string') return part;
197
- if (part && typeof part === 'object') return part.text || JSON.stringify(part);
198
- return '';
199
- }).join('');
200
- }
201
- if (content && typeof content === 'object') {
202
- return content.text || JSON.stringify(content);
203
- }
204
- return '';
205
- }
206
-
207
- function convertMessagesToOpenAi(messages, userPrompt, systemPrompt) {
208
- const normalized = [];
209
- if (systemPrompt) normalized.push({ role: 'system', content: systemPrompt });
210
-
211
- const list = Array.isArray(messages) && messages.length > 0
212
- ? messages
213
- : [{ role: 'user', content: userPrompt }];
214
-
215
- for (const message of list) {
216
- if (!message || typeof message !== 'object') continue;
217
- const role = message.role === 'assistant' || message.role === 'system' ? message.role : 'user';
218
- const content = normalizeMessageText(message.content);
219
- if (content) normalized.push({ role, content });
220
- }
221
-
222
- if (normalized.length === 0) {
223
- normalized.push({ role: 'user', content: String(userPrompt || '') });
224
- }
225
-
226
- return normalized;
227
- }
228
-
229
- function buildOpenAiCompatibleRequest({
230
- systemPrompt,
231
- userPrompt,
232
- messages,
233
- model,
234
- maxTokens,
235
- temperature,
236
- metadata,
237
- } = {}, env = process.env) {
238
- const request = {
239
- model: model || getZaiModel(env),
240
- messages: convertMessagesToOpenAi(messages, userPrompt, systemPrompt),
241
- max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
242
- };
243
-
244
- if (Number.isFinite(temperature)) request.temperature = temperature;
245
- if (metadata && typeof metadata === 'object') request.metadata = metadata;
246
- return request;
247
- }
248
-
249
128
  function extractTextContent(response) {
250
129
  return (response?.content || [])
251
130
  .filter((block) => block.type === 'text')
@@ -262,92 +141,54 @@ function parseClaudeJson(text) {
262
141
  }
263
142
  }
264
143
 
265
- async function callZaiInternal(options = {}) {
266
- const apiKey = getZaiApiKey();
267
- if (!apiKey) return null;
268
- if (typeof fetch !== 'function') return null;
269
-
270
- const baseUrl = getZaiBaseUrl();
271
- const requestedModel = options.model && isZaiModel(options.model) ? options.model : undefined;
272
- const request = buildOpenAiCompatibleRequest({
273
- ...options,
274
- model: requestedModel || getZaiModel(),
275
- });
276
-
277
- try {
278
- const response = await runStep('llm.callZai', {
279
- retries: 2,
280
- logger: (msg) => console.warn(msg),
281
- }, async () => fetch(`${baseUrl}/chat/completions`, {
282
- method: 'POST',
283
- headers: {
284
- Authorization: `Bearer ${apiKey}`,
285
- 'Content-Type': 'application/json',
286
- },
287
- body: JSON.stringify(request),
288
- }));
144
+ function getZaiApiKey(env = process.env) {
145
+ return env.ZAI_API_KEY || env.THUMBGATE_ZAI_API_KEY || '';
146
+ }
289
147
 
290
- if (!response || !response.ok) {
291
- throw new Error(`Z.ai request failed with status ${response?.status || 'unknown'}`);
292
- }
148
+ function getZaiBaseUrl(env = process.env) {
149
+ return env.ZAI_BASE_URL || env.THUMBGATE_ZAI_BASE_URL || DEFAULT_ZAI_BASE_URL;
150
+ }
293
151
 
294
- const json = await response.json();
295
- const text = stripCodeFences(json?.choices?.[0]?.message?.content || '');
296
- return {
297
- text,
298
- usage: json?.usage || null,
299
- stopReason: json?.choices?.[0]?.finish_reason || null,
300
- id: json?.id || null,
301
- model: json?.model || request.model,
302
- provider: 'zai',
303
- };
304
- } catch (err) {
305
- console.error('Z.ai execution error:', err?.message || err);
306
- return null;
307
- }
152
+ function getZaiModel(env = process.env) {
153
+ return env.ZAI_API_MODEL || env.THUMBGATE_ZAI_MODEL || DEFAULT_ZAI_MODEL;
308
154
  }
309
155
 
310
- async function callOmlxInternal(options = {}) {
311
- if (!hasOmlxConfig()) return null;
312
- if (typeof fetch !== 'function') return null;
156
+ async function callZaiInternal(options = {}, env = process.env) {
157
+ const apiKey = getZaiApiKey(env);
158
+ if (!apiKey || typeof fetch !== 'function') return null;
313
159
 
314
- const baseUrl = getOmlxBaseUrl();
315
- const request = buildOpenAiCompatibleRequest({
316
- ...options,
317
- model: options.model || getOmlxModel(),
318
- });
319
- const apiKey = getOmlxApiKey();
320
- const headers = {
321
- 'Content-Type': 'application/json',
322
- };
323
- if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
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
+ ];
324
166
 
325
167
  try {
326
- const response = await runStep('llm.callOmlx', {
327
- retries: 1,
328
- logger: (msg) => console.warn(msg),
329
- }, async () => fetch(`${baseUrl}/chat/completions`, {
168
+ const response = await fetch(`${getZaiBaseUrl(env).replace(/\/$/, '')}/chat/completions`, {
330
169
  method: 'POST',
331
- headers,
332
- body: JSON.stringify(request),
333
- }));
334
-
335
- if (!response || !response.ok) {
336
- throw new Error(`oMLX request failed with status ${response?.status || 'unknown'}`);
337
- }
338
-
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;
339
183
  const json = await response.json();
340
- const text = stripCodeFences(json?.choices?.[0]?.message?.content || '');
341
184
  return {
342
- text,
185
+ text: stripCodeFences(json?.choices?.[0]?.message?.content || ''),
343
186
  usage: json?.usage || null,
344
187
  stopReason: json?.choices?.[0]?.finish_reason || null,
345
188
  id: json?.id || null,
346
- model: json?.model || request.model,
347
- provider: 'omlx',
189
+ model: json?.model || options.model || getZaiModel(env),
348
190
  };
349
- } catch (err) {
350
- console.error('oMLX execution error:', err?.message || err);
191
+ } catch {
351
192
  return null;
352
193
  }
353
194
  }
@@ -434,16 +275,6 @@ function convertMessagesToGemini(messages, userPrompt) {
434
275
 
435
276
  async function callClaudeInternal(options = {}) {
436
277
  const modelName = options.model || '';
437
- const provider = options.provider || process.env.THUMBGATE_LLM_PROVIDER || process.env.LLM_PROVIDER || '';
438
- const requestedZai = isZaiProvider(provider) || isZaiModel(modelName);
439
- if (isOmlxProvider(provider)) {
440
- return callOmlxInternal(options);
441
- }
442
- if (requestedZai || (!process.env.ANTHROPIC_API_KEY && hasZaiApiKey() && !modelName.startsWith('gemini') && !modelName.startsWith('vertex'))) {
443
- const zaiOptions = requestedZai ? options : { ...options, model: undefined };
444
- return callZaiInternal(zaiOptions);
445
- }
446
-
447
278
  if (modelName.startsWith('gemini') || modelName.startsWith('vertex')) {
448
279
  return callGeminiInternal(options);
449
280
  }
@@ -496,19 +327,12 @@ async function callClaudeJson(options = {}) {
496
327
  stopReason: result.stopReason,
497
328
  id: result.id,
498
329
  model: result.model,
499
- provider: result.provider || 'anthropic',
500
330
  };
501
331
  }
502
332
 
503
333
  return parsed;
504
334
  }
505
335
 
506
- async function callZai(options = {}) {
507
- const result = await callZaiInternal(options);
508
- if (!result) return null;
509
- return options.returnMetadata ? result : result.text;
510
- }
511
-
512
336
  async function callZaiJson(options = {}) {
513
337
  const result = await callZaiInternal(options);
514
338
  if (!result) return null;
@@ -524,35 +348,6 @@ async function callZaiJson(options = {}) {
524
348
  stopReason: result.stopReason,
525
349
  id: result.id,
526
350
  model: result.model,
527
- provider: result.provider,
528
- };
529
- }
530
-
531
- return parsed;
532
- }
533
-
534
- async function callOmlx(options = {}) {
535
- const result = await callOmlxInternal(options);
536
- if (!result) return null;
537
- return options.returnMetadata ? result : result.text;
538
- }
539
-
540
- async function callOmlxJson(options = {}) {
541
- const result = await callOmlxInternal(options);
542
- if (!result) return null;
543
-
544
- const parsed = parseClaudeJson(result.text);
545
- if (parsed === null) return null;
546
-
547
- if (options.returnMetadata) {
548
- return {
549
- parsed,
550
- text: result.text,
551
- usage: result.usage,
552
- stopReason: result.stopReason,
553
- id: result.id,
554
- model: result.model,
555
- provider: result.provider,
556
351
  };
557
352
  }
558
353
 
@@ -563,24 +358,13 @@ module.exports = {
563
358
  isAvailable,
564
359
  callClaude,
565
360
  callClaudeJson,
566
- callZai,
567
361
  callZaiJson,
568
- callOmlx,
569
- callOmlxJson,
362
+ getZaiApiKey,
363
+ getZaiBaseUrl,
364
+ getZaiModel,
570
365
  stripCodeFences,
571
366
  parseClaudeJson,
572
367
  normalizeCacheOptions,
573
368
  buildClaudeRequest,
574
- buildOpenAiCompatibleRequest,
575
- convertMessagesToOpenAi,
576
- getZaiApiKey,
577
- getZaiBaseUrl,
578
- getZaiModel,
579
- getOmlxApiKey,
580
- getOmlxBaseUrl,
581
- getOmlxModel,
582
369
  MODELS,
583
- DEFAULT_ZAI_BASE_URL,
584
- DEFAULT_OMLX_BASE_URL,
585
- DEFAULT_OMLX_MODEL,
586
370
  };
@@ -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
 
@@ -37,14 +37,6 @@ function publishedCliShellCommand(pkgVersion, commandArgs = [], options = {}) {
37
37
  const escapedArgs = commandArgs.map(shellQuote).join(' ');
38
38
  const fastPath = `[ -x ${shellQuote(runtimeBin)} ] && exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`;
39
39
  const installPath = `mkdir -p ${shellQuote(prefixDir)} && exec npm ${publishedCliArgs(pkgVersion, commandArgs, { prefixDir }).map(shellQuote).join(' ')}`;
40
- if (options.preferInstalled === false) {
41
- const packageSpec = `thumbgate@${pkgVersion}`;
42
- return [
43
- `mkdir -p ${shellQuote(prefixDir)}`,
44
- `npm "install" "--prefix" ${shellQuote(prefixDir)} "--no-save" "--omit=dev" ${shellQuote(packageSpec)} >/dev/null 2>&1`,
45
- `exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`,
46
- ].join(' && ');
47
- }
48
40
  return `${fastPath} || ${installPath}`;
49
41
  }
50
42
 
@@ -12,7 +12,6 @@ const PRODUCT = {
12
12
  repoUrl: 'https://github.com/IgorGanapolsky/ThumbGate',
13
13
  homepageUrl: 'https://thumbgate.ai',
14
14
  verificationUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
15
- automationUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/proof/automation/report.json',
16
15
  sprintDiagnosticPaymentUrl: 'https://buy.stripe.com/00w14neyUcXA5pL5e33sI0e',
17
16
  workflowSprintPaymentUrl: 'https://buy.stripe.com/fZu9AT76saPsg4pbCr3sI0f',
18
17
  compatibility: ['Claude Code', 'Cursor', 'Codex', 'Gemini', 'Amp', 'OpenCode'],
@@ -3389,10 +3388,6 @@ function createPageSpec(blueprint, row) {
3389
3388
  },
3390
3389
  proofLinks: [
3391
3390
  { label: 'Verification evidence', href: PRODUCT.verificationUrl },
3392
- { label: 'Automation proof', href: PRODUCT.automationUrl },
3393
- ...(blueprint.path === '/guides/agent-harness-optimization'
3394
- ? [{ label: 'Harness proof', href: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/proof/harnesses-report.json' }]
3395
- : []),
3396
3391
  { label: 'GitHub repository', href: PRODUCT.repoUrl },
3397
3392
  ],
3398
3393
  changefreq: blueprint.pageType === 'comparison' ? 'weekly' : 'monthly',