glm-coding-router 0.5.0 → 1.1.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 +419 -381
- package/dist/bin/glm-mcp.js +25 -0
- package/dist/bin/glm-worker.js +37 -4
- package/dist/cli.js +13 -0
- package/dist/commands/doctor.js +31 -14
- package/dist/commands/init.js +5 -3
- package/dist/commands/key.js +46 -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/uninstall.js +2 -1
- package/dist/commands/usage.js +1 -1
- package/dist/core/config.js +31 -2
- package/dist/core/errors.js +33 -10
- package/dist/core/platform.js +37 -8
- package/dist/core/profile.js +6 -1
- package/dist/core/user-env.js +181 -0
- package/dist/core/zai-key.js +15 -59
- package/dist/integrations/skill.js +31 -12
- package/dist/mcp/server.js +232 -0
- package/package.json +3 -2
|
@@ -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": "
|
|
3
|
+
"version": "1.1.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"
|