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.
- package/.claude-plugin/marketplace.json +85 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +33 -5
- package/adapters/chatgpt/openapi.yaml +1707 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +63 -54
- package/bin/postinstall.js +23 -14
- package/config/post-deploy-marketing-pages.json +19 -0
- package/config/tessl-tiles.json +1 -3
- package/package.json +99 -229
- package/public/guide.html +0 -4
- package/public/index.html +28 -49
- package/public/learn.html +0 -8
- package/public/numbers.html +2 -2
- package/scripts/agent-readiness.js +0 -147
- package/scripts/bot-detection.js +61 -3
- package/scripts/cli-feedback.js +4 -14
- package/scripts/cli-schema.js +7 -0
- package/scripts/durability/step.js +3 -3
- package/scripts/feedback-loop.js +10 -32
- package/scripts/gates-engine.js +72 -1
- package/scripts/hook-runtime.js +2 -2
- package/scripts/license.js +10 -9
- package/scripts/llm-client.js +44 -260
- package/scripts/mcp-config.js +7 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/seo-gsd.js +0 -5
- package/src/api/server.js +111 -35
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/bot-detector.js +0 -50
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
package/scripts/gates-engine.js
CHANGED
|
@@ -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
|
|
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,
|
package/scripts/hook-runtime.js
CHANGED
|
@@ -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]
|
|
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]
|
|
61
|
+
return publishedCliShellCommand('latest', [subcommand]);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function buildPortableHookCommand(subcommand) {
|
package/scripts/license.js
CHANGED
|
@@ -23,15 +23,17 @@ function isValidKey(key) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function verifyLicense(options = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Only ThumbGate's own env vars are license candidates — scanning foreign
|
|
27
|
+
// *_API_KEY / *_PRO_KEY vars would treat another vendor's secret as a
|
|
28
|
+
// license key. The result object never carries the raw key value.
|
|
29
|
+
const envEntry = [
|
|
30
|
+
['THUMBGATE_API_KEY', process.env.THUMBGATE_API_KEY],
|
|
31
|
+
['THUMBGATE_PRO_KEY', process.env.THUMBGATE_PRO_KEY],
|
|
29
32
|
...Object.entries(process.env)
|
|
30
|
-
.filter(([name]) => /(?:_API_KEY|_PRO_KEY)$/.test(name))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { valid: true, source: 'env', key: envKey };
|
|
33
|
+
.filter(([name]) => name.startsWith('THUMBGATE_') && /(?:_API_KEY|_PRO_KEY)$/.test(name)),
|
|
34
|
+
].find(([, value]) => isValidKey(value));
|
|
35
|
+
if (envEntry) {
|
|
36
|
+
return { valid: true, source: 'env', envVar: envEntry[0] };
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const licensePath = getLicensePath(options.homeDir);
|
|
@@ -42,7 +44,6 @@ function verifyLicense(options = {}) {
|
|
|
42
44
|
return {
|
|
43
45
|
valid: true,
|
|
44
46
|
source: 'file',
|
|
45
|
-
key: data.key,
|
|
46
47
|
activatedAt: data.activatedAt,
|
|
47
48
|
path: licensePath,
|
|
48
49
|
};
|
package/scripts/llm-client.js
CHANGED
|
@@ -4,90 +4,26 @@
|
|
|
4
4
|
const { runStep } = require('./durability/step');
|
|
5
5
|
|
|
6
6
|
const MODELS = {
|
|
7
|
-
FAST: '
|
|
8
|
-
SMART: '
|
|
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/
|
|
18
|
-
const DEFAULT_ZAI_MODEL =
|
|
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
|
|
26
|
-
return
|
|
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(
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
|
311
|
-
|
|
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
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 ||
|
|
347
|
-
provider: 'omlx',
|
|
189
|
+
model: json?.model || options.model || getZaiModel(env),
|
|
348
190
|
};
|
|
349
|
-
} catch
|
|
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
|
-
|
|
569
|
-
|
|
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
|
};
|
package/scripts/mcp-config.js
CHANGED
|
@@ -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
|
|
118
|
+
args: ['-lc', publishedCliShellCommand('latest', commandArgs)],
|
|
113
119
|
};
|
|
114
120
|
}
|
|
115
121
|
|
package/scripts/published-cli.js
CHANGED
|
@@ -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
|
|
package/scripts/seo-gsd.js
CHANGED
|
@@ -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',
|