glm-coding-router 1.1.0 → 1.1.2

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 CHANGED
@@ -135,7 +135,14 @@ dependency inspection, and preliminary review:
135
135
  glm-review "Inspect this repository"
136
136
  ```
137
137
 
138
- Runs with `--tools Read,Glob,Grep` — it cannot edit files or run commands.
138
+ Runs with `--tools Read,Glob,Grep --strict-mcp-config` — it cannot edit files or run
139
+ commands.
140
+
141
+ The second flag is part of the guarantee, not a detail: `--tools` restricts only Claude
142
+ Code's **built-in** tools, so without it a review session would also inherit whatever MCP
143
+ servers you have registered — including this project's own, whose `glm_worker` tool writes
144
+ files. `glm-worker` and `glm-router benchmark` are isolated the same way. Interactive
145
+ sessions (`glm-chat`, `glm-fast`) are not: your servers are yours.
139
146
 
140
147
  ## Profiles
141
148
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { STRICT_MCP_ARGS } from "../core/agent-args.js";
2
3
  import { loadConfig } from "../core/config.js";
3
4
  import { locateClaude } from "../core/claude.js";
4
5
  import { createGlmEnv } from "../core/env.js";
@@ -11,8 +12,24 @@ import { spawnAgent } from "../core/process.js";
11
12
  import { resolveZaiApiKey } from "../core/zai-key.js";
12
13
  /** Read-only review surface (spec §17) — no Edit, Write, or Bash. */
13
14
  export const REVIEW_TOOLS = "Read,Glob,Grep";
15
+ /**
16
+ * Build the child arguments (spec §17, specs/review-mcp-isolation.md).
17
+ *
18
+ * `--tools` alone does not make this read-only: it restricts the built-in set,
19
+ * while MCP tools from the user's config are additive — with our own server
20
+ * registered, a review could call `glm_worker` and write files. STRICT_MCP_ARGS
21
+ * is what actually holds the guarantee.
22
+ */
14
23
  export function buildReviewArgs(prompt, config) {
15
- return ["-p", prompt, "--max-turns", String(config.review.maxTurns), "--tools", REVIEW_TOOLS];
24
+ return [
25
+ "-p",
26
+ prompt,
27
+ "--max-turns",
28
+ String(config.review.maxTurns),
29
+ "--tools",
30
+ REVIEW_TOOLS,
31
+ ...STRICT_MCP_ARGS,
32
+ ];
16
33
  }
17
34
  /**
18
35
  * glm-review (spec §17): read-only worker for exploration, call-graph
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { STRICT_MCP_ARGS } from "../core/agent-args.js";
2
3
  import { loadConfig } from "../core/config.js";
3
4
  import { locateClaude } from "../core/claude.js";
4
5
  import { createGlmEnv } from "../core/env.js";
@@ -21,6 +22,10 @@ export const WORKER_TOOLS_NO_BASH = "Read,Glob,Grep,Edit,Write";
21
22
  * `--allowedTools` every Bash call comes back "This command requires
22
23
  * approval". When the allowlist is empty we drop Bash from `--tools` entirely
23
24
  * rather than advertising a tool the worker can never use.
25
+ *
26
+ * STRICT_MCP_ARGS keeps the user's MCP servers out of the child
27
+ * (specs/review-mcp-isolation.md): write is expected here, undeclared
28
+ * recursion into `glm_delegate` and its own turn budget is not.
24
29
  */
25
30
  export function buildWorkerArgs(prompt, config) {
26
31
  const allowedBash = config.worker.allowedBash;
@@ -33,6 +38,8 @@ export function buildWorkerArgs(prompt, config) {
33
38
  "acceptEdits",
34
39
  "--tools",
35
40
  allowedBash.length > 0 ? WORKER_TOOLS : WORKER_TOOLS_NO_BASH,
41
+ // Before --allowedTools: its values are variadic and must stay last.
42
+ ...STRICT_MCP_ARGS,
36
43
  ];
37
44
  if (allowedBash.length > 0) {
38
45
  args.push("--allowedTools", ...allowedBash.map((pattern) => `Bash(${pattern})`));
@@ -10,6 +10,7 @@ import { locateClaude } from "../core/claude.js";
10
10
  import { createGlmEnv } from "../core/env.js";
11
11
  import { Errors } from "../core/errors.js";
12
12
  import { configDir } from "../core/paths.js";
13
+ import { STRICT_MCP_ARGS } from "../core/agent-args.js";
13
14
  import { spawnAgentCapture } from "../core/process.js";
14
15
  import { resolveZaiApiKey } from "../core/zai-key.js";
15
16
  import { BENCHMARK_TASKS, benchmarkTaskById } from "../templates/benchmark-tasks.js";
@@ -126,6 +127,9 @@ async function runTask(task, run, ctx) {
126
127
  "acceptEdits",
127
128
  "--tools",
128
129
  WORKER_TOOLS,
130
+ // A benchmark is only comparable if the tool surface is fixed
131
+ // (specs/review-mcp-isolation.md).
132
+ ...STRICT_MCP_ARGS,
129
133
  "--output-format",
130
134
  "json",
131
135
  ];
@@ -51,6 +51,8 @@ export function statusCommand(options, deps = {}) {
51
51
  });
52
52
  return 0;
53
53
  }
54
+ /** Every status row pads its label to this column (spec §41). */
55
+ const LABEL_WIDTH = 16;
54
56
  const lines = [
55
57
  `GLM Coding Router v${version}`,
56
58
  "",
@@ -63,7 +65,7 @@ export function statusCommand(options, deps = {}) {
63
65
  ];
64
66
  for (const row of skillState) {
65
67
  const enabled = row.homeDetected && row.installed;
66
- lines.push(`${row.agent} skill ${enabled ? "enabled" : "disabled"}`);
68
+ lines.push(`${`${row.agent} skill`.padEnd(LABEL_WIDTH)}${enabled ? "enabled" : "disabled"}`);
67
69
  }
68
70
  lines.push("", `Main model ${config.models.main}`, `Fast model ${config.models.fast}`);
69
71
  process.stdout.write(lines.join("\n") + "\n");
@@ -9,7 +9,7 @@ import { emitJson } from "./context.js";
9
9
  /** Z.ai monitor API used by their own dashboard (specs/usage.md). */
10
10
  const ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
11
11
  /** Window labels for the observed enum values (specs/usage.md); unknown values stay generic. */
12
- function describeWindow(limit) {
12
+ export function describeWindow(limit) {
13
13
  if (limit.unit === 3 && typeof limit.number === "number") {
14
14
  return `${limit.number}-hour window`;
15
15
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Arguments shared by every headless agent we spawn
3
+ * (specs/review-mcp-isolation.md).
4
+ */
5
+ /**
6
+ * Keep the user's MCP servers out of the children we construct.
7
+ *
8
+ * `--tools` only restricts Claude Code's **built-in** set, so MCP tools
9
+ * registered at user/project scope are additive and survive it. With our own
10
+ * server registered (`glm-router mcp install`), that handed every `glm-review`
11
+ * session an `mcp__glm-coding-router__glm_worker` — write access and recursion
12
+ * from a surface documented as read-only.
13
+ *
14
+ * `--strict-mcp-config` uses only the servers given by `--mcp-config`. We pass
15
+ * none, so the set is empty and the child's tool surface is exactly the one we
16
+ * asked for. Security setting: not configurable, not overridable by a profile.
17
+ * Interactive sessions (`glm-chat`, `glm-fast`) are deliberately excluded —
18
+ * the user's own servers are theirs.
19
+ */
20
+ export const STRICT_MCP_ARGS = ["--strict-mcp-config"];
@@ -168,14 +168,16 @@ export function describeShellExport(name, deps = {}) {
168
168
  const env = deps.env ?? process.env;
169
169
  const home = deps.home ?? env.HOME ?? "~";
170
170
  const exists = deps.exists ?? ((file) => fs.existsSync(file));
171
- const shell = path.basename(env.SHELL ?? "bash");
171
+ const shell = path.posix.basename(env.SHELL ?? "bash");
172
172
  const candidates = shell === "zsh"
173
173
  ? [".zshrc", ".zprofile", ".profile"]
174
174
  : shell === "fish"
175
175
  ? [".config/fish/config.fish"]
176
176
  : [".bashrc", ".bash_profile", ".profile"];
177
- const found = candidates.find((rel) => exists(path.join(home, rel)));
178
- const profile = path.join(home, found ?? candidates[0]);
177
+ // POSIX paths by definition: this guidance only ever names a shell rc file,
178
+ // so it must not pick up Windows separators when the process runs on win32.
179
+ const found = candidates.find((rel) => exists(path.posix.join(home, rel)));
180
+ const profile = path.posix.join(home, found ?? candidates[0]);
179
181
  const line = shell === "fish" ? `set -gx ${name} <your-key>` : `export ${name}="<your-key>"`;
180
182
  return { line, profile };
181
183
  }
@@ -11,7 +11,7 @@ import { applyProfile } from "../core/profile.js";
11
11
  import { version } from "../core/version.js";
12
12
  import { createDelegateWorktree, delegateBranch, removeDelegateWorktree, rollbackDelegateBranch, validateDelegateName, } from "../core/worktree.js";
13
13
  import { resolveZaiApiKey } from "../core/zai-key.js";
14
- import { aggregateLocalUsage, fetchZaiQuota } from "../commands/usage.js";
14
+ import { aggregateLocalUsage, describeWindow, fetchZaiQuota } from "../commands/usage.js";
15
15
  const PROMPT_PROPERTY = { type: "string", description: "The task prompt for the GLM agent." };
16
16
  export const MCP_TOOLS = [
17
17
  {
@@ -138,7 +138,7 @@ async function usage(deps) {
138
138
  lines.push(`Z.ai Coding Plan${quota.level ? ` (level: ${quota.level})` : ""}`);
139
139
  for (const limit of quota.limits ?? []) {
140
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}`);
141
+ lines.push(` ${describeWindow(limit).padEnd(15)} ${String(limit.currentValue ?? "?")} / ${String(limit.usage ?? "?")} credits (${String(limit.percentage ?? "?")}%)${resets}`);
142
142
  }
