glm-coding-router 0.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/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/bin/glm-chat.js +40 -0
- package/dist/bin/glm-review.js +47 -0
- package/dist/bin/glm-worker.js +57 -0
- package/dist/cli.js +108 -0
- package/dist/commands/config.js +39 -0
- package/dist/commands/context.js +20 -0
- package/dist/commands/doctor-command.js +64 -0
- package/dist/commands/doctor.js +93 -0
- package/dist/commands/init.js +123 -0
- package/dist/commands/key.js +56 -0
- package/dist/commands/project-init.js +61 -0
- package/dist/commands/project-remove.js +36 -0
- package/dist/commands/skill.js +43 -0
- package/dist/commands/status.js +57 -0
- package/dist/commands/uninstall.js +81 -0
- package/dist/core/claude.js +104 -0
- package/dist/core/config.js +150 -0
- package/dist/core/env.js +22 -0
- package/dist/core/errors.js +121 -0
- package/dist/core/logging.js +54 -0
- package/dist/core/main-guard.js +20 -0
- package/dist/core/paths.js +13 -0
- package/dist/core/platform.js +20 -0
- package/dist/core/process.js +60 -0
- package/dist/core/prompt.js +32 -0
- package/dist/core/version.js +3 -0
- package/dist/core/zai-key.js +84 -0
- package/dist/integrations/claude.js +18 -0
- package/dist/integrations/codex.js +18 -0
- package/dist/integrations/index.js +3 -0
- package/dist/integrations/skill.js +35 -0
- package/dist/project/atomic-write.js +21 -0
- package/dist/project/managed-block.js +100 -0
- package/dist/project/managed-file.js +67 -0
- package/dist/project/ownership.js +42 -0
- package/dist/project/project-root.js +26 -0
- package/dist/templates/agents-block.js +46 -0
- package/dist/templates/claude-block.js +49 -0
- package/dist/templates/glm-delegation-skill.js +68 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hieu9721
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# GLM Coding Router
|
|
2
|
+
|
|
3
|
+
GLM Coding Plan workers for Claude Code and Codex, on Windows.
|
|
4
|
+
|
|
5
|
+
Claude Code and Codex stay your orchestrators — they keep responsibility for requirements,
|
|
6
|
+
architecture, review, and integration. `glm-coding-router` delegates well-scoped
|
|
7
|
+
implementation work (exploration, CRUD, boilerplate, tests, mechanical refactoring) to
|
|
8
|
+
GLM workers via Z.ai's Anthropic-compatible endpoint.
|
|
9
|
+
|
|
10
|
+
One global npm install replaces the manual `.cmd` shim setup:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
Claude / Codex → shell → glm-worker → claude.exe harness → Z.ai endpoint → GLM Coding Plan
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
Developer
|
|
20
|
+
│
|
|
21
|
+
┌───────────────┴───────────────┐
|
|
22
|
+
▼ ▼
|
|
23
|
+
Claude Code Codex
|
|
24
|
+
│ │
|
|
25
|
+
└───────────────┬───────────────┘
|
|
26
|
+
shell command
|
|
27
|
+
│
|
|
28
|
+
┌───────────────┼───────────────┐
|
|
29
|
+
▼ ▼ ▼
|
|
30
|
+
glm-chat glm-worker glm-review
|
|
31
|
+
│ │ │
|
|
32
|
+
└───────────────┼───────────────┘
|
|
33
|
+
claude.exe
|
|
34
|
+
(injected environment only)
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
https://api.z.ai/api/anthropic
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
GLM Coding Plan
|
|
41
|
+
GLM-5.3 / GLM-5.3-Flash
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Windows 10/11 (v0.1; Linux/macOS planned for v0.2)
|
|
47
|
+
- Node.js >= 20
|
|
48
|
+
- Claude Code (`claude.exe`) — the GLM commands run on the Claude Code harness
|
|
49
|
+
- Codex (optional — Claude-only setups are fully supported)
|
|
50
|
+
- A Z.ai Coding Plan API key
|
|
51
|
+
|
|
52
|
+
No Anthropic pay-as-you-go, no OpenAI API, no LiteLLM, no proxy.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
npm install -g glm-coding-router
|
|
58
|
+
glm-router init
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`npx glm-coding-router init` also works for a one-off check, but the global install is
|
|
62
|
+
what puts `glm-worker` on your PATH long-term.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
After `glm-router init`:
|
|
67
|
+
|
|
68
|
+
```powershell
|
|
69
|
+
glm-chat
|
|
70
|
+
glm-worker "Implement validation and add tests"
|
|
71
|
+
glm-review "Analyze the auth module"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## glm-chat
|
|
75
|
+
|
|
76
|
+
Interactive GLM-backed Claude Code session. Resolves the Z.ai key, locates `claude.exe`,
|
|
77
|
+
injects the Z.ai environment **into the child process only**, and spawns it with
|
|
78
|
+
pass-through arguments:
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
glm-chat
|
|
82
|
+
glm-chat --version
|
|
83
|
+
glm-chat --any-claude-flag
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Your normal `claude` command and its authentication are untouched.
|
|
87
|
+
|
|
88
|
+
## glm-worker
|
|
89
|
+
|
|
90
|
+
Headless implementation worker:
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
glm-worker "Implement validation and add tests"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or via stdin (a structured task packet):
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
@"
|
|
100
|
+
TASK:
|
|
101
|
+
Implement refresh token validation.
|
|
102
|
+
|
|
103
|
+
SCOPE:
|
|
104
|
+
internal/auth/
|
|
105
|
+
|
|
106
|
+
VALIDATION:
|
|
107
|
+
go test ./internal/auth/...
|
|
108
|
+
"@ | glm-worker
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Input priority: **stdin → arguments → error**. The worker runs with
|
|
112
|
+
`--max-turns 20 --permission-mode acceptEdits --tools Read,Glob,Grep,Edit,Write,Bash`.
|
|
113
|
+
It never uses `--dangerously-skip-permissions`.
|
|
114
|
+
|
|
115
|
+
## glm-review
|
|
116
|
+
|
|
117
|
+
Read-only worker for repository exploration, call-graph discovery, duplicate detection,
|
|
118
|
+
dependency inspection, and preliminary review:
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
glm-review "Inspect this repository"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Runs with `--tools Read,Glob,Grep` — it cannot edit files or run commands.
|
|
125
|
+
|
|
126
|
+
## CLI reference
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
glm-router init guided setup
|
|
130
|
+
glm-router doctor [--network] full runtime diagnosis
|
|
131
|
+
glm-router status quick offline overview
|
|
132
|
+
glm-router key set store ZAI_API_KEY (Windows User Environment)
|
|
133
|
+
glm-router key check key configured? from which source?
|
|
134
|
+
glm-router config show
|
|
135
|
+
glm-router config set models.main glm-5.3
|
|
136
|
+
glm-router project init CLAUDE.md / AGENTS.md managed blocks (--dry-run supported)
|
|
137
|
+
glm-router project remove
|
|
138
|
+
glm-router skill install optional Codex delegation skill
|
|
139
|
+
glm-router skill remove
|
|
140
|
+
glm-router uninstall guided removal (keeps ZAI_API_KEY by default)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Global flags: `--json --quiet --verbose --dry-run --force --yes`
|
|
144
|
+
|
|
145
|
+
## Claude integration
|
|
146
|
+
|
|
147
|
+
`glm-router project init` adds a **managed block** to `CLAUDE.md` at the project root
|
|
148
|
+
(`git rev-parse --show-toplevel`, falling back to cwd):
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
<!-- glm-coding-router:start -->
|
|
152
|
+
... delegation policy ...
|
|
153
|
+
<!-- glm-coding-router:end -->
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- Everything outside the markers is preserved; existing blocks are replaced in place;
|
|
157
|
+
runs are idempotent and never duplicate.
|
|
158
|
+
- Files are updated atomically (tmp file → fsync → rename).
|
|
159
|
+
- On a malformed marker pair the file is left untouched with an actionable error.
|
|
160
|
+
- CRLF/LF and UTF-8 are preserved.
|
|
161
|
+
- `glm-router project remove` deletes only the managed block. A file the router
|
|
162
|
+
created entirely is deleted only when it would otherwise be empty.
|
|
163
|
+
|
|
164
|
+
## Codex integration
|
|
165
|
+
|
|
166
|
+
The same command updates `AGENTS.md` (Codex's repository instruction file) with an
|
|
167
|
+
equivalent managed block. Additionally, `glm-router skill install` installs the optional
|
|
168
|
+
`glm-delegation` skill to `~/.codex/skills/glm-delegation/SKILL.md`. If Codex is not
|
|
169
|
+
detected, the skill step warns and skips — AGENTS.md integration and the core tool are
|
|
170
|
+
unaffected.
|
|
171
|
+
|
|
172
|
+
## Orca behavior (stale environments)
|
|
173
|
+
|
|
174
|
+
Terminals embedded in Orca snapshot the Windows environment at startup. A key added
|
|
175
|
+
after Orca starts is invisible to those terminals. Every GLM command therefore resolves
|
|
176
|
+
the key in this order:
|
|
177
|
+
|
|
178
|
+
1. `process.env.ZAI_API_KEY`
|
|
179
|
+
2. Windows User Environment (via PowerShell)
|
|
180
|
+
3. fail with an actionable error
|
|
181
|
+
|
|
182
|
+
The key is never cached to disk.
|
|
183
|
+
|
|
184
|
+
## Security model
|
|
185
|
+
|
|
186
|
+
- The key lives only in the Windows User Environment; it is never written to
|
|
187
|
+
`config.json`, the repo, logs, or stack traces. Debug output redacts
|
|
188
|
+
`ZAI_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, and Authorization headers.
|
|
189
|
+
- Z.ai routing environment variables (`ANTHROPIC_AUTH_TOKEN`,
|
|
190
|
+
`ANTHROPIC_BASE_URL`, model overrides) are injected **only** into the spawned
|
|
191
|
+
`claude.exe` child process. `ANTHROPIC_API_KEY` is blanked in the child so your
|
|
192
|
+
normal Claude auth is never in play. `ANTHROPIC_BASE_URL` is never persisted globally.
|
|
193
|
+
- Claude Code and Codex global authentication are never modified.
|
|
194
|
+
- Child processes are spawned with argument arrays (`shell: false`) — prompts with
|
|
195
|
+
quotes, pipes, ampersands, or newlines are passed verbatim, never through a shell.
|
|
196
|
+
- No telemetry, no automatic git commits.
|
|
197
|
+
|
|
198
|
+
## Troubleshooting
|
|
199
|
+
|
|
200
|
+
| Symptom | Fix |
|
|
201
|
+
| --- | --- |
|
|
202
|
+
| `ERROR [ZAI_KEY_MISSING]` | `glm-router key set`, then open a **new** terminal |
|
|
203
|
+
| `ERROR [CLAUDE_NOT_FOUND]` | Install Claude Code, or `glm-router config set claudePath C:\path\to\claude.exe` |
|
|
204
|
+
| Key works in a new terminal but not inside Orca | Expected — workers re-read the Windows User Environment automatically; run `glm-router doctor` to confirm |
|
|
205
|
+
| `glm-*` not on PATH after install | Reopen the terminal; check `npm config get prefix` is on PATH |
|
|
206
|
+
| `ERROR [MANAGED_BLOCK_CORRUPT]` | Fix the marker pair in the named file manually, then re-run |
|
|
207
|
+
|
|
208
|
+
Run `glm-router doctor` (add `--network` to probe the Z.ai endpoint) for a full diagnosis.
|
|
209
|
+
|
|
210
|
+
## Uninstall
|
|
211
|
+
|
|
212
|
+
```powershell
|
|
213
|
+
glm-router uninstall
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The wizard removes the config, the Codex skill, and optionally the current project
|
|
217
|
+
integration. `ZAI_API_KEY` is **kept** by default — removing credentials requires
|
|
218
|
+
explicit consent. Finish with `npm uninstall -g glm-coding-router`.
|
|
219
|
+
|
|
220
|
+
## Development
|
|
221
|
+
|
|
222
|
+
```powershell
|
|
223
|
+
npm install
|
|
224
|
+
npm run build # tsc → dist/
|
|
225
|
+
npm test # vitest run
|
|
226
|
+
npm run lint # eslint src tests
|
|
227
|
+
npm run dev # tsx src/cli.ts <args>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Integration tests spawn `tests/fixtures/fake-agent.mjs` (via `node.exe`) to verify
|
|
231
|
+
argument passing, environment injection, and exit-code propagation without spending
|
|
232
|
+
API quota. See the `GLM Coding Router — Technical Specification v0.1.md` for the full
|
|
233
|
+
v0.1 contract (exit codes, managed-block test matrix, acceptance criteria).
|
|
234
|
+
|
|
235
|
+
## Publishing
|
|
236
|
+
|
|
237
|
+
```powershell
|
|
238
|
+
npm run build
|
|
239
|
+
npm test
|
|
240
|
+
npm publish
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`prepublishOnly` runs build + tests. The package ships only `dist/`; the four binaries
|
|
244
|
+
(`glm-router`, `glm-chat`, `glm-worker`, `glm-review`) are declared in `bin`.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from "../core/config.js";
|
|
3
|
+
import { locateClaude } from "../core/claude.js";
|
|
4
|
+
import { createGlmEnv } from "../core/env.js";
|
|
5
|
+
import { Errors, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
6
|
+
import { isMainModule } from "../core/main-guard.js";
|
|
7
|
+
import { logger, redact } from "../core/logging.js";
|
|
8
|
+
import { spawnAgent } from "../core/process.js";
|
|
9
|
+
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
10
|
+
/**
|
|
11
|
+
* glm-chat (spec §14): resolve key → detect claude.exe → inject Z.ai env →
|
|
12
|
+
* spawn claude interactively with pass-through arguments.
|
|
13
|
+
*/
|
|
14
|
+
export async function runChat(argv) {
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
const resolved = resolveZaiApiKey();
|
|
17
|
+
if (!resolved) {
|
|
18
|
+
throw Errors.zaiKeyMissing();
|
|
19
|
+
}
|
|
20
|
+
const claudePath = locateClaude(config);
|
|
21
|
+
const env = createGlmEnv(config, resolved.key);
|
|
22
|
+
logger.debug(`spawning ${claudePath}`);
|
|
23
|
+
logger.debug(redact(`env ANTHROPIC_BASE_URL=${env.ANTHROPIC_BASE_URL}`, [resolved.key]));
|
|
24
|
+
return spawnAgent(claudePath, {
|
|
25
|
+
args: [...argv],
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
env,
|
|
28
|
+
interactive: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (isMainModule(import.meta.url)) {
|
|
32
|
+
runChat(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
33
|
+
if (error instanceof GlmRouterError) {
|
|
34
|
+
process.stderr.write(formatGlmError(error) + "\n");
|
|
35
|
+
process.exit(error.exitCode);
|
|
36
|
+
}
|
|
37
|
+
process.stderr.write(String(error) + "\n");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from "../core/config.js";
|
|
3
|
+
import { locateClaude } from "../core/claude.js";
|
|
4
|
+
import { createGlmEnv } from "../core/env.js";
|
|
5
|
+
import { Errors, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
6
|
+
import { isMainModule } from "../core/main-guard.js";
|
|
7
|
+
import { logger, redact } from "../core/logging.js";
|
|
8
|
+
import { readStdin, resolvePrompt } from "../core/prompt.js";
|
|
9
|
+
import { spawnAgent } from "../core/process.js";
|
|
10
|
+
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
11
|
+
/** Read-only review surface (spec §17) — no Edit, Write, or Bash. */
|
|
12
|
+
export const REVIEW_TOOLS = "Read,Glob,Grep";
|
|
13
|
+
export function buildReviewArgs(prompt, config) {
|
|
14
|
+
return ["-p", prompt, "--max-turns", String(config.review.maxTurns), "--tools", REVIEW_TOOLS];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* glm-review (spec §17): read-only worker for exploration, call-graph
|
|
18
|
+
* discovery, duplicate detection, dependency inspection, and review.
|
|
19
|
+
*/
|
|
20
|
+
export async function runReview(argv) {
|
|
21
|
+
const prompt = await resolvePrompt(argv, readStdin, "glm-review");
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
const resolved = resolveZaiApiKey();
|
|
24
|
+
if (!resolved) {
|
|
25
|
+
throw Errors.zaiKeyMissing();
|
|
26
|
+
}
|
|
27
|
+
const claudePath = locateClaude(config);
|
|
28
|
+
const args = buildReviewArgs(prompt, config);
|
|
29
|
+
const env = createGlmEnv(config, resolved.key);
|
|
30
|
+
logger.debug(redact(`spawning ${claudePath} ${args.join(" ")}`, [resolved.key]));
|
|
31
|
+
return spawnAgent(claudePath, {
|
|
32
|
+
args,
|
|
33
|
+
cwd: process.cwd(),
|
|
34
|
+
env,
|
|
35
|
+
interactive: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (isMainModule(import.meta.url)) {
|
|
39
|
+
runReview(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
40
|
+
if (error instanceof GlmRouterError) {
|
|
41
|
+
process.stderr.write(formatGlmError(error) + "\n");
|
|
42
|
+
process.exit(error.exitCode);
|
|
43
|
+
}
|
|
44
|
+
process.stderr.write(String(error) + "\n");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from "../core/config.js";
|
|
3
|
+
import { locateClaude } from "../core/claude.js";
|
|
4
|
+
import { createGlmEnv } from "../core/env.js";
|
|
5
|
+
import { Errors, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
6
|
+
import { isMainModule } from "../core/main-guard.js";
|
|
7
|
+
import { logger, redact } from "../core/logging.js";
|
|
8
|
+
import { resolvePrompt } from "../core/prompt.js";
|
|
9
|
+
import { spawnAgent } from "../core/process.js";
|
|
10
|
+
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
11
|
+
/** Worker tool surface (spec §16). */
|
|
12
|
+
export const WORKER_TOOLS = "Read,Glob,Grep,Edit,Write,Bash";
|
|
13
|
+
export function buildWorkerArgs(prompt, config) {
|
|
14
|
+
return [
|
|
15
|
+
"-p",
|
|
16
|
+
prompt,
|
|
17
|
+
"--max-turns",
|
|
18
|
+
String(config.worker.maxTurns),
|
|
19
|
+
"--permission-mode",
|
|
20
|
+
"acceptEdits",
|
|
21
|
+
"--tools",
|
|
22
|
+
WORKER_TOOLS,
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* glm-worker (spec §15, §16): headless implementation worker.
|
|
27
|
+
* Prompt priority: stdin → arguments → error. Never uses
|
|
28
|
+
* --dangerously-skip-permissions.
|
|
29
|
+
*/
|
|
30
|
+
export async function runWorker(argv) {
|
|
31
|
+
const prompt = await resolvePrompt(argv);
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
const resolved = resolveZaiApiKey();
|
|
34
|
+
if (!resolved) {
|
|
35
|
+
throw Errors.zaiKeyMissing();
|
|
36
|
+
}
|
|
37
|
+
const claudePath = locateClaude(config);
|
|
38
|
+
const args = buildWorkerArgs(prompt, config);
|
|
39
|
+
const env = createGlmEnv(config, resolved.key);
|
|
40
|
+
logger.debug(redact(`spawning ${claudePath} ${args.join(" ")}`, [resolved.key]));
|
|
41
|
+
return spawnAgent(claudePath, {
|
|
42
|
+
args,
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
env,
|
|
45
|
+
interactive: false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (isMainModule(import.meta.url)) {
|
|
49
|
+
runWorker(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
50
|
+
if (error instanceof GlmRouterError) {
|
|
51
|
+
process.stderr.write(formatGlmError(error) + "\n");
|
|
52
|
+
process.exit(error.exitCode);
|
|
53
|
+
}
|
|
54
|
+
process.stderr.write(String(error) + "\n");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { version } from "./core/version.js";
|
|
4
|
+
import { ExitCode } from "./core/errors.js";
|
|
5
|
+
import { isMainModule } from "./core/main-guard.js";
|
|
6
|
+
import { applyGlobalOptions, reportError } from "./commands/context.js";
|
|
7
|
+
import { initCommand } from "./commands/init.js";
|
|
8
|
+
import { doctorCommand } from "./commands/doctor-command.js";
|
|
9
|
+
import { statusCommand } from "./commands/status.js";
|
|
10
|
+
import { keyCheckCommand, keySetCommand } from "./commands/key.js";
|
|
11
|
+
import { configSetCommand, configShowCommand } from "./commands/config.js";
|
|
12
|
+
import { projectInitCommand } from "./commands/project-init.js";
|
|
13
|
+
import { projectRemoveCommand } from "./commands/project-remove.js";
|
|
14
|
+
import { skillInstallCommand, skillRemoveCommand } from "./commands/skill.js";
|
|
15
|
+
import { uninstallCommand } from "./commands/uninstall.js";
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program
|
|
18
|
+
.name("glm-router")
|
|
19
|
+
.description("GLM Coding Plan workers for Claude Code and Codex")
|
|
20
|
+
.version(version);
|
|
21
|
+
program
|
|
22
|
+
.option("--json", "machine-readable JSON output")
|
|
23
|
+
.option("--quiet", "suppress non-error output")
|
|
24
|
+
.option("--verbose", "debug-level output")
|
|
25
|
+
.option("--dry-run", "preview file modifications without writing")
|
|
26
|
+
.option("--force", "apply actions even when already done")
|
|
27
|
+
.option("--yes", "assume defaults for all prompts");
|
|
28
|
+
/** Shared action wrapper: apply global flags, catch errors, set the exit code. */
|
|
29
|
+
async function execute(action) {
|
|
30
|
+
applyGlobalOptions(globalOptions());
|
|
31
|
+
try {
|
|
32
|
+
process.exitCode = await action();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
process.exitCode = reportError(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function globalOptions() {
|
|
39
|
+
return program.opts();
|
|
40
|
+
}
|
|
41
|
+
program
|
|
42
|
+
.command("init")
|
|
43
|
+
.description("guided setup: environment check, key, config, integrations")
|
|
44
|
+
.action(() => execute(() => initCommand(globalOptions())));
|
|
45
|
+
program
|
|
46
|
+
.command("doctor")
|
|
47
|
+
.description("diagnose the full runtime")
|
|
48
|
+
.option("--network", "also probe Z.ai endpoint reachability")
|
|
49
|
+
.action((commandOptions) => execute(() => doctorCommand({ ...globalOptions(), ...commandOptions })));
|
|
50
|
+
program
|
|
51
|
+
.command("status")
|
|
52
|
+
.description("quick offline status overview")
|
|
53
|
+
.action(() => execute(() => Promise.resolve(statusCommand(globalOptions()))));
|
|
54
|
+
const key = program.command("key").description("manage the Z.ai Coding Plan API key");
|
|
55
|
+
key
|
|
56
|
+
.command("set")
|
|
57
|
+
.description("prompt for and store the key in the Windows User Environment")
|
|
58
|
+
.action(() => execute(() => keySetCommand(globalOptions())));
|
|
59
|
+
key
|
|
60
|
+
.command("check")
|
|
61
|
+
.description("report whether the key is configured and from which source")
|
|
62
|
+
.action(() => execute(() => Promise.resolve(keyCheckCommand(globalOptions()))));
|
|
63
|
+
const config = program.command("config").description("show or change configuration");
|
|
64
|
+
config
|
|
65
|
+
.command("show")
|
|
66
|
+
.description("print the effective configuration")
|
|
67
|
+
.action(() => execute(() => Promise.resolve(configShowCommand(globalOptions()))));
|
|
68
|
+
config
|
|
69
|
+
.command("set <key> <value>")
|
|
70
|
+
.description("set a dotted config value, e.g. models.main glm-5.3")
|
|
71
|
+
.action((keyPath, value) => execute(() => Promise.resolve(configSetCommand(keyPath, value, globalOptions()))));
|
|
72
|
+
const project = program.command("project").description("per-project CLAUDE.md / AGENTS.md integration");
|
|
73
|
+
project
|
|
74
|
+
.command("init")
|
|
75
|
+
.description("create or update the GLM delegation managed blocks")
|
|
76
|
+
.action(() => execute(() => Promise.resolve(projectInitCommand(globalOptions()))));
|
|
77
|
+
project
|
|
78
|
+
.command("remove")
|
|
79
|
+
.description("remove the managed blocks, preserving user content")
|
|
80
|
+
.action(() => execute(() => Promise.resolve(projectRemoveCommand(globalOptions()))));
|
|
81
|
+
const skill = program.command("skill").description("manage the optional Codex delegation skill");
|
|
82
|
+
skill
|
|
83
|
+
.command("install")
|
|
84
|
+
.description("install the glm-delegation skill for Codex")
|
|
85
|
+
.action(() => execute(() => Promise.resolve(skillInstallCommand(globalOptions()))));
|
|
86
|
+
skill
|
|
87
|
+
.command("remove")
|
|
88
|
+
.description("remove the glm-delegation skill")
|
|
89
|
+
.action(() => execute(() => Promise.resolve(skillRemoveCommand(globalOptions()))));
|
|
90
|
+
program
|
|
91
|
+
.command("uninstall")
|
|
92
|
+
.description("guided removal (keeps ZAI_API_KEY by default)")
|
|
93
|
+
.action(() => execute(() => uninstallCommand(globalOptions())));
|
|
94
|
+
export async function main(argv) {
|
|
95
|
+
try {
|
|
96
|
+
// argv is the full process.argv; commander's default "node" origin strips
|
|
97
|
+
// the executable and script path itself.
|
|
98
|
+
await program.parseAsync(argv);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
process.stderr.write(error instanceof Error ? `${error.message}\n` : `${String(error)}\n`);
|
|
102
|
+
process.exitCode = ExitCode.InvalidArgs;
|
|
103
|
+
}
|
|
104
|
+
return Number(process.exitCode ?? ExitCode.Success);
|
|
105
|
+
}
|
|
106
|
+
if (isMainModule(import.meta.url)) {
|
|
107
|
+
void main(process.argv).then((code) => process.exit(code));
|
|
108
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { loadConfig, saveConfig, setConfigValue } from "../core/config.js";
|
|
2
|
+
import { configPath } from "../core/paths.js";
|
|
3
|
+
import { emitJson } from "./context.js";
|
|
4
|
+
/** glm-router config show (spec §28). */
|
|
5
|
+
export function configShowCommand(options) {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
if (options.json) {
|
|
8
|
+
emitJson({ path: configPath(), config });
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
const lines = [
|
|
12
|
+
`Provider: Z.ai`,
|
|
13
|
+
`Main model: ${config.models.main}`,
|
|
14
|
+
`Fast model: ${config.models.fast}`,
|
|
15
|
+
"",
|
|
16
|
+
"Worker:",
|
|
17
|
+
` max turns: ${config.worker.maxTurns}`,
|
|
18
|
+
"",
|
|
19
|
+
"Review:",
|
|
20
|
+
` max turns: ${config.review.maxTurns}`,
|
|
21
|
+
"",
|
|
22
|
+
"Integrations:",
|
|
23
|
+
` Claude: ${config.integrations.claude ? "enabled" : "disabled"}`,
|
|
24
|
+
` Codex: ${config.integrations.codex ? "enabled" : "disabled"}`,
|
|
25
|
+
` Codex skill: ${config.integrations.codexSkill ? "enabled" : "disabled"}`,
|
|
26
|
+
"",
|
|
27
|
+
`Config file: ${configPath()}`,
|
|
28
|
+
];
|
|
29
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
/** glm-router config set <dotted.key> <value> (spec §28). */
|
|
33
|
+
export function configSetCommand(key, value, _options) {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
const updated = setConfigValue(config, key, value);
|
|
36
|
+
saveConfig(updated);
|
|
37
|
+
process.stdout.write(`✓ ${key} = ${value}\n`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ExitCode, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
2
|
+
import { logger } from "../core/logging.js";
|
|
3
|
+
/** Configure the shared logger from global flags (spec §8, §37). */
|
|
4
|
+
export function applyGlobalOptions(options) {
|
|
5
|
+
const level = options.verbose ? "debug" : "info";
|
|
6
|
+
logger.setLevel(level, options.quiet ?? false);
|
|
7
|
+
}
|
|
8
|
+
/** Uniform error reporting for command actions. */
|
|
9
|
+
export function reportError(error) {
|
|
10
|
+
if (error instanceof GlmRouterError) {
|
|
11
|
+
process.stderr.write(formatGlmError(error) + "\n");
|
|
12
|
+
return error.exitCode;
|
|
13
|
+
}
|
|
14
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
15
|
+
process.stderr.write(`ERROR [INTERNAL]\n\n${message}\n`);
|
|
16
|
+
return ExitCode.GenericFailure;
|
|
17
|
+
}
|
|
18
|
+
export function emitJson(value) {
|
|
19
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
20
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { runDoctorChecks, doctorHasFailures } from "./doctor.js";
|
|
2
|
+
import { emitJson } from "./context.js";
|
|
3
|
+
import { logger } from "../core/logging.js";
|
|
4
|
+
const SYMBOL = {
|
|
5
|
+
ok: "✓",
|
|
6
|
+
warn: "⚠",
|
|
7
|
+
fail: "✗",
|
|
8
|
+
};
|
|
9
|
+
function renderText(results, networkResult) {
|
|
10
|
+
const lines = ["GLM Coding Router Doctor", ""];
|
|
11
|
+
let currentSection = "";
|
|
12
|
+
for (const result of results) {
|
|
13
|
+
if (result.section !== currentSection) {
|
|
14
|
+
currentSection = result.section;
|
|
15
|
+
lines.push(currentSection);
|
|
16
|
+
}
|
|
17
|
+
lines.push(` ${SYMBOL[result.status]} ${result.name}`);
|
|
18
|
+
if (result.detail) {
|
|
19
|
+
lines.push(` ${result.detail}`);
|
|
20
|
+
}
|
|
21
|
+
if (result.note) {
|
|
22
|
+
lines.push(` ${result.note}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (networkResult) {
|
|
26
|
+
lines.push("Network");
|
|
27
|
+
lines.push(` ${networkResult === "reachable" ? "✓" : "⚠"} Z.ai endpoint ${networkResult}`);
|
|
28
|
+
}
|
|
29
|
+
const failures = doctorHasFailures(results);
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push(`Status: ${failures ? "ISSUES DETECTED" : "HEALTHY"}`);
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
/** Lightweight endpoint reachability probe (spec §42). Never consumes coding quota. */
|
|
35
|
+
export async function probeEndpoint(baseUrl) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(baseUrl, {
|
|
38
|
+
method: "GET",
|
|
39
|
+
signal: AbortSignal.timeout(10_000),
|
|
40
|
+
});
|
|
41
|
+
// Any HTTP response proves reachability; auth errors are expected without a key.
|
|
42
|
+
return response.status < 500 ? "reachable" : `reachable with errors (HTTP ${response.status})`;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return "not reachable";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function doctorCommand(options) {
|
|
49
|
+
const report = runDoctorChecks();
|
|
50
|
+
if (options.json) {
|
|
51
|
+
emitJson({
|
|
52
|
+
status: doctorHasFailures(report.results) ? "ISSUES" : "HEALTHY",
|
|
53
|
+
checks: report.results,
|
|
54
|
+
keySource: report.keySource,
|
|
55
|
+
});
|
|
56
|
+
return doctorHasFailures(report.results) ? 1 : 0;
|
|
57
|
+
}
|
|
58
|
+
const networkResult = options.network
|
|
59
|
+
? await probeEndpoint(report.config.provider.anthropicBaseUrl)
|
|
60
|
+
: undefined;
|
|
61
|
+
process.stdout.write(renderText(report.results, networkResult) + "\n");
|
|
62
|
+
logger.debug(`anthropic base url: ${report.config.provider.anthropicBaseUrl}`);
|
|
63
|
+
return doctorHasFailures(report.results) ? 1 : 0;
|
|
64
|
+
}
|