glm-coding-router 0.5.0 → 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.
- package/README.md +34 -0
- package/dist/bin/glm-mcp.js +25 -0
- package/dist/cli.js +13 -0
- package/dist/commands/doctor.js +10 -9
- package/dist/commands/mcp.js +72 -0
- package/dist/commands/skill.js +48 -36
- package/dist/commands/status.js +21 -8
- package/dist/commands/usage.js +1 -1
- package/dist/integrations/skill.js +31 -12
- package/dist/mcp/server.js +232 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -253,6 +253,39 @@ glm-router usage
|
|
|
253
253
|
|
|
254
254
|
`--json` emits the same data machine-readably. No key configured → `ERROR [10]`.
|
|
255
255
|
|
|
256
|
+
## Agent skills (Claude Code + Codex)
|
|
257
|
+
|
|
258
|
+
`glm-router skill install` writes the `glm-delegation` SKILL.md into **both**
|
|
259
|
+
agent homes — `~/.claude/skills/` and `~/.codex/skills/` — so either
|
|
260
|
+
orchestrator natively knows how to delegate to GLM workers. Missing homes are
|
|
261
|
+
skipped with a note (optional enhancement, never fatal); `skill remove`
|
|
262
|
+
cleans both. `status` shows one skill row per agent.
|
|
263
|
+
|
|
264
|
+
## MCP server (optional)
|
|
265
|
+
|
|
266
|
+
`glm-mcp` (installed with the package) exposes the router as MCP tools over
|
|
267
|
+
stdio — any MCP client can delegate without shell syntax:
|
|
268
|
+
|
|
269
|
+
| Tool | What it does |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `glm_worker(prompt, profile?)` | implementation worker, returns output |
|
|
272
|
+
| `glm_review(prompt, profile?)` | read-only review/exploration |
|
|
273
|
+
| `glm_delegate(name, prompt)` | worker in an isolated git worktree |
|
|
274
|
+
| `glm_usage()` | Z.ai quota windows + local benchmark totals |
|
|
275
|
+
|
|
276
|
+
Register it with Claude Code (we never edit `~/.claude.json` ourselves — it
|
|
277
|
+
goes through Claude's own CLI):
|
|
278
|
+
|
|
279
|
+
```powershell
|
|
280
|
+
glm-router mcp # prints the snippet + the exact command
|
|
281
|
+
glm-router mcp install # claude mcp add -s user glm-coding-router -- node .../glm-mcp.js
|
|
282
|
+
glm-router mcp remove # claude mcp remove -s user glm-coding-router
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Tool-level failures return `isError` results (missing key, no claude, outside
|
|
286
|
+
a git repo, unreachable endpoint); the server never prints anything to stdout
|
|
287
|
+
except JSON-RPC frames.
|
|
288
|
+
|
|
256
289
|
## CLI reference
|
|
257
290
|
|
|
258
291
|
```text
|
|
@@ -266,6 +299,7 @@ glm-router config set models.main glm-5.3
|
|
|
266
299
|
glm-router delegate <name> run a GLM worker in an isolated git worktree
|
|
267
300
|
glm-router benchmark measure the Claude+GLM stack on built-in tasks
|
|
268
301
|
glm-router usage Z.ai quota snapshot + local benchmark totals
|
|
302
|
+
glm-router mcp optional MCP server registration (glm-mcp)
|
|
269
303
|
glm-router project init CLAUDE.md / AGENTS.md managed blocks (--dry-run supported)
|
|
270
304
|
glm-router project remove
|
|
271
305
|
glm-router skill install optional Codex delegation skill
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { isMainModule } from "../core/main-guard.js";
|
|
4
|
+
import { createMcpServer } from "../mcp/server.js";
|
|
5
|
+
/**
|
|
6
|
+
* glm-mcp (specs/v1-architecture.md): MCP server over stdio. Responses are
|
|
7
|
+
* serialized through a write queue so concurrent tool calls can never
|
|
8
|
+
* interleave frames. Stderr stays free for anything unexpected.
|
|
9
|
+
*/
|
|
10
|
+
if (isMainModule(import.meta.url)) {
|
|
11
|
+
const server = createMcpServer();
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
13
|
+
let queue = Promise.resolve();
|
|
14
|
+
rl.on("line", (line) => {
|
|
15
|
+
queue = queue.then(async () => {
|
|
16
|
+
const frame = await server.handleLine(line);
|
|
17
|
+
if (frame !== null) {
|
|
18
|
+
process.stdout.write(`${frame}\n`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
rl.on("close", () => {
|
|
23
|
+
void queue.then(() => process.exit(0));
|
|
24
|
+
});
|
|
25
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { uninstallCommand } from "./commands/uninstall.js";
|
|
|
16
16
|
import { delegateCommand } from "./commands/delegate.js";
|
|
17
17
|
import { benchmarkCommand } from "./commands/benchmark.js";
|
|
18
18
|
import { usageCommand } from "./commands/usage.js";
|
|
19
|
+
import { mcpCommand } from "./commands/mcp.js";
|
|
19
20
|
const program = new Command();
|
|
20
21
|
program
|
|
21
22
|
.name("glm-router")
|
|
@@ -108,6 +109,18 @@ program
|
|
|
108
109
|
.command("usage")
|
|
109
110
|
.description("provider usage snapshots: Z.ai Coding Plan quota + local benchmark totals")
|
|
110
111
|
.action(() => execute(() => usageCommand(globalOptions())));
|
|
112
|
+
const mcp = program
|
|
113
|
+
.command("mcp")
|
|
114
|
+
.description("optional glm-mcp MCP server: snippet, install, remove")
|
|
115
|
+
.action(() => execute(() => mcpCommand(globalOptions(), "info")));
|
|
116
|
+
mcp
|
|
117
|
+
.command("install")
|
|
118
|
+
.description("register glm-mcp with Claude Code (claude mcp add -s user)")
|
|
119
|
+
.action(() => execute(() => mcpCommand(globalOptions(), "install")));
|
|
120
|
+
mcp
|
|
121
|
+
.command("remove")
|
|
122
|
+
.description("unregister glm-mcp from Claude Code (claude mcp remove -s user)")
|
|
123
|
+
.action(() => execute(() => mcpCommand(globalOptions(), "remove")));
|
|
111
124
|
program
|
|
112
125
|
.command("uninstall")
|
|
113
126
|
.description("guided removal (keeps ZAI_API_KEY by default)")
|
package/dist/commands/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isWindows, windowsVersionName } from "../core/platform.js";
|
|
|
6
6
|
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
7
7
|
import { configPath } from "../core/paths.js";
|
|
8
8
|
import fs from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import { skillTargets } from "../integrations/skill.js";
|
|
10
10
|
import { GLM_DELEGATION_SKILL_NAME } from "../templates/glm-delegation-skill.js";
|
|
11
11
|
function check(section, name, status, detail, note) {
|
|
12
12
|
return { section, name, status, detail, note };
|
|
@@ -71,14 +71,15 @@ export function runDoctorChecks(options = {}) {
|
|
|
71
71
|
// --- Claude / Codex integrations ---
|
|
72
72
|
results.push(check("Claude", "Integration", config.integrations.claude ? "ok" : "warn", config.integrations.claude ? "enabled" : "disabled in config"));
|
|
73
73
|
results.push(check("Codex", "AGENTS.md integration", config.integrations.codex ? "ok" : "warn", config.integrations.codex ? "enabled" : "disabled in config"));
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
for (const { agent, installer } of skillTargets(home)) {
|
|
75
|
+
const homeDetected = installer.detect() !== null;
|
|
76
|
+
const skillInstalled = homeDetected && installer.isInstalled(GLM_DELEGATION_SKILL_NAME);
|
|
77
|
+
results.push(check(agent, "Delegation skill", skillInstalled ? "ok" : "warn", skillInstalled
|
|
78
|
+
? "installed"
|
|
79
|
+
: homeDetected
|
|
80
|
+
? "not installed (optional)"
|
|
81
|
+
: `${agent} home not detected — skill skipped (optional)`));
|
|
82
|
+
}
|
|
82
83
|
// --- Environment: the Orca stale-env case (spec §9, §10) ---
|
|
83
84
|
const hasProcessKey = Boolean(env.ZAI_API_KEY && env.ZAI_API_KEY.trim());
|
|
84
85
|
if (hasProcessKey) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { loadConfig } from "../core/config.js";
|
|
7
|
+
import { locateClaude } from "../core/claude.js";
|
|
8
|
+
import { Errors } from "../core/errors.js";
|
|
9
|
+
export const MCP_SERVER_NAME = "glm-coding-router";
|
|
10
|
+
/** Absolute path to the compiled glm-mcp entry next to this module. */
|
|
11
|
+
export function mcpServerScript() {
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url)); // …/commands (dist or src)
|
|
13
|
+
const compiled = path.resolve(here, "..", "bin", "glm-mcp.js");
|
|
14
|
+
if (fs.existsSync(compiled)) {
|
|
15
|
+
return compiled;
|
|
16
|
+
}
|
|
17
|
+
return path.resolve(here, "..", "bin", "glm-mcp.ts"); // dev checkout via tsx
|
|
18
|
+
}
|
|
19
|
+
function defaultRunClaude(binPath, args) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
execFile(binPath, args, { windowsHide: true, encoding: "utf8", timeout: 60_000 }, (error, stdout, stderr) => {
|
|
22
|
+
const code = error && typeof error.code === "number" ? error.code : 0;
|
|
23
|
+
resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* glm-router mcp (specs/v1-architecture.md): opt-in registration of the
|
|
29
|
+
* glm-mcp server. We never edit ~/.claude.json ourselves — install/remove go
|
|
30
|
+
* through Claude Code's own `claude mcp add/remove` CLI.
|
|
31
|
+
*/
|
|
32
|
+
export async function mcpCommand(options, action = "info", deps = {}) {
|
|
33
|
+
const script = mcpServerScript();
|
|
34
|
+
const node = process.execPath;
|
|
35
|
+
if (action === "info") {
|
|
36
|
+
const snippet = JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: node, args: [script] } } }, null, 2);
|
|
37
|
+
process.stdout.write([
|
|
38
|
+
"Optional MCP server: glm-mcp exposes glm_worker, glm_review, glm_delegate, glm_usage as MCP tools.",
|
|
39
|
+
"",
|
|
40
|
+
"Register it with Claude Code:",
|
|
41
|
+
"",
|
|
42
|
+
` claude mcp add -s user ${MCP_SERVER_NAME} -- "${node}" "${script}"`,
|
|
43
|
+
"",
|
|
44
|
+
"Or add this to your MCP client config:",
|
|
45
|
+
"",
|
|
46
|
+
snippet,
|
|
47
|
+
"",
|
|
48
|
+
"Remove again with:",
|
|
49
|
+
"",
|
|
50
|
+
` claude mcp remove -s user ${MCP_SERVER_NAME}`,
|
|
51
|
+
"",
|
|
52
|
+
].join("\n"));
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const home = deps.home ?? os.homedir();
|
|
56
|
+
const env = deps.env ?? process.env;
|
|
57
|
+
const config = loadConfig(home);
|
|
58
|
+
const claudePath = locateClaude(config, env); // ERROR [20] when missing
|
|
59
|
+
const runClaude = deps.runClaude ?? defaultRunClaude;
|
|
60
|
+
const args = action === "install"
|
|
61
|
+
? ["mcp", "add", "-s", "user", MCP_SERVER_NAME, "--", node, script]
|
|
62
|
+
: ["mcp", "remove", "-s", "user", MCP_SERVER_NAME];
|
|
63
|
+
const result = await runClaude(claudePath, args);
|
|
64
|
+
if (result.code !== 0) {
|
|
65
|
+
throw Errors.childAgentFailed(`claude mcp ${action} exited ${result.code}${result.stderr.trim() ? `: ${result.stderr.trim()}` : ""}`);
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write(`✓ ${MCP_SERVER_NAME} MCP server ${action === "install" ? "registered" : "removed"} (claude -s user scope)\n`);
|
|
68
|
+
if (result.stdout.trim() && !options.quiet) {
|
|
69
|
+
process.stdout.write(`${result.stdout.trim()}\n`);
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
package/dist/commands/skill.js
CHANGED
|
@@ -1,43 +1,55 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
-
import {
|
|
3
|
-
function
|
|
4
|
-
return new CodexSkillInstaller(os.homedir());
|
|
5
|
-
}
|
|
6
|
-
/** glm-router skill install (spec §27): optional enhancement; warn+skip when unsupported. */
|
|
7
|
-
export function skillInstallCommand(options) {
|
|
8
|
-
const skillInstaller = installer();
|
|
9
|
-
const location = skillInstaller.detect();
|
|
10
|
-
if (!location) {
|
|
11
|
-
process.stdout.write("⚠ Codex skill directory not detected (~/.codex not found).\n" +
|
|
12
|
-
" Skipping optional skill — AGENTS.md integration keeps working.\n");
|
|
13
|
-
return 0;
|
|
14
|
-
}
|
|
2
|
+
import { glmDelegationSkill, skillTargets } from "../integrations/skill.js";
|
|
3
|
+
function runForEach(options, action, home) {
|
|
15
4
|
const skill = glmDelegationSkill();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
const results = [];
|
|
6
|
+
for (const { agent, installer } of skillTargets(home)) {
|
|
7
|
+
const location = installer.detect();
|
|
8
|
+
if (!location) {
|
|
9
|
+
results.push({ agent, outcome: "skipped", detail: "home not detected — skipping optional skill" });
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (action === "install") {
|
|
13
|
+
if (installer.isInstalled(skill.name) && !options.force) {
|
|
14
|
+
results.push({ agent, outcome: "already", detail: `already installed at ${location.skillsDir}` });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (options.dryRun) {
|
|
18
|
+
results.push({ agent, outcome: "skipped", detail: `[dry-run] would install to ${location.skillsDir}` });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
installer.install(skill);
|
|
22
|
+
results.push({ agent, outcome: "installed", detail: `installed at ${location.skillsDir}` });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
if (!installer.isInstalled(skill.name)) {
|
|
26
|
+
results.push({ agent, outcome: "absent", detail: "not installed" });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (options.dryRun) {
|
|
30
|
+
results.push({ agent, outcome: "skipped", detail: "[dry-run] would remove skill" });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
installer.remove(skill.name);
|
|
34
|
+
results.push({ agent, outcome: "removed", detail: "removed" });
|
|
35
|
+
}
|
|
19
36
|
}
|
|
20
|
-
|
|
21
|
-
process.stdout.write(`[dry-run] would install skill "${skill.name}" to ${location.skillsDir}\n`);
|
|
22
|
-
return 0;
|
|
23
|
-
}
|
|
24
|
-
skillInstaller.install(skill);
|
|
25
|
-
process.stdout.write(`✓ Skill "${skill.name}" installed at ${location.skillsDir}\n`);
|
|
26
|
-
return 0;
|
|
37
|
+
return results;
|
|
27
38
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const skill = glmDelegationSkill();
|
|
32
|
-
if (!skillInstaller.isInstalled(skill.name)) {
|
|
33
|
-
process.stdout.write(`✓ Skill "${skill.name}" is not installed\n`);
|
|
34
|
-
return 0;
|
|
35
|
-
}
|
|
36
|
-
if (options.dryRun) {
|
|
37
|
-
process.stdout.write(`[dry-run] would remove skill "${skill.name}"\n`);
|
|
38
|
-
return 0;
|
|
39
|
+
function render(results) {
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
process.stdout.write(`✓ ${result.agent}: ${result.detail}\n`);
|
|
39
42
|
}
|
|
40
|
-
skillInstaller.remove(skill.name);
|
|
41
|
-
process.stdout.write(`✓ Skill "${skill.name}" removed\n`);
|
|
42
43
|
return 0;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* glm-router skill install (spec §27, specs/v1-architecture.md): optional
|
|
47
|
+
* enhancement for BOTH agents; per-agent warn+skip, never fatal.
|
|
48
|
+
*/
|
|
49
|
+
export function skillInstallCommand(options, deps = {}) {
|
|
50
|
+
return render(runForEach(options, "install", deps.home ?? os.homedir()));
|
|
51
|
+
}
|
|
52
|
+
/** glm-router skill remove — removes from both agents. */
|
|
53
|
+
export function skillRemoveCommand(options, deps = {}) {
|
|
54
|
+
return render(runForEach(options, "remove", deps.home ?? os.homedir()));
|
|
55
|
+
}
|
package/dist/commands/status.js
CHANGED
|
@@ -3,7 +3,7 @@ import { loadConfig } from "../core/config.js";
|
|
|
3
3
|
import { locateClaude, locateCodex } from "../core/claude.js";
|
|
4
4
|
import { version } from "../core/version.js";
|
|
5
5
|
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
6
|
-
import {
|
|
6
|
+
import { skillTargets } from "../integrations/skill.js";
|
|
7
7
|
import { GLM_DELEGATION_SKILL_NAME } from "../templates/glm-delegation-skill.js";
|
|
8
8
|
import { emitJson } from "./context.js";
|
|
9
9
|
/** Fast, fully offline summary (spec §41) — no API requests, no key values. */
|
|
@@ -22,8 +22,17 @@ export function statusCommand(options, deps = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
})();
|
|
24
24
|
const codexInstalled = Boolean(locateCodex(config, env));
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const skillState = (() => {
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const { agent, installer } of skillTargets(home)) {
|
|
28
|
+
rows.push({
|
|
29
|
+
agent,
|
|
30
|
+
homeDetected: installer.detect() !== null,
|
|
31
|
+
installed: installer.isInstalled(GLM_DELEGATION_SKILL_NAME),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return rows;
|
|
35
|
+
})();
|
|
27
36
|
if (options.json) {
|
|
28
37
|
emitJson({
|
|
29
38
|
version,
|
|
@@ -33,7 +42,10 @@ export function statusCommand(options, deps = {}) {
|
|
|
33
42
|
integrations: {
|
|
34
43
|
claude: config.integrations.claude,
|
|
35
44
|
codex: config.integrations.codex,
|
|
36
|
-
|
|
45
|
+
skills: skillState.map((row) => ({
|
|
46
|
+
agent: row.agent,
|
|
47
|
+
enabled: row.homeDetected && row.installed,
|
|
48
|
+
})),
|
|
37
49
|
},
|
|
38
50
|
models: config.models,
|
|
39
51
|
});
|
|
@@ -48,11 +60,12 @@ export function statusCommand(options, deps = {}) {
|
|
|
48
60
|
"",
|
|
49
61
|
`Claude policy ${config.integrations.claude ? "enabled" : "disabled"}`,
|
|
50
62
|
`Codex policy ${config.integrations.codex ? "enabled" : "disabled"}`,
|
|
51
|
-
`Codex skill ${skillInstalled ? "enabled" : "disabled"}`,
|
|
52
|
-
"",
|
|
53
|
-
`Main model ${config.models.main}`,
|
|
54
|
-
`Fast model ${config.models.fast}`,
|
|
55
63
|
];
|
|
64
|
+
for (const row of skillState) {
|
|
65
|
+
const enabled = row.homeDetected && row.installed;
|
|
66
|
+
lines.push(`${row.agent} skill ${enabled ? "enabled" : "disabled"}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push("", `Main model ${config.models.main}`, `Fast model ${config.models.fast}`);
|
|
56
69
|
process.stdout.write(lines.join("\n") + "\n");
|
|
57
70
|
return 0;
|
|
58
71
|
}
|
package/dist/commands/usage.js
CHANGED
|
@@ -19,7 +19,7 @@ function describeWindow(limit) {
|
|
|
19
19
|
return `window unit=${String(limit.unit)} x ${String(limit.number)}`;
|
|
20
20
|
}
|
|
21
21
|
/** Fetch and validate the Z.ai quota snapshot. Never logs the Authorization header. */
|
|
22
|
-
async function fetchZaiQuota(key, fetchImpl) {
|
|
22
|
+
export async function fetchZaiQuota(key, fetchImpl) {
|
|
23
23
|
let response;
|
|
24
24
|
try {
|
|
25
25
|
response = await fetchImpl(ZAI_QUOTA_URL, {
|
|
@@ -3,33 +3,52 @@ import path from "node:path";
|
|
|
3
3
|
import { atomicWriteFile } from "../project/atomic-write.js";
|
|
4
4
|
import { GLM_DELEGATION_SKILL_MD, GLM_DELEGATION_SKILL_NAME, } from "../templates/glm-delegation-skill.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Detection
|
|
8
|
-
* supported
|
|
6
|
+
* Shared SKILL.md-folder mechanics; subclasses only pick the agent home dir
|
|
7
|
+
* (specs/v1-architecture.md). Detection stays conservative: a missing home
|
|
8
|
+
* means no supported installation to enhance — return null, callers warn+skip.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
this.
|
|
10
|
+
class HomeDirSkillInstaller {
|
|
11
|
+
agentHome;
|
|
12
|
+
constructor(agentHome) {
|
|
13
|
+
this.agentHome = agentHome;
|
|
14
14
|
}
|
|
15
15
|
detect() {
|
|
16
|
-
if (!fs.existsSync(this.
|
|
16
|
+
if (!fs.existsSync(this.agentHome)) {
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
-
return { skillsDir: path.join(this.
|
|
19
|
+
return { skillsDir: path.join(this.agentHome, "skills") };
|
|
20
20
|
}
|
|
21
21
|
install(skill) {
|
|
22
|
-
const skillDir = path.join(this.
|
|
22
|
+
const skillDir = path.join(this.agentHome, "skills", skill.name);
|
|
23
23
|
atomicWriteFile(path.join(skillDir, "SKILL.md"), skill.content);
|
|
24
24
|
}
|
|
25
25
|
remove(name) {
|
|
26
|
-
const skillDir = path.join(this.
|
|
26
|
+
const skillDir = path.join(this.agentHome, "skills", name);
|
|
27
27
|
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
28
28
|
}
|
|
29
29
|
isInstalled(name) {
|
|
30
|
-
return fs.existsSync(path.join(this.
|
|
30
|
+
return fs.existsSync(path.join(this.agentHome, "skills", name, "SKILL.md"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Installs skills into the Codex home (~/.codex/skills). */
|
|
34
|
+
export class CodexSkillInstaller extends HomeDirSkillInstaller {
|
|
35
|
+
constructor(home) {
|
|
36
|
+
super(path.join(home, ".codex"));
|
|
31
37
|
}
|
|
32
38
|
}
|
|
39
|
+
/** Installs skills into the Claude Code home (~/.claude/skills, specs/v1-architecture.md). */
|
|
40
|
+
export class ClaudeSkillInstaller extends HomeDirSkillInstaller {
|
|
41
|
+
constructor(home) {
|
|
42
|
+
super(path.join(home, ".claude"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Every agent the delegation skill supports, in stable display order. */
|
|
46
|
+
export function skillTargets(home) {
|
|
47
|
+
return [
|
|
48
|
+
{ agent: "Claude", installer: new ClaudeSkillInstaller(home) },
|
|
49
|
+
{ agent: "Codex", installer: new CodexSkillInstaller(home) },
|
|
50
|
+
];
|
|
51
|
+
}
|
|
33
52
|
export function glmDelegationSkill() {
|
|
34
53
|
return { name: GLM_DELEGATION_SKILL_NAME, content: GLM_DELEGATION_SKILL_MD };
|
|
35
54
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { buildWorkerArgs } from "../bin/glm-worker.js";
|
|
3
|
+
import { buildReviewArgs } from "../bin/glm-review.js";
|
|
4
|
+
import { loadConfig } from "../core/config.js";
|
|
5
|
+
import { locateClaude } from "../core/claude.js";
|
|
6
|
+
import { createGlmEnv } from "../core/env.js";
|
|
7
|
+
import { Errors, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
8
|
+
import { gitTopLevel } from "../core/git.js";
|
|
9
|
+
import { spawnAgentCapture } from "../core/process.js";
|
|
10
|
+
import { applyProfile } from "../core/profile.js";
|
|
11
|
+
import { version } from "../core/version.js";
|
|
12
|
+
import { createDelegateWorktree, delegateBranch, removeDelegateWorktree, rollbackDelegateBranch, validateDelegateName, } from "../core/worktree.js";
|
|
13
|
+
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
14
|
+
import { aggregateLocalUsage, fetchZaiQuota } from "../commands/usage.js";
|
|
15
|
+
const PROMPT_PROPERTY = { type: "string", description: "The task prompt for the GLM agent." };
|
|
16
|
+
export const MCP_TOOLS = [
|
|
17
|
+
{
|
|
18
|
+
name: "glm_worker",
|
|
19
|
+
description: "Run a GLM implementation worker (claude.exe + Z.ai env; tools Read,Glob,Grep,Edit,Write,Bash; acceptEdits). Returns the worker's output.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: { prompt: PROMPT_PROPERTY, profile: { type: "string", description: "Config profile overlay (optional)." } },
|
|
23
|
+
required: ["prompt"],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "glm_review",
|
|
28
|
+
description: "Run a read-only GLM review/exploration worker (tools Read,Glob,Grep only — cannot edit or run commands).",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: { prompt: PROMPT_PROPERTY, profile: { type: "string", description: "Config profile overlay (optional)." } },
|
|
32
|
+
required: ["prompt"],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "glm_delegate",
|
|
37
|
+
description: "Run a GLM worker in an isolated git worktree (branch glm/delegate/<name> from HEAD). Worktree and branch are kept afterwards — nothing is committed or merged automatically.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
name: { type: "string", description: "Delegate slug, e.g. backend or tests." },
|
|
42
|
+
prompt: PROMPT_PROPERTY,
|
|
43
|
+
},
|
|
44
|
+
required: ["name", "prompt"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "glm_usage",
|
|
49
|
+
description: "Usage snapshot: Z.ai Coding Plan credit windows (5h/weekly) and local benchmark totals.",
|
|
50
|
+
inputSchema: { type: "object", properties: {} },
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
function requiredString(args, key) {
|
|
54
|
+
const value = args[key];
|
|
55
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
56
|
+
throw new Error(`"${key}" is required and must be a non-empty string.`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function optionalString(args, key) {
|
|
61
|
+
const value = args[key];
|
|
62
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
63
|
+
}
|
|
64
|
+
function errorText(error) {
|
|
65
|
+
if (error instanceof GlmRouterError) {
|
|
66
|
+
return { text: formatGlmError(error), isError: true };
|
|
67
|
+
}
|
|
68
|
+
return { text: error instanceof Error ? error.message : String(error), isError: true };
|
|
69
|
+
}
|
|
70
|
+
function tail(text, max = 400) {
|
|
71
|
+
return text.length > max ? `…${text.slice(-max)}` : text;
|
|
72
|
+
}
|
|
73
|
+
async function runAgent(prompt, profile, kind, deps) {
|
|
74
|
+
const home = deps.home ?? os.homedir();
|
|
75
|
+
const env = deps.env ?? process.env;
|
|
76
|
+
const config = applyProfile(loadConfig(home), profile);
|
|
77
|
+
const resolved = resolveZaiApiKey({ env, readUserEnv: deps.readUserEnv });
|
|
78
|
+
if (!resolved) {
|
|
79
|
+
throw Errors.zaiKeyMissing();
|
|
80
|
+
}
|
|
81
|
+
const claudePath = locateClaude(config, env);
|
|
82
|
+
const args = kind === "worker" ? buildWorkerArgs(prompt, config) : buildReviewArgs(prompt, config);
|
|
83
|
+
const spawn = deps.spawn ?? spawnAgentCapture;
|
|
84
|
+
const captured = await spawn(claudePath, {
|
|
85
|
+
args,
|
|
86
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
87
|
+
env: createGlmEnv(config, resolved.key, env),
|
|
88
|
+
interactive: false,
|
|
89
|
+
});
|
|
90
|
+
const text = captured.stdout.trim() || "(no output)";
|
|
91
|
+
if (captured.code !== 0) {
|
|
92
|
+
return { text: `${text}\n[worker exited ${captured.code}]${captured.stderr ? `\n${tail(captured.stderr.trim())}` : ""}`, isError: true };
|
|
93
|
+
}
|
|
94
|
+
return { text, isError: false };
|
|
95
|
+
}
|
|
96
|
+
async function delegate(name, prompt, deps) {
|
|
97
|
+
validateDelegateName(name);
|
|
98
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
99
|
+
const repoRoot = await gitTopLevel(cwd, deps.runGit);
|
|
100
|
+
if (!repoRoot) {
|
|
101
|
+
return { text: `Not inside a git repository (cwd: ${cwd}) — glm_delegate needs a repo root.`, isError: true };
|
|
102
|
+
}
|
|
103
|
+
const worktree = await createDelegateWorktree(repoRoot, name, { runGit: deps.runGit });
|
|
104
|
+
const branch = delegateBranch(name);
|
|
105
|
+
const summary = (exitCode, output) => ({
|
|
106
|
+
text: [
|
|
107
|
+
output.trim() || "(no output)",
|
|
108
|
+
"",
|
|
109
|
+
`worker exited ${exitCode}`,
|
|
110
|
+
`worktree kept: ${worktree}`,
|
|
111
|
+
`branch kept: ${branch}`,
|
|
112
|
+
`next: inspect ${worktree}, then merge ${branch} (or discard with git worktree remove)`,
|
|
113
|
+
].join("\n"),
|
|
114
|
+
isError: exitCode !== 0,
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const result = await runAgent(prompt, undefined, "worker", { ...deps, cwd: worktree });
|
|
118
|
+
return summary(result.isError ? 1 : 0, result.text);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// Worker never started — roll back the pristine worktree + branch (specs/delegate-worktrees.md).
|
|
122
|
+
await removeDelegateWorktree(repoRoot, worktree, { runGit: deps.runGit });
|
|
123
|
+
await rollbackDelegateBranch(repoRoot, branch, { runGit: deps.runGit });
|
|
124
|
+
return errorText(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function usage(deps) {
|
|
128
|
+
const home = deps.home ?? os.homedir();
|
|
129
|
+
const env = deps.env ?? process.env;
|
|
130
|
+
const resolved = resolveZaiApiKey({ env, readUserEnv: deps.readUserEnv });
|
|
131
|
+
if (!resolved) {
|
|
132
|
+
throw Errors.zaiKeyMissing();
|
|
133
|
+
}
|
|
134
|
+
const lines = [];
|
|
135
|
+
let isError = false;
|
|
136
|
+
try {
|
|
137
|
+
const quota = await fetchZaiQuota(resolved.key, deps.fetchImpl ?? fetch);
|
|
138
|
+
lines.push(`Z.ai Coding Plan${quota.level ? ` (level: ${quota.level})` : ""}`);
|
|
139
|
+
for (const limit of quota.limits ?? []) {
|
|
140
|
+
const resets = typeof limit.nextResetTime === "number" ? ` — resets ${new Date(limit.nextResetTime).toISOString()}` : "";
|
|
141
|
+
lines.push(` ${String(limit.number ?? "?")}x unit ${String(limit.unit ?? "?")}: ${String(limit.currentValue ?? "?")} / ${String(limit.usage ?? "?")} credits (${String(limit.percentage ?? "?")}%)${resets}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
isError = true;
|
|
146
|
+
lines.push(`Z.ai Coding Plan: ✗ ${error instanceof Error ? error.message : String(error)}`);
|
|
147
|
+
}
|
|
148
|
+
const local = aggregateLocalUsage(home);
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push(local.runs === 0
|
|
151
|
+
? "Local (benchmark reports): (none yet)"
|
|
152
|
+
: `Local (benchmark reports): runs ${local.runs} · tokens ${local.tokensIn} in / ${local.tokensOut} out · last ${local.lastFinishedAt}`);
|
|
153
|
+
return { text: lines.join("\n"), isError };
|
|
154
|
+
}
|
|
155
|
+
/** Execute one MCP tool call. Tool-level failures return isError, never throw. */
|
|
156
|
+
export async function callMcpTool(name, args, deps = {}) {
|
|
157
|
+
try {
|
|
158
|
+
switch (name) {
|
|
159
|
+
case "glm_worker":
|
|
160
|
+
return await runAgent(requiredString(args, "prompt"), optionalString(args, "profile"), "worker", deps);
|
|
161
|
+
case "glm_review":
|
|
162
|
+
return await runAgent(requiredString(args, "prompt"), optionalString(args, "profile"), "review", deps);
|
|
163
|
+
case "glm_delegate":
|
|
164
|
+
return await delegate(requiredString(args, "name"), requiredString(args, "prompt"), deps);
|
|
165
|
+
case "glm_usage":
|
|
166
|
+
return await usage(deps);
|
|
167
|
+
default:
|
|
168
|
+
return { text: `Unknown tool "${name}". Available: ${MCP_TOOLS.map((tool) => tool.name).join(", ")}.`, isError: true };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return errorText(error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* One MCP server instance: `handleLine` takes a stdin line and resolves the
|
|
177
|
+
* JSON-RPC response frame to write back, or null (nothing to send —
|
|
178
|
+
* notifications, blank or malformed lines). Pure with respect to stdio.
|
|
179
|
+
*/
|
|
180
|
+
export function createMcpServer(deps = {}) {
|
|
181
|
+
return {
|
|
182
|
+
async handleLine(line) {
|
|
183
|
+
const trimmed = line.trim();
|
|
184
|
+
if (trimmed.length === 0)
|
|
185
|
+
return null;
|
|
186
|
+
let message;
|
|
187
|
+
try {
|
|
188
|
+
message = JSON.parse(trimmed);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (message.jsonrpc !== "2.0" || typeof message.method !== "string")
|
|
194
|
+
return null;
|
|
195
|
+
const id = message.id;
|
|
196
|
+
if (id === undefined || id === null)
|
|
197
|
+
return null; // notification
|
|
198
|
+
const params = (typeof message.params === "object" && message.params !== null ? message.params : {});
|
|
199
|
+
let body;
|
|
200
|
+
try {
|
|
201
|
+
switch (message.method) {
|
|
202
|
+
case "initialize":
|
|
203
|
+
body = {
|
|
204
|
+
result: {
|
|
205
|
+
protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : "2024-11-05",
|
|
206
|
+
capabilities: { tools: {} },
|
|
207
|
+
serverInfo: { name: "glm-coding-router", version },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
break;
|
|
211
|
+
case "ping":
|
|
212
|
+
body = { result: {} };
|
|
213
|
+
break;
|
|
214
|
+
case "tools/list":
|
|
215
|
+
body = { result: { tools: MCP_TOOLS } };
|
|
216
|
+
break;
|
|
217
|
+
case "tools/call": {
|
|
218
|
+
const result = await callMcpTool(String(params.name ?? ""), (params.arguments ?? {}), deps);
|
|
219
|
+
body = { result: { content: [{ type: "text", text: result.text }], isError: result.isError } };
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
default:
|
|
223
|
+
body = { error: { code: -32601, message: `Method not found: ${message.method}` } };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
body = { error: { code: -32603, message: `Internal error: ${error instanceof Error ? error.message : String(error)}` } };
|
|
228
|
+
}
|
|
229
|
+
return JSON.stringify({ jsonrpc: "2.0", id, ...body });
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glm-coding-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "GLM Coding Plan workers for Claude Code and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"glm-chat": "./dist/bin/glm-chat.js",
|
|
15
15
|
"glm-worker": "./dist/bin/glm-worker.js",
|
|
16
16
|
"glm-review": "./dist/bin/glm-review.js",
|
|
17
|
-
"glm-fast": "./dist/bin/glm-fast.js"
|
|
17
|
+
"glm-fast": "./dist/bin/glm-fast.js",
|
|
18
|
+
"glm-mcp": "./dist/bin/glm-mcp.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"dist"
|