thumbgate 1.30.0 → 1.31.0
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/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +54 -16
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +66 -8
- package/adapters/opencode/opencode.json +1 -1
- package/bench/observability-eval-suite.json +2 -2
- package/bin/cli.js +147 -31
- package/config/evals/generation-quality-golden.json +95 -0
- package/config/evals/rag-answer-quality-golden.json +91 -0
- package/config/evals/retrieval-hybrid-ablation.json +66 -0
- package/config/evals/retrieval-ranking-golden.json +522 -0
- package/config/gates/default.json +217 -50
- package/config/mcp-allowlists.json +212 -206
- package/config/model-tiers.json +7 -2
- package/glama.json +6 -0
- package/package.json +60 -8
- package/public/assets/diagrams/before-after.svg +17 -16
- package/public/assets/diagrams/hero-thumbs.svg +68 -0
- package/public/assets/diagrams/loop.svg +19 -13
- package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
- package/public/compare.html +1 -0
- package/public/dashboard.html +126 -28
- package/public/evaluations.html +1 -1
- package/public/index.html +142 -13
- package/public/numbers.html +3 -2
- package/public/pricing.html +143 -30
- package/scripts/a-plus-evidence-scorecard.js +303 -0
- package/scripts/async-eval-observability.js +36 -11
- package/scripts/audit-trail.js +37 -1
- package/scripts/auto-promote-gates.js +149 -34
- package/scripts/colbert-style-maxsim.js +236 -0
- package/scripts/cross-encoder-reranker.js +356 -126
- package/scripts/dashboard-chat.js +350 -17
- package/scripts/document-intake.js +283 -7
- package/scripts/eval-quality-suite.js +204 -0
- package/scripts/feedback-loop.js +115 -7
- package/scripts/feedback-paths.js +32 -13
- package/scripts/feedback-quality.js +53 -0
- package/scripts/filesystem-search.js +17 -7
- package/scripts/gates-engine.js +17 -0
- package/scripts/harness-tool-names.js +70 -0
- package/scripts/hook-runtime.js +10 -3
- package/scripts/lesson-db.js +16 -5
- package/scripts/lesson-embedding-index.js +67 -20
- package/scripts/lesson-embedding-maintenance.js +177 -0
- package/scripts/lesson-reranker.js +55 -9
- package/scripts/lesson-retrieval.js +305 -29
- package/scripts/lesson-search.js +22 -8
- package/scripts/llm-client.js +304 -15
- package/scripts/model-tier-router.js +593 -0
- package/scripts/pragmatic-hybrid-search.js +379 -0
- package/scripts/rag-document-pipeline.js +461 -0
- package/scripts/rag-structured-output.js +441 -0
- package/scripts/ragas-style-metrics.js +351 -0
- package/scripts/request-envelope.js +178 -0
- package/scripts/rerank-pipeline.js +370 -0
- package/scripts/rerank-quality-eval.js +155 -0
- package/scripts/retrieval-hybrid-ablation.js +120 -0
- package/scripts/retrieval-quality-tier.js +118 -0
- package/scripts/secret-scanner.js +395 -4
- package/scripts/self-distill-agent.js +7 -1
- package/scripts/self-healing-check.js +25 -0
- package/scripts/skill-packs.js +183 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/statusline-links.js +1 -1
- package/scripts/statusline.sh +8 -1
- package/scripts/telemetry-analytics.js +13 -1
- package/scripts/thumbgate-search.js +98 -6
- package/scripts/tier-budget-guard.js +186 -0
- package/scripts/tool-registry.js +46 -0
- package/scripts/vector-store.js +108 -4
- package/scripts/verify-marketing-pages-deployed.js +85 -3
- package/server.json +44 -0
- package/smithery.yaml +17 -0
- package/src/api/server.js +194 -13
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
3
|
"description": "One 👎 becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.31.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
7
7
|
"email": "ig5973700@gmail.com",
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# ThumbGate
|
|
2
2
|
|
|
3
|
+
[](https://mcptoplist.com/server/glama%2FIgorGanapolsky%2FThumbGate)
|
|
4
|
+
|
|
3
5
|
<p align="center">
|
|
4
6
|
<a href="https://thumbgate.ai">
|
|
5
7
|
<img src="public/assets/brand/thumbgate-icon-512.png" alt="ThumbGate" width="128" height="128" />
|
|
@@ -28,6 +30,19 @@ Accepted feedback is stored as local lessons. Repeated concrete failures can bec
|
|
|
28
30
|
npx thumbgate init # auto-detects the supported agent and wires its integration
|
|
29
31
|
```
|
|
30
32
|
|
|
33
|
+
### MCP / Glama / registry install (stdio)
|
|
34
|
+
|
|
35
|
+
Directories and clients that install ThumbGate as an MCP server must start **stdio MCP**, not the HTTP API:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx -y thumbgate serve
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Equivalent: `npx -y thumbgate mcp`
|
|
42
|
+
- Do **not** use `npm start` for MCP — that launches the hosted HTTP API (`src/api/server.js`), not the agent-facing stdio server.
|
|
43
|
+
- Canonical package metadata: `server.json` (`runtimeHint: npx` + `packageArguments: ["serve"]`), Smithery: `smithery.yaml`, maintainers: `glama.json`.
|
|
44
|
+
- Product name is **ThumbGate** only (npm: `thumbgate`). Retired legacy package aliases are not active product surfaces.
|
|
45
|
+
|
|
31
46
|
Works with **Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode** and MCP-compatible agents after their integration is configured. Free tier: 2 feedback captures/day (10 total) and up to 3 active auto-promoted prevention rules. [Pro: $19/mo or $149/yr](https://thumbgate.ai/checkout/pro?utm_source=github&utm_medium=readme) is the individual tier for unlimited rules, history-aware lessons, feedback sessions, a personal dashboard, and DPO export. Enterprise is custom and scoped after intake; hosted team sync and a hosted org dashboard are not in the current general-availability runtime.
|
|
32
47
|
|
|
33
48
|
[](https://github.com/IgorGanapolsky/ThumbGate/actions/workflows/ci.yml)
|
|
@@ -256,6 +271,11 @@ evaluation/observability—is maintained in
|
|
|
256
271
|
architecture exists, what can fail, how it is measured and secured, how it is
|
|
257
272
|
deployed, and what evidence is required before claiming it works.
|
|
258
273
|
|
|
274
|
+
The full framework decision and one end-to-end RAG request—tenant authorization,
|
|
275
|
+
query transformation, parent-child retrieval, hybrid fusion, reranking,
|
|
276
|
+
structured generation, evaluation, and tracing—are documented in
|
|
277
|
+
[`docs/RAG_PRODUCTION_ARCHITECTURE.md`](./docs/RAG_PRODUCTION_ARCHITECTURE.md).
|
|
278
|
+
|
|
259
279
|
### Retrieval & latency: local-first, zero network hops
|
|
260
280
|
|
|
261
281
|
ThumbGate's latency advantage is structural, not a tuned cloud cluster: there is no retrieval service and no model on the enforcement path, so the gate decision never leaves your machine.
|
|
@@ -302,22 +322,40 @@ Each recommendation ships with the benchmark commands to run next: feedback-deri
|
|
|
302
322
|
|
|
303
323
|
## Install for Your Agent
|
|
304
324
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
|
311
|
-
|
|
312
|
-
| **
|
|
313
|
-
| **
|
|
314
|
-
| **
|
|
315
|
-
| **
|
|
316
|
-
| **
|
|
317
|
-
| **
|
|
318
|
-
| **
|
|
319
|
-
|
|
320
|
-
|
|
325
|
+
Enforcement depends on what the harness lets us intercept, so the table says which
|
|
326
|
+
you get. This distinction is real: with a pre-tool hook ThumbGate stops the action
|
|
327
|
+
before it runs; over MCP only, ThumbGate answers `gate_check` and the agent decides
|
|
328
|
+
whether to obey.
|
|
329
|
+
|
|
330
|
+
| Agent | Command | Enforcement |
|
|
331
|
+
|-------|---------|-------------|
|
|
332
|
+
| **Claude Code** | `npx thumbgate init --agent claude-code` | Hard — PreToolUse hook |
|
|
333
|
+
| **Codex** | `npx thumbgate init --agent codex` | Hard — `pre_tool_use` hook |
|
|
334
|
+
| **Gemini CLI** | `npx thumbgate init --agent gemini` | Hard — PreToolUse hook |
|
|
335
|
+
| **ForgeCode** | `npx thumbgate init --agent forge` | Hard — `pre_tool_use` trigger |
|
|
336
|
+
| **Cursor** | `npx thumbgate init --agent cursor` | Advisory — MCP `gate_check` |
|
|
337
|
+
| **Cline** (Roo Code successor) | `npx thumbgate init --agent cline` | Advisory — MCP `gate_check` + `.clinerules` |
|
|
338
|
+
| **OpenCode** | `npx thumbgate init --agent opencode` | Advisory — MCP `gate_check` |
|
|
339
|
+
| **Any MCP agent** | `npx thumbgate serve` | Advisory — MCP `gate_check` |
|
|
340
|
+
| **Amp** | `npx thumbgate init --agent amp` | Feedback capture only |
|
|
341
|
+
| **Claude Desktop** | [Download extension bundle](https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-claude-desktop.mcpb) | Advisory — MCP |
|
|
342
|
+
| **VS Code / Open VSX** | [plugins/vscode-extension/README.md](plugins/vscode-extension/README.md) | See plugin README |
|
|
343
|
+
| **Antigravity-compatible** | [plugins/antigravity-extension/INSTALL.md](plugins/antigravity-extension/INSTALL.md) | See plugin README |
|
|
344
|
+
| **JetBrains** | [plugins/jetbrains-plugin/README.md](plugins/jetbrains-plugin/README.md) | See plugin README |
|
|
345
|
+
|
|
346
|
+
**Advisory means the agent can ignore it.** Harnesses without a pre-tool hook expose no
|
|
347
|
+
interception point, so ThumbGate cannot stop the call itself — it returns a verdict the
|
|
348
|
+
agent is instructed to honor. Treat advisory coverage as a strong default, not a
|
|
349
|
+
guarantee, and prefer a hard-enforcement harness for anything irreversible.
|
|
350
|
+
|
|
351
|
+
The gate is **model-agnostic**: verdicts come from deterministic policy evaluation over
|
|
352
|
+
the proposed tool call (`scripts/gates-engine.js`), never from an LLM. Swapping the model
|
|
353
|
+
behind any harness does not change what is allowed.
|
|
354
|
+
|
|
355
|
+
> **Enforcement posture:** ThumbGate ships **warn-by-default** — a matched gate is logged
|
|
356
|
+
> and surfaced, not blocked. Set `THUMBGATE_STRICT_ENFORCEMENT=1` to hard-block. The
|
|
357
|
+
> `gate_check` tool reports `warn` (never `allow`) when a gate matched but posture
|
|
358
|
+
> downgraded it, so an agent is never told a flagged action is fine.
|
|
321
359
|
|
|
322
360
|
### Install scope: machine-wide vs per-project
|
|
323
361
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.31.0", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.31.0", "thumbgate", "gate-check"]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -9,12 +9,12 @@ version: "1"
|
|
|
9
9
|
skills:
|
|
10
10
|
thumbgate-gate-check:
|
|
11
11
|
description: "ThumbGate PreToolUse gate — blocks known-bad tool calls"
|
|
12
|
-
command: "npx --yes --package thumbgate@1.
|
|
12
|
+
command: "npx --yes --package thumbgate@1.31.0 thumbgate gate-check"
|
|
13
13
|
trigger: pre_tool_use
|
|
14
14
|
|
|
15
15
|
thumbgate-feedback:
|
|
16
16
|
description: "ThumbGate feedback capture — logs user prompt context"
|
|
17
|
-
command: "npx --yes --package thumbgate@1.
|
|
17
|
+
command: "npx --yes --package thumbgate@1.31.0 thumbgate hook-auto-capture"
|
|
18
18
|
trigger: user_prompt
|
|
19
19
|
|
|
20
20
|
mcp:
|
|
@@ -23,6 +23,6 @@ mcp:
|
|
|
23
23
|
args:
|
|
24
24
|
- "--yes"
|
|
25
25
|
- "--package"
|
|
26
|
-
- "thumbgate@1.
|
|
26
|
+
- "thumbgate@1.31.0"
|
|
27
27
|
- "thumbgate"
|
|
28
28
|
- "serve"
|
|
@@ -133,9 +133,7 @@ const {
|
|
|
133
133
|
throw error;
|
|
134
134
|
},
|
|
135
135
|
}));
|
|
136
|
-
const {
|
|
137
|
-
searchThumbgate,
|
|
138
|
-
} = require('../../scripts/thumbgate-search');
|
|
136
|
+
const { searchThumbgateAsync } = require('../../scripts/thumbgate-search');
|
|
139
137
|
const {
|
|
140
138
|
buildMultimodalRetrievalPlan,
|
|
141
139
|
} = require('../../scripts/multimodal-retrieval-plan');
|
|
@@ -317,7 +315,7 @@ const {
|
|
|
317
315
|
finalizeSession: finalizeFeedbackSession,
|
|
318
316
|
} = require('../../scripts/feedback-session');
|
|
319
317
|
|
|
320
|
-
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.
|
|
318
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.31.0' };
|
|
321
319
|
const COMMERCE_CATEGORIES = [
|
|
322
320
|
'product_recommendation',
|
|
323
321
|
'brand_compliance',
|
|
@@ -971,14 +969,14 @@ async function callToolInner(name, args) {
|
|
|
971
969
|
return buildSuggestFixResponse(args);
|
|
972
970
|
case 'retrieve_lessons': {
|
|
973
971
|
// Cross-encoder reranking: retrieve more candidates, then rerank for precision
|
|
974
|
-
const {
|
|
975
|
-
|
|
972
|
+
const { retrieveWithReranking } = loadOptionalModule(path.join(__dirname, '../../scripts/cross-encoder-reranker'), () => ({
|
|
973
|
+
retrieveWithReranking: async (toolName, actionContext, options = {}) => retrieveRelevantLessons(
|
|
976
974
|
toolName,
|
|
977
975
|
actionContext,
|
|
978
976
|
{ maxResults: options.maxResults || 5 },
|
|
979
977
|
),
|
|
980
978
|
}));
|
|
981
|
-
return toTextResult(
|
|
979
|
+
return toTextResult(await retrieveWithReranking(
|
|
982
980
|
args.toolName,
|
|
983
981
|
args.actionContext || '',
|
|
984
982
|
{
|
|
@@ -987,6 +985,9 @@ async function callToolInner(name, args) {
|
|
|
987
985
|
scope: args.scope,
|
|
988
986
|
requireScope: args.requireScope === true,
|
|
989
987
|
includeShared: args.includeShared !== false,
|
|
988
|
+
metadataFilters: args.filters,
|
|
989
|
+
queryRewrite: args.queryRewrite !== false,
|
|
990
|
+
includeRetrievalMeta: args.includeRetrievalMeta === true,
|
|
990
991
|
},
|
|
991
992
|
));
|
|
992
993
|
}
|
|
@@ -1009,11 +1010,13 @@ async function callToolInner(name, args) {
|
|
|
1009
1010
|
}
|
|
1010
1011
|
case 'search_thumbgate':
|
|
1011
1012
|
enforceLimit('search_thumbgate');
|
|
1012
|
-
return toTextResult(
|
|
1013
|
+
return toTextResult(await searchThumbgateAsync({
|
|
1013
1014
|
query: args.query,
|
|
1014
1015
|
limit: args.limit,
|
|
1015
1016
|
source: args.source,
|
|
1016
1017
|
signal: args.signal,
|
|
1018
|
+
metadataFilters: args.filters,
|
|
1019
|
+
queryRewrite: args.queryRewrite !== false,
|
|
1017
1020
|
}));
|
|
1018
1021
|
case 'import_document':
|
|
1019
1022
|
return toTextResult(importDocument({
|
|
@@ -1038,6 +1041,61 @@ async function callToolInner(name, args) {
|
|
|
1038
1041
|
}
|
|
1039
1042
|
return toTextResult(document);
|
|
1040
1043
|
}
|
|
1044
|
+
case 'gate_check': {
|
|
1045
|
+
// Same engine the PreToolUse hook uses, so an MCP client and a hook cannot
|
|
1046
|
+
// disagree about whether an action is allowed.
|
|
1047
|
+
const { runAsync } = require('../../scripts/gates-engine');
|
|
1048
|
+
const { canonicalizeToolCall } = require('../../scripts/harness-tool-names');
|
|
1049
|
+
const canonical = canonicalizeToolCall(args.tool_name, args.tool_input || {});
|
|
1050
|
+
const raw = await runAsync({
|
|
1051
|
+
tool_name: canonical.toolName,
|
|
1052
|
+
tool_input: canonical.toolInput,
|
|
1053
|
+
});
|
|
1054
|
+
let decision = 'allow';
|
|
1055
|
+
let reason = '';
|
|
1056
|
+
let flagged = false;
|
|
1057
|
+
try {
|
|
1058
|
+
const parsed = JSON.parse(raw);
|
|
1059
|
+
const hook = parsed.hookSpecificOutput || {};
|
|
1060
|
+
const verdict = hook.permissionDecision || parsed.decision || '';
|
|
1061
|
+
reason = hook.permissionDecisionReason || parsed.reason || hook.additionalContext || '';
|
|
1062
|
+
// The hook wire format says "deny"; the documented tool contract says "block".
|
|
1063
|
+
if (verdict === 'deny' || verdict === 'block') {
|
|
1064
|
+
decision = 'block';
|
|
1065
|
+
flagged = true;
|
|
1066
|
+
} else if (/\[GATE:/.test(reason)) {
|
|
1067
|
+
// A gate MATCHED but the warn-by-default posture downgraded it. Reporting
|
|
1068
|
+
// "allow" here is how this tool would become theater: .clinerules tells the
|
|
1069
|
+
// agent to abort only on "block", so a matched rm -rf / would have been run
|
|
1070
|
+
// with the warning text ignored. "warn" is the honest third state.
|
|
1071
|
+
decision = 'warn';
|
|
1072
|
+
flagged = true;
|
|
1073
|
+
}
|
|
1074
|
+
} catch (_) {
|
|
1075
|
+
// Unparseable engine output must never read as "allow" — fail closed.
|
|
1076
|
+
decision = 'error';
|
|
1077
|
+
reason = 'gate engine returned unparseable output';
|
|
1078
|
+
}
|
|
1079
|
+
const strict = process.env.THUMBGATE_STRICT_ENFORCEMENT === '1';
|
|
1080
|
+
return {
|
|
1081
|
+
content: [{
|
|
1082
|
+
type: 'text',
|
|
1083
|
+
text: JSON.stringify({
|
|
1084
|
+
decision,
|
|
1085
|
+
flagged,
|
|
1086
|
+
enforcement: strict ? 'strict' : 'warn-by-default',
|
|
1087
|
+
guidance: decision === 'block'
|
|
1088
|
+
? 'Do NOT run this action. Surface the reason to the user.'
|
|
1089
|
+
: decision === 'warn'
|
|
1090
|
+
? 'A policy gate matched but enforcement is warn-by-default. Do NOT run this action without explicit user confirmation; show them the reason.'
|
|
1091
|
+
: decision === 'error'
|
|
1092
|
+
? 'Gate evaluation failed. Treat as unsafe and ask the user.'
|
|
1093
|
+
: 'No policy gate matched.',
|
|
1094
|
+
reason,
|
|
1095
|
+
}, null, 2),
|
|
1096
|
+
}],
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1041
1099
|
case 'feedback_stats':
|
|
1042
1100
|
return toTextResult(analyzeFeedback(undefined, { humanOnly: true }));
|
|
1043
1101
|
case 'diagnose_failure':
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"id": "checkout-link-grounding",
|
|
5
5
|
"traceId": "trace_checkout_001",
|
|
6
6
|
"question": "Is the Pro checkout link working?",
|
|
7
|
-
"response": "The Pro checkout link is working because the checkout diagnostic confirms Stripe Pro checkout is reachable.",
|
|
7
|
+
"response": "The Pro checkout link is working because the checkout diagnostic confirms Stripe Pro checkout is reachable [context-1].",
|
|
8
8
|
"retrievedContexts": [
|
|
9
9
|
"Stripe Pro checkout diagnostic confirms the Pro checkout link is reachable and returns a valid checkout target.",
|
|
10
10
|
"The workflow sprint checkout is intake-led and should not expose a stale payment link."
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"id": "letta-tool-gate",
|
|
16
16
|
"traceId": "trace_letta_001",
|
|
17
17
|
"question": "Should Letta execute a force push tool call?",
|
|
18
|
-
"response": "ThumbGate blocks force push before execution for Letta tool calls.",
|
|
18
|
+
"response": "ThumbGate blocks force push before execution for Letta tool calls [context-1].",
|
|
19
19
|
"retrievedContexts": [
|
|
20
20
|
"ThumbGate's Letta adapter wraps Letta tool calls and runs gate-check before the executor is called.",
|
|
21
21
|
"Force push to main is a blocked high-risk git action."
|
package/bin/cli.js
CHANGED
|
@@ -39,7 +39,6 @@ const {
|
|
|
39
39
|
codexAutoUpdateCliEntry,
|
|
40
40
|
codexAutoUpdateMcpEntry,
|
|
41
41
|
isSourceCheckout,
|
|
42
|
-
publishedCliAvailable,
|
|
43
42
|
localMcpEntry,
|
|
44
43
|
resolveMcpEntry,
|
|
45
44
|
} = require(path.join(__dirname, '..', 'scripts', 'mcp-config'));
|
|
@@ -411,16 +410,17 @@ function canonicalMcpEntry(scope = 'project') {
|
|
|
411
410
|
}
|
|
412
411
|
|
|
413
412
|
function canonicalCodexMcpEntry() {
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
// Codex config is user-global and must survive disposable worktree cleanup.
|
|
414
|
+
// Use the stable published launcher even when init is invoked from source;
|
|
415
|
+
// developers can explicitly opt into a checkout-pinned runtime when needed.
|
|
416
|
+
if (isSourceCheckout(PKG_ROOT) && process.env.THUMBGATE_CODEX_USE_SOURCE_RUNTIME === '1') {
|
|
416
417
|
return localMcpEntry(PKG_ROOT, 'home');
|
|
417
418
|
}
|
|
418
419
|
return codexAutoUpdateMcpEntry();
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
function canonicalCodexCliEntry(commandArgs) {
|
|
422
|
-
|
|
423
|
-
if (isSourceCheckout(PKG_ROOT) && !publishedCliAvailable(version)) {
|
|
423
|
+
if (isSourceCheckout(PKG_ROOT) && process.env.THUMBGATE_CODEX_USE_SOURCE_RUNTIME === '1') {
|
|
424
424
|
return {
|
|
425
425
|
command: 'node',
|
|
426
426
|
args: [path.join(PKG_ROOT, 'bin', 'cli.js'), ...commandArgs],
|
|
@@ -584,7 +584,20 @@ function detectPlatform(name, checks) {
|
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
function whichExists(cmd) {
|
|
587
|
-
|
|
587
|
+
if (!cmd || /[\\/]/.test(cmd)) return false;
|
|
588
|
+
const pathEntries = String(process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
589
|
+
const extensions = process.platform === 'win32'
|
|
590
|
+
? String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
591
|
+
: [''];
|
|
592
|
+
return pathEntries.some((entry) => extensions.some((extension) => {
|
|
593
|
+
const candidate = path.join(entry, `${cmd}${extension}`);
|
|
594
|
+
try {
|
|
595
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
596
|
+
return fs.statSync(candidate).isFile();
|
|
597
|
+
} catch (_) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}));
|
|
588
601
|
}
|
|
589
602
|
|
|
590
603
|
function setupClaude() {
|
|
@@ -636,18 +649,26 @@ function setupCodex() {
|
|
|
636
649
|
return configChanged || hookResult.changed;
|
|
637
650
|
}
|
|
638
651
|
|
|
639
|
-
function setupGemini() {
|
|
640
|
-
//
|
|
641
|
-
|
|
652
|
+
function setupGemini(options = {}) {
|
|
653
|
+
// Importing a plugin executes an external package manager and can mutate
|
|
654
|
+
// machine-wide Gemini state. Keep ordinary init/quick-start deterministic;
|
|
655
|
+
// operators can opt in explicitly when they want the plugin import too.
|
|
656
|
+
const importPlugin = options.importPlugin === true
|
|
657
|
+
|| process.env.THUMBGATE_IMPORT_AGENT_PLUGINS === '1';
|
|
642
658
|
let pluginImported = false;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
659
|
+
if (importPlugin) {
|
|
660
|
+
for (const binName of ['agy', 'gemini']) {
|
|
661
|
+
try {
|
|
662
|
+
execFileSync(binName, ['plugin', 'import', PKG_ROOT, '--force'], {
|
|
663
|
+
stdio: 'ignore',
|
|
664
|
+
timeout: 5000,
|
|
665
|
+
});
|
|
666
|
+
console.log(` Gemini: imported thumbgate plugin via ${binName}`);
|
|
667
|
+
pluginImported = true;
|
|
668
|
+
break;
|
|
669
|
+
} catch (err) {
|
|
670
|
+
// A missing, failing, or slow optional plugin manager must not block init.
|
|
671
|
+
}
|
|
651
672
|
}
|
|
652
673
|
}
|
|
653
674
|
|
|
@@ -693,6 +714,66 @@ function setupAmp() {
|
|
|
693
714
|
return true;
|
|
694
715
|
}
|
|
695
716
|
|
|
717
|
+
function setupOpenCode() {
|
|
718
|
+
// OpenCode was listed in README's install table and in `init --help` since the
|
|
719
|
+
// adapter asset landed, but no setup function ever existed — `--agent opencode`
|
|
720
|
+
// silently wrote Claude/Codex/Gemini config and exited 0. OpenCode has no hook
|
|
721
|
+
// surface, so this wires the MCP server, which is its real integration point.
|
|
722
|
+
const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
|
|
723
|
+
const srcPath = path.join(PKG_ROOT, 'adapters', 'opencode', 'opencode.json');
|
|
724
|
+
if (!fs.existsSync(srcPath)) return false;
|
|
725
|
+
|
|
726
|
+
let src;
|
|
727
|
+
try { src = JSON.parse(fs.readFileSync(srcPath, 'utf8')); } catch (_) { return false; }
|
|
728
|
+
const desired = (src.mcp || {}).thumbgate;
|
|
729
|
+
if (!desired) return false;
|
|
730
|
+
|
|
731
|
+
let config = {};
|
|
732
|
+
if (fs.existsSync(configPath)) {
|
|
733
|
+
try {
|
|
734
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
735
|
+
} catch (error) {
|
|
736
|
+
// Resetting to {} here and writing would replace the whole file — silently deleting
|
|
737
|
+
// the user's model, provider, plugin and other MCP settings just to add ours.
|
|
738
|
+
// A config we cannot parse is a reason to stop, not to overwrite.
|
|
739
|
+
console.error(` OpenCode: ${configPath} could not be parsed (${error.message}).`);
|
|
740
|
+
console.error(' Refusing to overwrite it. Fix or move the file, then re-run.');
|
|
741
|
+
process.exitCode = 1;
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
config.mcp = config.mcp || {};
|
|
746
|
+
if (JSON.stringify(config.mcp[MCP_SERVER_NAME]) === JSON.stringify(desired)) return false;
|
|
747
|
+
|
|
748
|
+
config.$schema = config.$schema || src.$schema;
|
|
749
|
+
config.mcp[MCP_SERVER_NAME] = desired;
|
|
750
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
751
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
752
|
+
console.log(` OpenCode: registered thumbgate MCP server in ${configPath}`);
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Single source of truth for `--agent`. Every value advertised in `init --help`
|
|
757
|
+
// and README.md must appear here with a real handler, or the flag lies.
|
|
758
|
+
// `hookAgent: true` means scripts/auto-wire-hooks.js can wire a pre-tool hook;
|
|
759
|
+
// the rest integrate over MCP only and are wired by their setup function.
|
|
760
|
+
const AGENT_ALIASES = {
|
|
761
|
+
claude: 'claude-code',
|
|
762
|
+
'claude_code': 'claude-code',
|
|
763
|
+
'open-code': 'opencode',
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const SUPPORTED_AGENTS = {
|
|
767
|
+
'claude-code': { hookAgent: true },
|
|
768
|
+
codex: { hookAgent: true },
|
|
769
|
+
gemini: { hookAgent: true },
|
|
770
|
+
forge: { hookAgent: true },
|
|
771
|
+
cursor: { hookAgent: true },
|
|
772
|
+
cline: { hookAgent: false, setup: () => setupCline() },
|
|
773
|
+
amp: { hookAgent: false, setup: () => setupAmp() },
|
|
774
|
+
opencode: { hookAgent: false, setup: () => setupOpenCode() },
|
|
775
|
+
};
|
|
776
|
+
|
|
696
777
|
function setupCursor() {
|
|
697
778
|
return mergeMcpJson(path.join(CWD, '.cursor', 'mcp.json'), 'Cursor', 'project');
|
|
698
779
|
}
|
|
@@ -934,14 +1015,23 @@ function quickstart() {
|
|
|
934
1015
|
|
|
935
1016
|
function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
936
1017
|
const args = { ...cliArgs };
|
|
1018
|
+
// A typo used to exit 0 having wired nothing for the requested agent.
|
|
1019
|
+
// Aliases must resolve BEFORE validation: scripts/auto-wire-hooks.js accepts `claude`
|
|
1020
|
+
// for `claude-code`, and plugins/claude-skill/README.md publishes that exact command.
|
|
1021
|
+
if (args.agent && AGENT_ALIASES[args.agent]) args.agent = AGENT_ALIASES[args.agent];
|
|
1022
|
+
if (args.agent && !Object.prototype.hasOwnProperty.call(SUPPORTED_AGENTS, args.agent)) {
|
|
1023
|
+
console.error(`Unknown --agent "${args.agent}". Supported: ${Object.keys(SUPPORTED_AGENTS).join(', ')}`);
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
937
1026
|
if (args.help || args.h) {
|
|
938
|
-
console.log('Usage: npx thumbgate init [--agent <name>] [--wire-hooks] [--email you@company.com]');
|
|
1027
|
+
console.log('Usage: npx thumbgate init [--agent <name>] [--wire-hooks] [--import-agent-plugins] [--email you@company.com]');
|
|
939
1028
|
console.log('');
|
|
940
1029
|
console.log('Scaffold ThumbGate in the current project and wire detected agent integrations.');
|
|
941
1030
|
console.log('');
|
|
942
1031
|
console.log('Options:');
|
|
943
|
-
console.log(
|
|
1032
|
+
console.log(` --agent <name> Wire a specific agent: ${Object.keys(SUPPORTED_AGENTS).join(', ')}`);
|
|
944
1033
|
console.log(' --wire-hooks Wire hooks only; do not scaffold project files');
|
|
1034
|
+
console.log(' --import-agent-plugins Also import optional agent plugins when supported');
|
|
945
1035
|
console.log(' --email <email> Subscribe installer to the setup guide and trial reminders');
|
|
946
1036
|
console.log(' --dry-run Show hook changes without writing them');
|
|
947
1037
|
return;
|
|
@@ -1037,17 +1127,26 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
1037
1127
|
let configured = 0;
|
|
1038
1128
|
|
|
1039
1129
|
const platforms = [
|
|
1040
|
-
{ name: 'Claude Code', detect: [
|
|
1130
|
+
{ agent: 'claude-code', name: 'Claude Code', detect: [
|
|
1041
1131
|
() => whichExists('claude'),
|
|
1042
1132
|
() => fs.existsSync(path.join(HOME, '.claude')),
|
|
1043
1133
|
() => fs.existsSync(path.join(CWD, '.claude')),
|
|
1044
1134
|
], setup: setupClaude },
|
|
1045
|
-
{ name: 'Codex', detect: [() => whichExists('codex'), () => fs.existsSync(path.join(HOME, '.codex'))], setup: setupCodex },
|
|
1046
|
-
{
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1135
|
+
{ agent: 'codex', name: 'Codex', detect: [() => whichExists('codex'), () => fs.existsSync(path.join(HOME, '.codex'))], setup: setupCodex },
|
|
1136
|
+
{
|
|
1137
|
+
agent: 'gemini',
|
|
1138
|
+
name: 'Gemini',
|
|
1139
|
+
detect: [
|
|
1140
|
+
() => whichExists('gemini'),
|
|
1141
|
+
() => whichExists('agy'),
|
|
1142
|
+
() => fs.existsSync(path.join(HOME, '.gemini')),
|
|
1143
|
+
],
|
|
1144
|
+
setup: () => setupGemini({ importPlugin: args['import-agent-plugins'] === true }),
|
|
1145
|
+
},
|
|
1146
|
+
{ agent: 'amp', name: 'Amp', detect: [() => whichExists('amp'), () => fs.existsSync(path.join(HOME, '.amp'))], setup: setupAmp },
|
|
1147
|
+
{ agent: 'cursor', name: 'Cursor', detect: [() => fs.existsSync(path.join(HOME, '.cursor', 'mcp.json')), () => fs.existsSync(path.join(CWD, '.cursor'))], setup: setupCursor },
|
|
1148
|
+
{ agent: 'forge', name: 'ForgeCode', detect: [() => whichExists('forge'), () => fs.existsSync(path.join(CWD, 'forge.yaml'))], setup: setupForge },
|
|
1149
|
+
{ agent: 'cline', name: 'Cline', detect: [
|
|
1051
1150
|
() => fs.existsSync(path.join(CWD, '.clinerules')),
|
|
1052
1151
|
() => process.platform === 'darwin' && fs.existsSync(path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')),
|
|
1053
1152
|
() => process.platform === 'linux' && fs.existsSync(path.join(HOME, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')),
|
|
@@ -1056,6 +1155,11 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
1056
1155
|
];
|
|
1057
1156
|
|
|
1058
1157
|
for (const p of platforms) {
|
|
1158
|
+
// An explicit target must stay scoped to that integration. Quick-start
|
|
1159
|
+
// always resolves one target before calling init; configuring every other
|
|
1160
|
+
// CLI merely because it exists on PATH caused surprise machine mutations
|
|
1161
|
+
// and multi-second hangs in ordinary onboarding.
|
|
1162
|
+
if (args.agent && p.agent !== args.agent) continue;
|
|
1059
1163
|
if (detectPlatform(p.name, p.detect)) {
|
|
1060
1164
|
const didSetup = p.setup();
|
|
1061
1165
|
if (didSetup) configured++;
|
|
@@ -1076,15 +1180,27 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
1076
1180
|
|
|
1077
1181
|
if (configured === 0) console.log(' All detected platforms already configured.');
|
|
1078
1182
|
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
|
|
1082
|
-
|
|
1183
|
+
// Agents without a native pre-tool hook surface (cline, amp, opencode) are wired
|
|
1184
|
+
// by their own setup function. Previously only cline was handled here: `--agent amp`
|
|
1185
|
+
// and `--agent opencode` fell through to wireHooks, which rejected them, printed the
|
|
1186
|
+
// rejection as an ordinary log line, and exited 0 — so the flag appeared to work
|
|
1187
|
+
// while writing nothing for that agent.
|
|
1188
|
+
const agentSpec = args.agent ? SUPPORTED_AGENTS[args.agent] : null;
|
|
1189
|
+
if (agentSpec && agentSpec.setup) {
|
|
1190
|
+
// A setup function returns false both when nothing needed doing and when it refused
|
|
1191
|
+
// (e.g. an unparseable config it declined to overwrite). Reporting "already
|
|
1192
|
+
// configured" for a refusal would repeat the original sin of this PR: a failure
|
|
1193
|
+
// dressed up as success. The refusal path sets process.exitCode, so key off that.
|
|
1194
|
+
const changed = agentSpec.setup();
|
|
1195
|
+
if (!changed && !process.exitCode) console.log(` ${args.agent}: already configured`);
|
|
1083
1196
|
} else if (args.agent || args['wire-hooks']) {
|
|
1084
1197
|
const { wireHooks } = require(path.join(PKG_ROOT, 'scripts', 'auto-wire-hooks'));
|
|
1085
1198
|
const hookResult = wireHooks({ agent: args.agent, dryRun: args['dry-run'] });
|
|
1086
1199
|
if (hookResult.error) {
|
|
1087
|
-
|
|
1200
|
+
// An explicit --agent that cannot be wired is a FAILURE, not a note. Exiting 0
|
|
1201
|
+
// here is what let a user believe `--agent opencode` had protected them.
|
|
1202
|
+
console.error(` Hook wiring failed for --agent ${args.agent}: ${hookResult.error}`);
|
|
1203
|
+
process.exitCode = 1;
|
|
1088
1204
|
} else if (!hookResult.changed) {
|
|
1089
1205
|
console.log(` Hooks: already wired for ${hookResult.agent}`);
|
|
1090
1206
|
} else {
|