glm-mcp-claude 1.0.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.
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ // glm_subagent_router.mjs
3
+ // PreToolUse hook for the Task (subagent) tool. Fires ONLY when a subagent is
4
+ // about to be spawned -> zero token cost the rest of the time.
5
+ //
6
+ // It infers the task profile from the Task description/prompt, runs the shared
7
+ // GLM-vs-Opus router logic, and injects a concise verdict as additionalContext
8
+ // so the orchestrator can route cheap work to the `glm` subagent automatically.
9
+ //
10
+ // It NEVER blocks: on any error or unsupported field it simply exits 0.
11
+
12
+ import { pathToFileURL, fileURLToPath } from "node:url";
13
+ import { dirname, resolve } from "node:path";
14
+ import { readFileSync } from "node:fs";
15
+
16
+ // Portable: resolve the router relative to this hook's own location.
17
+ // Layout: <claude>/hooks/glm_subagent_router.mjs + <claude>/glm-mcp/src/router.js
18
+ const HERE = dirname(fileURLToPath(import.meta.url));
19
+ const ROUTER = resolve(HERE, "..", "glm-mcp", "src", "router.js");
20
+
21
+ // Pull the most recent genuine human message from the transcript (skipping
22
+ // tool_result turns), so we can detect when the user explicitly picked an agent.
23
+ function lastUserText(transcriptPath) {
24
+ if (!transcriptPath) return "";
25
+ try {
26
+ const lines = readFileSync(transcriptPath, "utf8").trim().split(/\r?\n/);
27
+ for (let i = lines.length - 1; i >= 0; i--) {
28
+ let o;
29
+ try { o = JSON.parse(lines[i]); } catch { continue; }
30
+ const m = o.message || o;
31
+ if (o.type === "user" || m.role === "user") {
32
+ const c = m.content;
33
+ if (typeof c === "string") return c;
34
+ if (Array.isArray(c)) {
35
+ const texts = c.filter((b) => b && b.type === "text").map((b) => b.text);
36
+ if (texts.length) return texts.join("\n"); // else it's a tool_result turn -> keep scanning
37
+ }
38
+ }
39
+ }
40
+ } catch {}
41
+ return "";
42
+ }
43
+
44
+ // Did the user explicitly name an agent/model to use? Then honor it -- no nudge.
45
+ function explicitAgentRequested(text) {
46
+ const t = (text || "").toLowerCase();
47
+ return (
48
+ /\b(use|using|with|via|run (?:this|it) (?:on|with)|delegate (?:this |it )?to|hand (?:this|it) to|switch to)\b[^.\n]*\b(glm|opus|sonnet|haiku|general[- ]purpose|explore|plan)\b/.test(t) ||
49
+ /\b(glm|opus|sonnet|haiku|general[- ]purpose)\b\s+(agent|sub-?agent|model)\b/.test(t)
50
+ );
51
+ }
52
+
53
+ function readStdin() {
54
+ return new Promise((resolve) => {
55
+ let d = "";
56
+ process.stdin.on("data", (c) => (d += c));
57
+ process.stdin.on("end", () => resolve(d));
58
+ // safety: if no stdin arrives, don't hang
59
+ setTimeout(() => resolve(d), 2000);
60
+ });
61
+ }
62
+
63
+ function inferProfile(text) {
64
+ const t = (text || "").toLowerCase();
65
+ const has = (...ks) => ks.some((k) => t.includes(k));
66
+ const extra = {};
67
+
68
+ // Cross-cutting condition flags (can co-exist with a task type).
69
+ if (has("screenshot", "image", "diagram", "this photo", "the picture", "gui ", "computer use")) extra.vision = true;
70
+ if (has("中文", "chinese", "mandarin", "bilingual", "translate to chinese")) extra.chinese = true;
71
+ if (has("undocumented", "internal api", "proprietary api", "niche", "obscure api", "post-cutoff", "brand-new api", "newly released")) extra.unfamiliarApi = true;
72
+ if (has("parallel", "concurrent", "fan out", "fan-out", "at the same time", "multiple agents")) extra.needsParallel = true;
73
+ if (has("entire repo", "whole codebase", "across the codebase", "many files", "multi-hour", "long-running", "fully autonomous")) { extra.longHorizon = true; }
74
+ // tool-use shape: heavy/dependent agentic loop -> Opus; one-shot/short -> GLM
75
+ if (has("agent loop", "agentic", "many tool calls", "multi-step tool", "orchestrate tools", "chain of tool", "dependent tool", "tool-heavy", "long tool")) extra.toolPattern = "heavy";
76
+ else if (has("single tool call", "one tool call", "structured extraction", "function call", "json schema", "extract fields")) extra.toolPattern = "single";
77
+
78
+ // Hard-sensitive short-circuit.
79
+ if (has("secret", "password", "credential", "api key", "private key", " auth", "authentication", "oauth", "crypto", "encrypt", "vulnerab", "proprietary", "security review"))
80
+ return { taskType: "security", sensitive: true, ...extra };
81
+
82
+ // Task type (first match wins; order = specificity).
83
+ let taskType = "general";
84
+ if (extra.toolPattern === "heavy") taskType = "toolcall_heavy";
85
+ else if (has("migration", "migrate the database", "schema migration", "alembic", "flyway")) taskType = "migration";
86
+ else if (has("code review", "review this diff", "review the pr", "review my code")) taskType = "code_review";
87
+ else if (has("terraform", "kubernetes", "k8s", "helm", "dockerfile", "infrastructure as code", "cloudformation")) taskType = "iac";
88
+ else if (has("github actions", "ci/cd", "ci pipeline", "gitlab ci", "jenkins", "workflow yaml")) taskType = "cicd";
89
+ else if (has("sql", "query the", "select ", "join ", "optimize the query")) taskType = "sql";
90
+ else if (has("regex", "regular expression")) taskType = "regex";
91
+ else if (has("etl", "data pipeline", "ingest", "transform the data")) taskType = "etl";
92
+ else if (has("integration test", "e2e", "end-to-end test")) taskType = "integration_test";
93
+ else if (has("unit test", "write tests", "test coverage", "pytest", "jest", "vitest")) taskType = "unit_test";
94
+ else if (has("type error", "typescript error", "lint", "eslint", "mypy", "type fix")) taskType = "type_lint";
95
+ else if (has("upgrade", "bump version", "migrate to v", "dependency", "deprecat")) taskType = "dependency_upgrade";
96
+ else if (has("performance", "optimize the", "speed up", "bottleneck", "profil")) taskType = "perf";
97
+ else if (has("integrate with", "third-party api", "external api", "sdk for", "api integration")) taskType = "api_integration";
98
+ else if (has("rust", "golang", " go ", "c++", "concurrency", "mutex", "memory safety", "data race")) taskType = "systems";
99
+ else if (has("translate", "i18n", "localization", "localize")) taskType = "i18n";
100
+ else if (has("notebook", "jupyter", "exploratory", "eda", "pandas analysis")) taskType = "notebook";
101
+ else if (has("train a model", "training loop", "ml model", "fine-tune", "neural network")) taskType = "ml_training";
102
+ else if (has("cli", "shell script", "bash script", "automation script")) taskType = "cli";
103
+ else if (has("frontend", "react", "component", "css", "tailwind", " ui", "button", "styling", "html", "landing page", "dashboard")) taskType = "frontend";
104
+ else if (has("boilerplate", "scaffold", "template", "stub", "starter")) taskType = "boilerplate";
105
+ else if (has("config file", "yaml config", "set up config", ".env", "settings file")) taskType = "config";
106
+ else if (has("prototype", "proof of concept", "poc", "quick demo", "mvp")) taskType = "prototype";
107
+ else if (has("crud", "endpoint", "rest api", "data model")) taskType = "crud";
108
+ else if (has("refactor")) taskType = has("large", "entire", "whole", "across the", "codebase-wide", "many files") ? "refactor_large" : "refactor_local";
109
+ else if (has("debug", "why is", "not working", "stack trace", "root cause", "fix the bug", "intermittent")) taskType = "debugging";
110
+ else if (has("architect", "system design", "high-level design", "trade-off", "tradeoff", "design the system")) taskType = "architecture";
111
+ else if (has("document", "readme", "docstring", "changelog", "comment the")) taskType = "docs";
112
+ else if (has("summar", "research", "find out", "look up", "investigate", "gather")) taskType = "research";
113
+ else if (has("algorithm", "leetcode", "competitive programming", "time complexity", "dynamic programming")) taskType = "algorithm";
114
+ else if (extra.toolPattern === "single") taskType = "toolcall_single";
115
+
116
+ if (has("large", "entire", "whole codebase", "complex", "subtle", "tricky")) extra.complexity = "high";
117
+
118
+ return { taskType, ...extra };
119
+ }
120
+
121
+ (async () => {
122
+ try {
123
+ const raw = await readStdin();
124
+ const payload = JSON.parse(raw || "{}");
125
+ const ti = payload.tool_input || {};
126
+ const subagent = ti.subagent_type || "";
127
+
128
+ // Already routing to the GLM delegate -> nothing to advise.
129
+ if (subagent === "glm") process.exit(0);
130
+
131
+ // User explicitly chose an agent/model ("use opus", "use the sonnet agent", ...)
132
+ // -> honor their choice, do not inject a recommendation.
133
+ if (explicitAgentRequested(lastUserText(payload.transcript_path))) process.exit(0);
134
+
135
+ const text = [ti.description, ti.prompt].filter(Boolean).join("\n");
136
+ const profile = inferProfile(text);
137
+ const cwd = payload.cwd || "<the project root absolute path>";
138
+ // Is this hands-on repo work (edit/create/run files) vs pure text generation?
139
+ const pureText = ["research", "summarization"].includes(profile.taskType) ||
140
+ /\b(explain|summari[sz]e|what is|describe|brainstorm|outline|draft an? (email|message|note))\b/.test(text.toLowerCase());
141
+ const repoTask = !pureText;
142
+
143
+ let verdict, peakNote = "";
144
+ try {
145
+ const { recommend, isPeak } = await import(pathToFileURL(ROUTER).href);
146
+ const rec = recommend(profile);
147
+ const peak = isPeak();
148
+ peakNote = peak
149
+ ? `Currently CHINA PEAK (14:00-18:00 UTC+8): GLM-5.2 costs ~3x now, so the router routes LESS to GLM during peak (only stronger-fit tasks go to GLM).`
150
+ : `Currently OFF-PEAK in China: "auto" uses GLM-5.2 (best capability, cheapest now).`;
151
+ if (rec.engine !== "glm") {
152
+ verdict = `KEEP ON OPUS (inferred: ${profile.taskType}, confidence ${rec.confidence}). Why: ${rec.reasons[0] || ""}`;
153
+ } else if (repoTask) {
154
+ // Hands-on repo task -> point at glm_agent directly (skip the subagent middle layer).
155
+ verdict =
156
+ `GLM-SUITABLE repo task (inferred: ${profile.taskType}, confidence ${rec.confidence}). ` +
157
+ `Best path: instead of spawning ${ti.subagent_type || "a subagent"}, call mcp__glm__glm_agent directly ` +
158
+ `with workdir="${cwd}" so GLM (model ${rec.model}, ~10x cheaper) reads/edits the files and runs tests itself. ` +
159
+ `For oversight, pass dry_run:true first to review GLM's proposed diff, then call again to apply. ` +
160
+ `Why: ${rec.reasons[rec.reasons.length - 1] || rec.reasons[0] || ""}`;
161
+ } else {
162
+ // Pure generation -> draft via glm_delegate / the glm subagent.
163
+ verdict =
164
+ `GLM-SUITABLE generation (inferred: ${profile.taskType}, confidence ${rec.confidence}). ` +
165
+ `Use mcp__glm__glm_delegate (or the "glm" subagent) to draft this (model ${rec.model}, ~10x cheaper); ` +
166
+ `then place/verify the result yourself. Why: ${rec.reasons[rec.reasons.length - 1] || rec.reasons[0] || ""}`;
167
+ }
168
+ } catch {
169
+ verdict = `Could not load router; GLM delegate is still available (mcp__glm__glm_agent for repo tasks, mcp__glm__glm_delegate for drafts, mcp__glm__glm_recommend to decide).`;
170
+ }
171
+
172
+ // Detailed rules -- only injected when a subagent is actually spawned, so the
173
+ // extra tokens are pay-per-use, not charged on every command.
174
+ const RULES = [
175
+ "PHILOSOPHY: GLM is ~10x cheaper than Opus (still ~3-4x cheaper even at peak), so GLM is the",
176
+ "DEFAULT for safe-to-be-wrong work; Opus is the exception you 'pay up' for only when quality or",
177
+ "risk justifies it. Cheaper-but-wrong is expensive on hard tasks (rework + Opus tokens to fix),",
178
+ "so the cost bias applies to easy/neutral work, not to the hard-task overrides below.",
179
+ "ROUTING RULES (GLM = Zhipu/Z.ai delegate, ~10x cheaper than Opus):",
180
+ "- Lean GLM: frontend/UI, boilerplate, scaffolding, CRUD, single-file/local refactors,",
181
+ " docs, summarization, research drafts, tool-calling/MCP work, algorithmic codegen.",
182
+ " GLM shines on well-specified, single-purpose work where being wrong is cheap.",
183
+ "- Lean OPUS: subtle or long debugging, open-ended architecture, large multi-step",
184
+ " refactors, security review, systems languages (Rust/Go/C), vision, GUI/computer control.",
185
+ "- HARD OVERRIDES -> always Opus regardless of cost: (1) sensitive/proprietary/secret code",
186
+ " (GLM routes through servers in China), (2) needs parallel agents (GLM ~1-concurrency cap),",
187
+ " (3) long-horizon AND high complexity (GLM drifts off-plan), (4) latency-critical loops.",
188
+ "- Complexity adjusts: low -> nudge GLM, high -> nudge Opus.",
189
+ "HOW TO ACT: if GLM-suitable, spawn the \"glm\" subagent (it gathers context and pastes it in,",
190
+ "since GLM cannot read files or use Claude's tools). Unsure -> call mcp__glm__glm_recommend (free,",
191
+ "local). After GLM returns, verify the output; if it's weak or wrong, retry once with a sharper",
192
+ "prompt, then escalate to Opus. Never let cost-saving degrade correctness on work that matters.",
193
+ ].join("\n");
194
+
195
+ const out = {
196
+ hookSpecificOutput: {
197
+ hookEventName: "PreToolUse",
198
+ additionalContext: `[GLM router] Verdict: ${verdict}\n${peakNote}\n\n${RULES}`,
199
+ },
200
+ };
201
+ process.stdout.write(JSON.stringify(out));
202
+ } catch {
203
+ // never block a subagent spawn on hook failure
204
+ }
205
+ process.exit(0);
206
+ })();
package/install.mjs ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // install.mjs -- one-shot installer for the GLM hybrid setup.
3
+ // Installs GLM as a cheap, full-capability subagent for Claude Code, with auto-routing.
4
+ //
5
+ // Usage:
6
+ // node install.mjs # install for the current user (global)
7
+ // node install.mjs --key YOUR_ZAI_KEY # also write the API key into .env
8
+ // node install.mjs --claude-dir PATH # target a custom .claude dir (default ~/.claude)
9
+ // node install.mjs --no-register # skip `claude mcp add` (do it manually)
10
+ // node install.mjs --skip-npm # skip `npm install` (deps already present)
11
+ //
12
+ // It is idempotent: re-running updates files without duplicating hook/policy entries.
13
+
14
+ import { cpSync, mkdirSync, existsSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { execSync } from "node:child_process";
19
+
20
+ const SELF = dirname(fileURLToPath(import.meta.url));
21
+ const args = process.argv.slice(2);
22
+ const getFlag = (n) => args.includes(n);
23
+ const getOpt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
24
+
25
+ const CLAUDE = getOpt("--claude-dir") || join(homedir(), ".claude");
26
+ const KEY = getOpt("--key") || process.env.GLM_API_KEY || "";
27
+ const NO_REGISTER = getFlag("--no-register");
28
+ const SKIP_NPM = getFlag("--skip-npm");
29
+
30
+ const log = (s) => console.log(s);
31
+ const step = (s) => console.log(`\n→ ${s}`);
32
+
33
+ log(`GLM hybrid installer`);
34
+ log(` source : ${SELF}`);
35
+ log(` target : ${CLAUDE}`);
36
+
37
+ // 1. Copy the MCP server (skip node_modules and any stray .env).
38
+ step("Installing MCP server -> " + join(CLAUDE, "glm-mcp"));
39
+ mkdirSync(CLAUDE, { recursive: true });
40
+ cpSync(join(SELF, "glm-mcp"), join(CLAUDE, "glm-mcp"), {
41
+ recursive: true,
42
+ filter: (src) => {
43
+ const base = src.split(/[\\/]/).pop();
44
+ return base !== "node_modules" && base !== ".env";
45
+ },
46
+ });
47
+
48
+ // 2. .env
49
+ step("Setting up .env");
50
+ const envPath = join(CLAUDE, "glm-mcp", ".env");
51
+ if (!existsSync(envPath)) {
52
+ copyFileSync(join(CLAUDE, "glm-mcp", ".env.example"), envPath);
53
+ log(" created .env from .env.example");
54
+ }
55
+ if (KEY) {
56
+ let env = readFileSync(envPath, "utf8");
57
+ env = /^GLM_API_KEY=/m.test(env)
58
+ ? env.replace(/^GLM_API_KEY=.*$/m, `GLM_API_KEY=${KEY}`)
59
+ : `GLM_API_KEY=${KEY}\n` + env;
60
+ writeFileSync(envPath, env);
61
+ log(" wrote GLM_API_KEY into .env");
62
+ } else if (!/^GLM_API_KEY=\S+/m.test(readFileSync(envPath, "utf8"))) {
63
+ log(" ⚠ No API key set yet. Edit " + envPath + " and set GLM_API_KEY=... before use.");
64
+ }
65
+
66
+ // 3. npm install
67
+ if (!SKIP_NPM) {
68
+ step("Installing dependencies (npm install)");
69
+ execSync("npm install --no-audit --no-fund", { cwd: join(CLAUDE, "glm-mcp"), stdio: "inherit" });
70
+ }
71
+
72
+ // 4. Subagent + hook
73
+ step("Installing subagent and auto-delegation hook");
74
+ mkdirSync(join(CLAUDE, "agents"), { recursive: true });
75
+ mkdirSync(join(CLAUDE, "hooks"), { recursive: true });
76
+ copyFileSync(join(SELF, "agents", "glm.md"), join(CLAUDE, "agents", "glm.md"));
77
+ copyFileSync(join(SELF, "hooks", "glm_subagent_router.mjs"), join(CLAUDE, "hooks", "glm_subagent_router.mjs"));
78
+ log(" agents/glm.md, hooks/glm_subagent_router.mjs");
79
+
80
+ // 5. Merge the PreToolUse hook into settings.json (idempotent, with backup).
81
+ step("Wiring the hook into settings.json");
82
+ const settingsPath = join(CLAUDE, "settings.json");
83
+ let settings = {};
84
+ if (existsSync(settingsPath)) {
85
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch { settings = {}; }
86
+ writeFileSync(settingsPath + ".bak-" + Date.now(), readFileSync(settingsPath));
87
+ }
88
+ const hookCmd = `node "${join(CLAUDE, "hooks", "glm_subagent_router.mjs").replace(/\\/g, "/")}"`;
89
+ settings.hooks ||= {};
90
+ settings.hooks.PreToolUse ||= [];
91
+ const already = settings.hooks.PreToolUse.some((e) =>
92
+ (e.hooks || []).some((h) => typeof h.command === "string" && h.command.includes("glm_subagent_router"))
93
+ );
94
+ if (!already) {
95
+ settings.hooks.PreToolUse.push({ matcher: "Task", hooks: [{ type: "command", command: hookCmd }] });
96
+ log(" added PreToolUse(Task) -> glm_subagent_router");
97
+ } else {
98
+ log(" hook already present (left as-is)");
99
+ }
100
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
101
+
102
+ // 6. Append the global delegation policy (idempotent).
103
+ step("Adding global delegation policy to CLAUDE.md");
104
+ const claudeMd = join(CLAUDE, "CLAUDE.md");
105
+ const policy = `\n# Delegation (GLM hybrid)\n\nPrefer offloading well-specified, self-contained subtasks. When you spawn a subagent (the\n\`Task\` tool), a PreToolUse hook injects a \`[GLM router]\` line advising whether to route it to\nthe cheaper \`glm\` subagent / \`mcp__glm__glm_agent\` or keep it on Opus -- follow that advice.\nIf the user explicitly named an agent, honor it. Full rules: \`~/.claude/glm-mcp/README.md\`.\n`;
106
+ const existing = existsSync(claudeMd) ? readFileSync(claudeMd, "utf8") : "";
107
+ if (!existing.includes("[GLM router]")) {
108
+ writeFileSync(claudeMd, existing + policy);
109
+ log(" policy appended");
110
+ } else {
111
+ log(" policy already present");
112
+ }
113
+
114
+ // 7. Register the MCP server (user scope) via the claude CLI.
115
+ if (!NO_REGISTER) {
116
+ step("Registering MCP server with Claude Code (user scope)");
117
+ const idx = join(CLAUDE, "glm-mcp", "src", "index.js").replace(/\\/g, "/");
118
+ try {
119
+ try { execSync("claude mcp remove glm -s user", { stdio: "ignore" }); } catch {}
120
+ execSync(`claude mcp add glm -s user -- node "${idx}"`, { stdio: "inherit" });
121
+ log(" registered (key is read from .env).");
122
+ } catch (e) {
123
+ log(" ⚠ Could not run `claude mcp add` (" + e.message + ").");
124
+ log(" Register manually:");
125
+ log(` claude mcp add glm -s user -- node "${idx}"`);
126
+ }
127
+ }
128
+
129
+ log("\n✅ Done. Next steps:");
130
+ log(" 1. Ensure GLM_API_KEY is set in " + envPath);
131
+ log(" 2. RESTART Claude Code, then run `glm_status` to verify (api_key_loaded: true).");
132
+ log(" 3. Your main agent stays on whatever model Claude Code uses; GLM handles delegated subtasks.");
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "glm-mcp-claude",
3
+ "version": "1.0.0",
4
+ "description": "GLM (Zhipu/Z.ai) as a cheap, full-capability subagent for Claude Code — auto-routing between Opus and GLM, a file-editing agent with diff/dry-run/git-revert oversight, and a one-command installer.",
5
+ "type": "module",
6
+ "bin": {
7
+ "glm-mcp-claude": "install.mjs"
8
+ },
9
+ "files": [
10
+ "glm-mcp/",
11
+ "agents/",
12
+ "hooks/",
13
+ "docs/",
14
+ "assets/",
15
+ "install.mjs",
16
+ "uninstall.mjs",
17
+ ".mcp.json.example",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "claude-code",
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "glm",
29
+ "zhipu",
30
+ "z.ai",
31
+ "subagent",
32
+ "ai-agents",
33
+ "anthropic",
34
+ "llm",
35
+ "cost-optimization"
36
+ ],
37
+ "author": "djerok",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/djerok/glm_mcp_claude.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/djerok/glm_mcp_claude/issues"
45
+ },
46
+ "homepage": "https://github.com/djerok/glm_mcp_claude#readme"
47
+ }
package/uninstall.mjs ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // uninstall.mjs -- removes what install.mjs added.
3
+ // node uninstall.mjs # remove from ~/.claude
4
+ // node uninstall.mjs --claude-dir P # custom dir
5
+ // node uninstall.mjs --purge # also delete the glm-mcp server folder (and your .env!)
6
+
7
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join, dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { execSync } from "node:child_process";
12
+
13
+ const args = process.argv.slice(2);
14
+ const getOpt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
15
+ const CLAUDE = getOpt("--claude-dir") || join(homedir(), ".claude");
16
+ const PURGE = args.includes("--purge");
17
+
18
+ console.log("Uninstalling GLM hybrid from " + CLAUDE);
19
+
20
+ try { execSync("claude mcp remove glm -s user", { stdio: "inherit" }); } catch { console.log(" (claude mcp remove skipped/failed)"); }
21
+
22
+ const settingsPath = join(CLAUDE, "settings.json");
23
+ if (existsSync(settingsPath)) {
24
+ try {
25
+ const s = JSON.parse(readFileSync(settingsPath, "utf8"));
26
+ if (s.hooks?.PreToolUse) {
27
+ s.hooks.PreToolUse = s.hooks.PreToolUse.filter(
28
+ (e) => !(e.hooks || []).some((h) => typeof h.command === "string" && h.command.includes("glm_subagent_router"))
29
+ );
30
+ if (!s.hooks.PreToolUse.length) delete s.hooks.PreToolUse;
31
+ writeFileSync(settingsPath, JSON.stringify(s, null, 2) + "\n");
32
+ console.log(" removed hook from settings.json");
33
+ }
34
+ } catch (e) { console.log(" settings.json edit failed: " + e.message); }
35
+ }
36
+
37
+ for (const f of [join(CLAUDE, "agents", "glm.md"), join(CLAUDE, "hooks", "glm_subagent_router.mjs")]) {
38
+ if (existsSync(f)) { rmSync(f); console.log(" removed " + f); }
39
+ }
40
+
41
+ if (PURGE && existsSync(join(CLAUDE, "glm-mcp"))) {
42
+ rmSync(join(CLAUDE, "glm-mcp"), { recursive: true, force: true });
43
+ console.log(" purged glm-mcp/ (including .env)");
44
+ }
45
+
46
+ console.log("\nNote: the '# Delegation (GLM hybrid)' block in CLAUDE.md was left in place -- remove it by hand if you want.");
47
+ console.log("Done. Restart Claude Code.");