143
143
  }
144
144
  catch (error) {
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "glm-coding-router",
3
- "version": "1.1.0",
4
- "description": "GLM Coding Plan workers for Claude Code and Codex",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "hieu9721",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/hieu9721/GLM-coding-router.git"
11
- },
12
- "bin": {
13
- "glm-router": "./dist/cli.js",
14
- "glm-chat": "./dist/bin/glm-chat.js",
15
- "glm-worker": "./dist/bin/glm-worker.js",
16
- "glm-review": "./dist/bin/glm-review.js",
17
- "glm-fast": "./dist/bin/glm-fast.js",
18
- "glm-mcp": "./dist/bin/glm-mcp.js"
19
- },
20
- "files": [
21
- "dist"
22
- ],
23
- "scripts": {
24
- "dev": "tsx src/cli.ts",
25
- "build": "tsc",
26
- "test": "vitest run",
27
- "test:watch": "vitest",
28
- "lint": "eslint src tests",
29
- "prepublishOnly": "npm run build && npm test"
30
- },
31
- "engines": {
32
- "node": ">=20"
33
- },
34
- "dependencies": {
35
- "commander": "^15.0.0",
36
- "prompts": "^2.4.2",
37
- "zod": "^4.6.5"
38
- },
39
- "devDependencies": {
40
- "@types/node": "^22.20.3",
41
- "@types/prompts": "^2.4.9",
42
- "eslint": "^9.39.5",
43
- "tsx": "^4.23.13",
44
- "typescript": "^5.9.3",
45
- "typescript-eslint": "^8.70.0",
46
- "vitest": "^5.0.1"
47
- }
48
- }
1
+ {
2
+ "name": "glm-coding-router",
3
+ "version": "1.1.2",
4
+ "description": "GLM Coding Plan workers for Claude Code and Codex",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "hieu9721",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/hieu9721/GLM-coding-router.git"
11
+ },
12
+ "bin": {
13
+ "glm-router": "./dist/cli.js",
14
+ "glm-chat": "./dist/bin/glm-chat.js",
15
+ "glm-worker": "./dist/bin/glm-worker.js",
16
+ "glm-review": "./dist/bin/glm-review.js",
17
+ "glm-fast": "./dist/bin/glm-fast.js",
18
+ "glm-mcp": "./dist/bin/glm-mcp.js"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "dev": "tsx src/cli.ts",
25
+ "build": "tsc",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "lint": "eslint src tests",
29
+ "prepublishOnly": "npm run build && npm test"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "dependencies": {
35
+ "commander": "^15.0.0",
36
+ "prompts": "^2.4.2",
37
+ "zod": "^4.6.5"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.20.3",
41
+ "@types/prompts": "^2.4.9",
42
+ "eslint": "^9.39.5",
43
+ "tsx": "^4.23.13",
44
+ "typescript": "^5.9.3",
45
+ "typescript-eslint": "^8.70.0",
46
+ "vitest": "^5.0.1"
47
+ }
48
+ }