glm-coding-router 0.1.0 → 0.2.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 -21
- package/README.md +42 -4
- package/dist/bin/glm-chat.js +4 -2
- package/dist/bin/glm-fast.js +50 -0
- package/dist/bin/glm-review.js +4 -2
- package/dist/bin/glm-worker.js +4 -2
- package/dist/commands/config.js +7 -7
- package/dist/commands/doctor-command.js +9 -5
- package/dist/commands/doctor.js +5 -4
- package/dist/commands/init.js +32 -18
- package/dist/commands/key.js +10 -7
- package/dist/commands/project-init.js +11 -6
- package/dist/commands/status.js +7 -6
- package/dist/commands/uninstall.js +30 -16
- package/dist/core/claude.js +13 -9
- package/dist/core/config.js +10 -0
- package/dist/core/profile.js +57 -0
- package/dist/templates/agents-block.js +44 -44
- package/dist/templates/claude-block.js +47 -47
- package/dist/templates/glm-delegation-skill.js +65 -65
- package/package.json +47 -46
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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
CHANGED
|
@@ -27,9 +27,9 @@ Claude / Codex → shell → glm-worker → claude.exe harness → Z.ai endpoint
|
|
|
27
27
|
│
|
|
28
28
|
┌───────────────┼───────────────┐
|
|
29
29
|
▼ ▼ ▼
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
glm-chat glm-fast glm-worker glm-review
|
|
31
|
+
│ │ │ │
|
|
32
|
+
└─────────┴──────────┴──────────┘
|
|
33
33
|
claude.exe
|
|
34
34
|
(injected environment only)
|
|
35
35
|
│
|
|
@@ -67,6 +67,7 @@ After `glm-router init`:
|
|
|
67
67
|
|
|
68
68
|
```powershell
|
|
69
69
|
glm-chat
|
|
70
|
+
glm-fast
|
|
70
71
|
glm-worker "Implement validation and add tests"
|
|
71
72
|
glm-review "Analyze the auth module"
|
|
72
73
|
```
|
|
@@ -85,6 +86,18 @@ glm-chat --any-claude-flag
|
|
|
85
86
|
|
|
86
87
|
Your normal `claude` command and its authentication are untouched.
|
|
87
88
|
|
|
89
|
+
## glm-fast
|
|
90
|
+
|
|
91
|
+
Interactive GLM-backed session pinned to the **fast model** (`models.fast`,
|
|
92
|
+
`glm-5.3-flash` by default) — every model slot in the child environment maps to
|
|
93
|
+
it, so whichever tier Claude Code picks, it gets the fast model. Same pass-through
|
|
94
|
+
arguments as `glm-chat`:
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
glm-fast
|
|
98
|
+
glm-fast --profile air
|
|
99
|
+
```
|
|
100
|
+
|
|
88
101
|
## glm-worker
|
|
89
102
|
|
|
90
103
|
Headless implementation worker:
|
|
@@ -123,6 +136,31 @@ glm-review "Inspect this repository"
|
|
|
123
136
|
|
|
124
137
|
Runs with `--tools Read,Glob,Grep` — it cannot edit files or run commands.
|
|
125
138
|
|
|
139
|
+
## Profiles
|
|
140
|
+
|
|
141
|
+
All four task binaries (`glm-chat`, `glm-fast`, `glm-worker`, `glm-review`)
|
|
142
|
+
accept `--profile <name>` to overlay saved model/maxTurns settings. Profiles
|
|
143
|
+
live in `config.json`:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"profiles": {
|
|
148
|
+
"test": { "workerMaxTurns": 10, "fast": "glm-5.3-flash" },
|
|
149
|
+
"frontend": { "main": "glm-5.3", "reviewMaxTurns": 30 }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```powershell
|
|
155
|
+
glm-worker --profile test "Add failing test then fix it"
|
|
156
|
+
glm-review --profile frontend "Review the component tree"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Fields (all optional): `main`, `fast`, `workerMaxTurns`, `reviewMaxTurns`.
|
|
160
|
+
Unknown profile names fail with `ERROR [11]` listing the available ones.
|
|
161
|
+
Note: `--profile` belongs to these wrappers — it shadows Claude Code's own
|
|
162
|
+
`--profile` flag inside them.
|
|
163
|
+
|
|
126
164
|
## CLI reference
|
|
127
165
|
|
|
128
166
|
```text
|
|
@@ -241,7 +279,7 @@ npm publish
|
|
|
241
279
|
```
|
|
242
280
|
|
|
243
281
|
`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`.
|
|
282
|
+
(`glm-router`, `glm-chat`, `glm-fast`, `glm-worker`, `glm-review`) are declared in `bin`.
|
|
245
283
|
|
|
246
284
|
## License
|
|
247
285
|
|
package/dist/bin/glm-chat.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createGlmEnv } from "../core/env.js";
|
|
|
5
5
|
import { Errors, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
6
6
|
import { isMainModule } from "../core/main-guard.js";
|
|
7
7
|
import { logger, redact } from "../core/logging.js";
|
|
8
|
+
import { applyProfile, extractProfileFlag } from "../core/profile.js";
|
|
8
9
|
import { spawnAgent } from "../core/process.js";
|
|
9
10
|
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
10
11
|
/**
|
|
@@ -12,7 +13,8 @@ import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
|
12
13
|
* spawn claude interactively with pass-through arguments.
|
|
13
14
|
*/
|
|
14
15
|
export async function runChat(argv) {
|
|
15
|
-
const
|
|
16
|
+
const { rest, profile } = extractProfileFlag(argv);
|
|
17
|
+
const config = applyProfile(loadConfig(), profile);
|
|
16
18
|
const resolved = resolveZaiApiKey();
|
|
17
19
|
if (!resolved) {
|
|
18
20
|
throw Errors.zaiKeyMissing();
|
|
@@ -22,7 +24,7 @@ export async function runChat(argv) {
|
|
|
22
24
|
logger.debug(`spawning ${claudePath}`);
|
|
23
25
|
logger.debug(redact(`env ANTHROPIC_BASE_URL=${env.ANTHROPIC_BASE_URL}`, [resolved.key]));
|
|
24
26
|
return spawnAgent(claudePath, {
|
|
25
|
-
args: [...
|
|
27
|
+
args: [...rest],
|
|
26
28
|
cwd: process.cwd(),
|
|
27
29
|
env,
|
|
28
30
|
interactive: true,
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { applyProfile, extractProfileFlag } from "../core/profile.js";
|
|
9
|
+
import { spawnAgent } from "../core/process.js";
|
|
10
|
+
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
11
|
+
/**
|
|
12
|
+
* Effective config for glm-fast: every model slot pinned to the fast model
|
|
13
|
+
* (specs/glm-fast-profiles.md). Applied AFTER the profile so a profile's
|
|
14
|
+
* `fast` model flows into all slots; a profile's `main` is overridden by design.
|
|
15
|
+
*/
|
|
16
|
+
export function fastModelConfig(config) {
|
|
17
|
+
return { ...config, models: { main: config.models.fast, fast: config.models.fast } };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* glm-fast (spec §54, specs/glm-fast-profiles.md): interactive chat pinned to
|
|
21
|
+
* the fast model. Pass-through args like glm-chat; supports --profile.
|
|
22
|
+
*/
|
|
23
|
+
export async function runFast(argv) {
|
|
24
|
+
const { rest, profile } = extractProfileFlag(argv);
|
|
25
|
+
const config = fastModelConfig(applyProfile(loadConfig(), profile));
|
|
26
|
+
const resolved = resolveZaiApiKey();
|
|
27
|
+
if (!resolved) {
|
|
28
|
+
throw Errors.zaiKeyMissing();
|
|
29
|
+
}
|
|
30
|
+
const claudePath = locateClaude(config);
|
|
31
|
+
const env = createGlmEnv(config, resolved.key);
|
|
32
|
+
logger.debug(`spawning ${claudePath}`);
|
|
33
|
+
logger.debug(redact(`env ANTHROPIC_BASE_URL=${env.ANTHROPIC_BASE_URL}`, [resolved.key]));
|
|
34
|
+
return spawnAgent(claudePath, {
|
|
35
|
+
args: [...rest],
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
env,
|
|
38
|
+
interactive: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (isMainModule(import.meta.url)) {
|
|
42
|
+
runFast(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
43
|
+
if (error instanceof GlmRouterError) {
|
|
44
|
+
process.stderr.write(formatGlmError(error) + "\n");
|
|
45
|
+
process.exit(error.exitCode);
|
|
46
|
+
}
|
|
47
|
+
process.stderr.write(String(error) + "\n");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/bin/glm-review.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createGlmEnv } from "../core/env.js";
|
|
|
5
5
|
import { Errors, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
6
6
|
import { isMainModule } from "../core/main-guard.js";
|
|
7
7
|
import { logger, redact } from "../core/logging.js";
|
|
8
|
+
import { applyProfile, extractProfileFlag } from "../core/profile.js";
|
|
8
9
|
import { readStdin, resolvePrompt } from "../core/prompt.js";
|
|
9
10
|
import { spawnAgent } from "../core/process.js";
|
|
10
11
|
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
@@ -18,8 +19,9 @@ export function buildReviewArgs(prompt, config) {
|
|
|
18
19
|
* discovery, duplicate detection, dependency inspection, and review.
|
|
19
20
|
*/
|
|
20
21
|
export async function runReview(argv) {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
22
|
+
const { rest, profile } = extractProfileFlag(argv);
|
|
23
|
+
const prompt = await resolvePrompt(rest, readStdin, "glm-review");
|
|
24
|
+
const config = applyProfile(loadConfig(), profile);
|
|
23
25
|
const resolved = resolveZaiApiKey();
|
|
24
26
|
if (!resolved) {
|
|
25
27
|
throw Errors.zaiKeyMissing();
|
package/dist/bin/glm-worker.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createGlmEnv } from "../core/env.js";
|
|
|
5
5
|
import { Errors, formatGlmError, GlmRouterError } from "../core/errors.js";
|
|
6
6
|
import { isMainModule } from "../core/main-guard.js";
|
|
7
7
|
import { logger, redact } from "../core/logging.js";
|
|
8
|
+
import { applyProfile, extractProfileFlag } from "../core/profile.js";
|
|
8
9
|
import { resolvePrompt } from "../core/prompt.js";
|
|
9
10
|
import { spawnAgent } from "../core/process.js";
|
|
10
11
|
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
@@ -28,8 +29,9 @@ export function buildWorkerArgs(prompt, config) {
|
|
|
28
29
|
* --dangerously-skip-permissions.
|
|
29
30
|
*/
|
|
30
31
|
export async function runWorker(argv) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
32
|
+
const { rest, profile } = extractProfileFlag(argv);
|
|
33
|
+
const prompt = await resolvePrompt(rest);
|
|
34
|
+
const config = applyProfile(loadConfig(), profile);
|
|
33
35
|
const resolved = resolveZaiApiKey();
|
|
34
36
|
if (!resolved) {
|
|
35
37
|
throw Errors.zaiKeyMissing();
|
package/dist/commands/config.js
CHANGED
|
@@ -2,10 +2,10 @@ import { loadConfig, saveConfig, setConfigValue } from "../core/config.js";
|
|
|
2
2
|
import { configPath } from "../core/paths.js";
|
|
3
3
|
import { emitJson } from "./context.js";
|
|
4
4
|
/** glm-router config show (spec §28). */
|
|
5
|
-
export function configShowCommand(options) {
|
|
6
|
-
const config = loadConfig();
|
|
5
|
+
export function configShowCommand(options, deps = {}) {
|
|
6
|
+
const config = loadConfig(deps.home);
|
|
7
7
|
if (options.json) {
|
|
8
|
-
emitJson({ path: configPath(), config });
|
|
8
|
+
emitJson({ path: configPath(deps.home), config });
|
|
9
9
|
return 0;
|
|
10
10
|
}
|
|
11
11
|
const lines = [
|
|
@@ -24,16 +24,16 @@ export function configShowCommand(options) {
|
|
|
24
24
|
` Codex: ${config.integrations.codex ? "enabled" : "disabled"}`,
|
|
25
25
|
` Codex skill: ${config.integrations.codexSkill ? "enabled" : "disabled"}`,
|
|
26
26
|
"",
|
|
27
|
-
`Config file: ${configPath()}`,
|
|
27
|
+
`Config file: ${configPath(deps.home)}`,
|
|
28
28
|
];
|
|
29
29
|
process.stdout.write(lines.join("\n") + "\n");
|
|
30
30
|
return 0;
|
|
31
31
|
}
|
|
32
32
|
/** glm-router config set <dotted.key> <value> (spec §28). */
|
|
33
|
-
export function configSetCommand(key, value, _options) {
|
|
34
|
-
const config = loadConfig();
|
|
33
|
+
export function configSetCommand(key, value, _options, deps = {}) {
|
|
34
|
+
const config = loadConfig(deps.home);
|
|
35
35
|
const updated = setConfigValue(config, key, value);
|
|
36
|
-
saveConfig(updated);
|
|
36
|
+
saveConfig(updated, deps.home);
|
|
37
37
|
process.stdout.write(`✓ ${key} = ${value}\n`);
|
|
38
38
|
return 0;
|
|
39
39
|
}
|
|
@@ -32,9 +32,9 @@ function renderText(results, networkResult) {
|
|
|
32
32
|
return lines.join("\n");
|
|
33
33
|
}
|
|
34
34
|
/** Lightweight endpoint reachability probe (spec §42). Never consumes coding quota. */
|
|
35
|
-
export async function probeEndpoint(baseUrl) {
|
|
35
|
+
export async function probeEndpoint(baseUrl, fetchImpl = fetch) {
|
|
36
36
|
try {
|
|
37
|
-
const response = await
|
|
37
|
+
const response = await fetchImpl(baseUrl, {
|
|
38
38
|
method: "GET",
|
|
39
39
|
signal: AbortSignal.timeout(10_000),
|
|
40
40
|
});
|
|
@@ -45,18 +45,22 @@ export async function probeEndpoint(baseUrl) {
|
|
|
45
45
|
return "not reachable";
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
export async function doctorCommand(options) {
|
|
49
|
-
const report = runDoctorChecks();
|
|
48
|
+
export async function doctorCommand(options, deps = {}) {
|
|
49
|
+
const report = runDoctorChecks({ home: deps.home, env: deps.env, readUserEnv: deps.readUserEnv });
|
|
50
50
|
if (options.json) {
|
|
51
|
+
const networkResult = options.network
|
|
52
|
+
? await probeEndpoint(report.config.provider.anthropicBaseUrl, deps.fetchImpl)
|
|
53
|
+
: undefined;
|
|
51
54
|
emitJson({
|
|
52
55
|
status: doctorHasFailures(report.results) ? "ISSUES" : "HEALTHY",
|
|
53
56
|
checks: report.results,
|
|
54
57
|
keySource: report.keySource,
|
|
58
|
+
network: networkResult,
|
|
55
59
|
});
|
|
56
60
|
return doctorHasFailures(report.results) ? 1 : 0;
|
|
57
61
|
}
|
|
58
62
|
const networkResult = options.network
|
|
59
|
-
? await probeEndpoint(report.config.provider.anthropicBaseUrl)
|
|
63
|
+
? await probeEndpoint(report.config.provider.anthropicBaseUrl, deps.fetchImpl)
|
|
60
64
|
: undefined;
|
|
61
65
|
process.stdout.write(renderText(report.results, networkResult) + "\n");
|
|
62
66
|
logger.debug(`anthropic base url: ${report.config.provider.anthropicBaseUrl}`);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -27,6 +27,7 @@ function gitFound() {
|
|
|
27
27
|
export function runDoctorChecks(options = {}) {
|
|
28
28
|
const results = [];
|
|
29
29
|
const home = options.home ?? os.homedir();
|
|
30
|
+
const env = options.env ?? process.env;
|
|
30
31
|
// --- System ---
|
|
31
32
|
results.push(check("System", isWindows() ? windowsVersionName() : `Platform ${process.platform}`, isWindows() ? "ok" : "fail", undefined, isWindows() ? undefined : "v0.1 targets Windows only."));
|
|
32
33
|
const nodeMajor = nodeVersionMajor();
|
|
@@ -50,16 +51,16 @@ export function runDoctorChecks(options = {}) {
|
|
|
50
51
|
}
|
|
51
52
|
// --- Agents ---
|
|
52
53
|
try {
|
|
53
|
-
const claudePath = locateClaude(config);
|
|
54
|
+
const claudePath = locateClaude(config, env);
|
|
54
55
|
results.push(check("Agents", "Claude Code", "ok", claudePath));
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
57
58
|
results.push(check("Agents", "Claude Code", "fail", error instanceof Error ? error.message : String(error)));
|
|
58
59
|
}
|
|
59
|
-
const codexPath = locateCodex(config);
|
|
60
|
+
const codexPath = locateCodex(config, env);
|
|
60
61
|
results.push(check("Agents", "Codex", codexPath ? "ok" : "warn", codexPath, codexPath ? undefined : "Optional — Claude-only setups are supported."));
|
|
61
62
|
// --- Z.ai key ---
|
|
62
|
-
const resolved = resolveZaiApiKey();
|
|
63
|
+
const resolved = resolveZaiApiKey({ env, readUserEnv: options.readUserEnv });
|
|
63
64
|
results.push(check("Z.ai", "ZAI_API_KEY", resolved ? "ok" : "fail", resolved ? `configured (${resolved.source})` : "not found", resolved ? undefined : "Run: glm-router key set"));
|
|
64
65
|
results.push(check("Z.ai", "Anthropic endpoint", "ok", config.provider.anthropicBaseUrl));
|
|
65
66
|
// --- Commands (PATH shims; a dev checkout warns instead of failing) ---
|
|
@@ -79,7 +80,7 @@ export function runDoctorChecks(options = {}) {
|
|
|
79
80
|
? "not installed (optional)"
|
|
80
81
|
: "Codex home not detected — skill skipped (optional)"));
|
|
81
82
|
// --- Environment: the Orca stale-env case (spec §9, §10) ---
|
|
82
|
-
const hasProcessKey = Boolean(
|
|
83
|
+
const hasProcessKey = Boolean(env.ZAI_API_KEY && env.ZAI_API_KEY.trim());
|
|
83
84
|
if (hasProcessKey) {
|
|
84
85
|
results.push(check("Environment", "Process environment", "ok", "ZAI_API_KEY visible in current process"));
|
|
85
86
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { loadConfig, saveConfig } from "../core/config.js";
|
|
5
5
|
import { configPath } from "../core/paths.js";
|
|
6
|
+
import { ExitCode } from "../core/errors.js";
|
|
6
7
|
import { runDoctorChecks } from "./doctor.js";
|
|
7
8
|
import { setWindowsUserEnv, ZAI_API_KEY_ENV } from "../core/zai-key.js";
|
|
8
9
|
import { isWindows } from "../core/platform.js";
|
|
@@ -16,15 +17,14 @@ function renderEnvironment(results) {
|
|
|
16
17
|
}
|
|
17
18
|
return lines.join("\n");
|
|
18
19
|
}
|
|
19
|
-
async function askChoices(existingKey, options) {
|
|
20
|
+
async function askChoices(existingKey, options, prompt) {
|
|
20
21
|
if (options.yes) {
|
|
21
|
-
return { configureKey: !existingKey, claude: true, codex: true, codexSkill: true };
|
|
22
|
+
return { kind: "ok", choices: { configureKey: !existingKey, claude: true, codex: true, codexSkill: true } };
|
|
22
23
|
}
|
|
23
24
|
if (!process.stdin.isTTY) {
|
|
24
|
-
|
|
25
|
-
process.exit(2);
|
|
25
|
+
return { kind: "non-interactive" };
|
|
26
26
|
}
|
|
27
|
-
const response = await
|
|
27
|
+
const response = await prompt([
|
|
28
28
|
{
|
|
29
29
|
type: existingKey ? null : "confirm",
|
|
30
30
|
name: "configureKey",
|
|
@@ -36,27 +36,41 @@ async function askChoices(existingKey, options) {
|
|
|
36
36
|
{ type: "confirm", name: "codexSkill", message: "Install Codex delegation skill?", initial: true },
|
|
37
37
|
]);
|
|
38
38
|
if (response.claude === undefined) {
|
|
39
|
-
|
|
40
|
-
process.exit(1);
|
|
39
|
+
return { kind: "cancelled" };
|
|
41
40
|
}
|
|
42
41
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
kind: "ok",
|
|
43
|
+
choices: {
|
|
44
|
+
configureKey: existingKey ? true : Boolean(response.configureKey),
|
|
45
|
+
claude: Boolean(response.claude),
|
|
46
|
+
codex: Boolean(response.codex),
|
|
47
|
+
codexSkill: Boolean(response.codexSkill),
|
|
48
|
+
},
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
51
|
/** glm-router init (spec §7): environment report, key setup, config, skill. Idempotent. */
|
|
50
|
-
export async function initCommand(options) {
|
|
52
|
+
export async function initCommand(options, deps = {}) {
|
|
53
|
+
const prompt = deps.prompt ?? prompts;
|
|
54
|
+
const home = deps.home ?? os.homedir();
|
|
55
|
+
const env = deps.env ?? process.env;
|
|
56
|
+
const setEnv = deps.setEnv ?? setWindowsUserEnv;
|
|
51
57
|
process.stdout.write(`GLM Coding Router v${version}\n\n`);
|
|
52
|
-
const report = runDoctorChecks();
|
|
58
|
+
const report = runDoctorChecks({ home, env, readUserEnv: deps.readUserEnv });
|
|
53
59
|
process.stdout.write("Environment\n\n");
|
|
54
60
|
process.stdout.write(renderEnvironment(report.results) + "\n\n");
|
|
55
61
|
const existingKey = report.keySource !== undefined;
|
|
56
|
-
const
|
|
62
|
+
const askResult = await askChoices(existingKey, options, prompt);
|
|
63
|
+
if (askResult.kind === "non-interactive") {
|
|
64
|
+
process.stdout.write("Non-interactive terminal detected. Re-run with --yes to accept defaults.\n");
|
|
65
|
+
return ExitCode.InvalidArgs;
|
|
66
|
+
}
|
|
67
|
+
if (askResult.kind === "cancelled") {
|
|
68
|
+
process.stdout.write("Cancelled.\n");
|
|
69
|
+
return ExitCode.GenericFailure;
|
|
70
|
+
}
|
|
71
|
+
const choices = askResult.choices;
|
|
57
72
|
process.stdout.write("\nInstalling...\n\n");
|
|
58
|
-
const
|
|
59
|
-
const config = loadConfig();
|
|
73
|
+
const config = loadConfig(home);
|
|
60
74
|
const nextConfig = {
|
|
61
75
|
...config,
|
|
62
76
|
integrations: {
|
|
@@ -74,7 +88,7 @@ export async function initCommand(options) {
|
|
|
74
88
|
process.stdout.write("⚠ Key storage requires Windows in v0.1 — skipped\n");
|
|
75
89
|
}
|
|
76
90
|
else {
|
|
77
|
-
const keyResponse = await
|
|
91
|
+
const keyResponse = await prompt({
|
|
78
92
|
type: "password",
|
|
79
93
|
name: "key",
|
|
80
94
|
message: "Enter Z.ai Coding Plan API key:",
|
|
@@ -84,7 +98,7 @@ export async function initCommand(options) {
|
|
|
84
98
|
process.stderr.write("Cancelled.\n");
|
|
85
99
|
return 1;
|
|
86
100
|
}
|
|
87
|
-
|
|
101
|
+
setEnv(ZAI_API_KEY_ENV, String(keyResponse.key).trim());
|
|
88
102
|
process.stdout.write(`✓ ${ZAI_API_KEY_ENV} configured\n`);
|
|
89
103
|
}
|
|
90
104
|
}
|
package/dist/commands/key.js
CHANGED
|
@@ -5,9 +5,11 @@ import { logger } from "../core/logging.js";
|
|
|
5
5
|
import { ZAI_API_KEY_ENV, deleteWindowsUserEnv, resolveZaiApiKey, setWindowsUserEnv, } from "../core/zai-key.js";
|
|
6
6
|
import { emitJson } from "./context.js";
|
|
7
7
|
/** glm-router key set (spec §11): prompt, save to Windows User Environment. */
|
|
8
|
-
export async function keySetCommand(_options) {
|
|
8
|
+
export async function keySetCommand(_options, deps = {}) {
|
|
9
9
|
assertWindows();
|
|
10
|
-
const
|
|
10
|
+
const prompt = deps.prompt ?? prompts;
|
|
11
|
+
const setEnv = deps.setEnv ?? setWindowsUserEnv;
|
|
12
|
+
const response = await prompt({
|
|
11
13
|
type: "password",
|
|
12
14
|
name: "key",
|
|
13
15
|
message: "Enter Z.ai Coding Plan API key:",
|
|
@@ -19,7 +21,7 @@ export async function keySetCommand(_options) {
|
|
|
19
21
|
}
|
|
20
22
|
const key = String(response.key).trim();
|
|
21
23
|
try {
|
|
22
|
-
|
|
24
|
+
setEnv(ZAI_API_KEY_ENV, key);
|
|
23
25
|
}
|
|
24
26
|
catch (error) {
|
|
25
27
|
logger.error(`Failed to write the Windows User Environment: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -30,8 +32,8 @@ export async function keySetCommand(_options) {
|
|
|
30
32
|
return 0;
|
|
31
33
|
}
|
|
32
34
|
/** glm-router key check (spec §11): report presence + source; never the value. */
|
|
33
|
-
export function keyCheckCommand(options) {
|
|
34
|
-
const resolved = resolveZaiApiKey();
|
|
35
|
+
export function keyCheckCommand(options, deps = {}) {
|
|
36
|
+
const resolved = resolveZaiApiKey({ env: deps.env, readUserEnv: deps.readUserEnv });
|
|
35
37
|
if (options.json) {
|
|
36
38
|
emitJson({
|
|
37
39
|
configured: Boolean(resolved),
|
|
@@ -50,7 +52,8 @@ export function keyCheckCommand(options) {
|
|
|
50
52
|
return 0;
|
|
51
53
|
}
|
|
52
54
|
/** Used by uninstall; keeps the key by default (spec §44). */
|
|
53
|
-
export async function keyRemoveCommand() {
|
|
55
|
+
export async function keyRemoveCommand(deps = {}) {
|
|
54
56
|
assertWindows();
|
|
55
|
-
deleteWindowsUserEnv
|
|
57
|
+
const deleteEnv = deps.deleteEnv ?? deleteWindowsUserEnv;
|
|
58
|
+
deleteEnv(ZAI_API_KEY_ENV);
|
|
56
59
|
}
|
|
@@ -29,15 +29,15 @@ export function renderChangeDiff(change) {
|
|
|
29
29
|
* CLAUDE.md and AGENTS.md at the project root. Idempotent; never overwrites
|
|
30
30
|
* user content; supports --dry-run.
|
|
31
31
|
*/
|
|
32
|
-
export function projectInitCommand(options) {
|
|
33
|
-
const root = findProjectRoot();
|
|
34
|
-
const config = loadConfig();
|
|
32
|
+
export function projectInitCommand(options, deps = {}) {
|
|
33
|
+
const root = deps.root ?? findProjectRoot();
|
|
34
|
+
const config = loadConfig(deps.home);
|
|
35
35
|
const changes = [];
|
|
36
36
|
if (config.integrations.claude) {
|
|
37
|
-
changes.push(installClaudeIntegration(root, { dryRun: options.dryRun }));
|
|
37
|
+
changes.push(installClaudeIntegration(root, { home: deps.home, dryRun: options.dryRun }));
|
|
38
38
|
}
|
|
39
39
|
if (config.integrations.codex) {
|
|
40
|
-
changes.push(installCodexIntegration(root, { dryRun: options.dryRun }));
|
|
40
|
+
changes.push(installCodexIntegration(root, { home: deps.home, dryRun: options.dryRun }));
|
|
41
41
|
}
|
|
42
42
|
if (changes.length === 0) {
|
|
43
43
|
process.stdout.write("All integrations are disabled in config — nothing to do.\n");
|
|
@@ -45,7 +45,12 @@ export function projectInitCommand(options) {
|
|
|
45
45
|
}
|
|
46
46
|
for (const change of changes) {
|
|
47
47
|
if (options.dryRun) {
|
|
48
|
-
|
|
48
|
+
if (change.changed) {
|
|
49
|
+
process.stdout.write(`[dry-run] would update ${change.file}:\n${renderChangeDiff(change)}\n`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
process.stdout.write(`[dry-run] ${change.file} — already up to date\n`);
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
55
|
else if (!change.changed) {
|
|
51
56
|
process.stdout.write(`✓ ${change.file} — already up to date\n`);
|
package/dist/commands/status.js
CHANGED
|
@@ -7,20 +7,21 @@ import { CodexSkillInstaller } 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. */
|
|
10
|
-
export function statusCommand(options) {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
10
|
+
export function statusCommand(options, deps = {}) {
|
|
11
|
+
const home = deps.home ?? os.homedir();
|
|
12
|
+
const env = deps.env ?? process.env;
|
|
13
|
+
const config = loadConfig(home);
|
|
14
|
+
const resolved = resolveZaiApiKey({ env, readUserEnv: deps.readUserEnv });
|
|
14
15
|
const claudeInstalled = (() => {
|
|
15
16
|
try {
|
|
16
|
-
locateClaude(config);
|
|
17
|
+
locateClaude(config, env);
|
|
17
18
|
return true;
|
|
18
19
|
}
|
|
19
20
|
catch {
|
|
20
21
|
return false;
|
|
21
22
|
}
|
|
22
23
|
})();
|
|
23
|
-
const codexInstalled = Boolean(locateCodex(config));
|
|
24
|
+
const codexInstalled = Boolean(locateCodex(config, env));
|
|
24
25
|
const skillInstaller = new CodexSkillInstaller(home);
|
|
25
26
|
const skillInstalled = skillInstaller.detect() !== null && skillInstaller.isInstalled(GLM_DELEGATION_SKILL_NAME);
|
|
26
27
|
if (options.json) {
|
|
@@ -2,10 +2,11 @@ import prompts from "prompts";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { configDir } from "../core/paths.js";
|
|
5
|
+
import { ExitCode } from "../core/errors.js";
|
|
5
6
|
import { CodexSkillInstaller, glmDelegationSkill } from "../integrations/skill.js";
|
|
6
7
|
import { keyRemoveCommand } from "./key.js";
|
|
7
8
|
import { removeClaudeIntegration, removeCodexIntegration } from "../integrations/index.js";
|
|
8
|
-
async function askChoices(options) {
|
|
9
|
+
async function askChoices(options, prompt) {
|
|
9
10
|
// Defaults per spec §44: keep ZAI_API_KEY; destructive credential removal
|
|
10
11
|
// requires explicit consent, so --yes never flips it.
|
|
11
12
|
const defaults = {
|
|
@@ -15,33 +16,46 @@ async function askChoices(options) {
|
|
|
15
16
|
removeKey: false,
|
|
16
17
|
};
|
|
17
18
|
if (options.yes || options.force) {
|
|
18
|
-
return
|
|
19
|
+
return {
|
|
20
|
+
kind: "ok",
|
|
21
|
+
choices: options.force ? { ...defaults, removeProjectIntegration: true } : defaults,
|
|
22
|
+
};
|
|
19
23
|
}
|
|
20
24
|
if (!process.stdin.isTTY) {
|
|
21
|
-
|
|
22
|
-
process.exit(2);
|
|
25
|
+
return { kind: "non-interactive" };
|
|
23
26
|
}
|
|
24
|
-
const response = await
|
|
27
|
+
const response = await prompt([
|
|
25
28
|
{ type: "confirm", name: "removeConfig", message: "Remove global configuration?", initial: true },
|
|
26
29
|
{ type: "confirm", name: "removeSkill", message: "Remove Codex skill?", initial: true },
|
|
27
30
|
{ type: "confirm", name: "removeProject", message: "Remove current project integration?", initial: false },
|
|
28
31
|
{ type: "confirm", name: "removeKey", message: "Remove ZAI_API_KEY?", initial: false },
|
|
29
32
|
]);
|
|
30
33
|
if (response.removeConfig === undefined) {
|
|
31
|
-
|
|
32
|
-
process.exit(1);
|
|
34
|
+
return { kind: "cancelled" };
|
|
33
35
|
}
|
|
34
36
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
kind: "ok",
|
|
38
|
+
choices: {
|
|
39
|
+
removeConfig: Boolean(response.removeConfig),
|
|
40
|
+
removeSkill: Boolean(response.removeSkill),
|
|
41
|
+
removeProjectIntegration: Boolean(response.removeProject),
|
|
42
|
+
removeKey: Boolean(response.removeKey),
|
|
43
|
+
},
|
|
39
44
|
};
|
|
40
45
|
}
|
|
41
46
|
/** glm-router uninstall (spec §44): wizard with safe defaults. */
|
|
42
|
-
export async function uninstallCommand(options) {
|
|
43
|
-
const
|
|
44
|
-
|
|
47
|
+
export async function uninstallCommand(options, deps = {}) {
|
|
48
|
+
const askResult = await askChoices(options, deps.prompt ?? prompts);
|
|
49
|
+
if (askResult.kind === "non-interactive") {
|
|
50
|
+
process.stdout.write("Non-interactive terminal detected. Re-run with --yes for safe defaults.\n");
|
|
51
|
+
return ExitCode.InvalidArgs;
|
|
52
|
+
}
|
|
53
|
+
if (askResult.kind === "cancelled") {
|
|
54
|
+
process.stdout.write("Cancelled.\n");
|
|
55
|
+
return ExitCode.GenericFailure;
|
|
56
|
+
}
|
|
57
|
+
const choices = askResult.choices;
|
|
58
|
+
const home = deps.home ?? os.homedir();
|
|
45
59
|
if (choices.removeSkill) {
|
|
46
60
|
const skillInstaller = new CodexSkillInstaller(home);
|
|
47
61
|
const skill = glmDelegationSkill();
|
|
@@ -54,7 +68,7 @@ export async function uninstallCommand(options) {
|
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
if (choices.removeProjectIntegration) {
|
|
57
|
-
const root = process.cwd();
|
|
71
|
+
const root = deps.root ?? process.cwd();
|
|
58
72
|
removeClaudeIntegration(root);
|
|
59
73
|
removeCodexIntegration(root);
|
|
60
74
|
process.stdout.write("✓ Project integration removed (CLAUDE.md / AGENTS.md managed blocks)\n");
|
|
@@ -70,7 +84,7 @@ export async function uninstallCommand(options) {
|
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
if (choices.removeKey) {
|
|
73
|
-
await keyRemoveCommand();
|
|
87
|
+
await keyRemoveCommand({ deleteEnv: deps.deleteEnv });
|
|
74
88
|
process.stdout.write("✓ ZAI_API_KEY removed from Windows User Environment\n");
|
|
75
89
|
}
|
|
76
90
|
else {
|
package/dist/core/claude.js
CHANGED
|
@@ -3,13 +3,14 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Errors } from "./errors.js";
|
|
5
5
|
import { isWindows } from "./platform.js";
|
|
6
|
-
function runWhere(name) {
|
|
6
|
+
function runWhere(name, env = process.env) {
|
|
7
7
|
const finder = isWindows() ? "where.exe" : "which";
|
|
8
8
|
try {
|
|
9
9
|
const output = execFileSync(finder, [name], {
|
|
10
10
|
encoding: "utf8",
|
|
11
11
|
windowsHide: true,
|
|
12
12
|
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
env,
|
|
13
14
|
});
|
|
14
15
|
return output
|
|
15
16
|
.split(/\r?\n/)
|
|
@@ -55,14 +56,17 @@ function preferNative(matches) {
|
|
|
55
56
|
* 2. PATH search through Node
|
|
56
57
|
* 3. config override
|
|
57
58
|
* 4. error
|
|
59
|
+
*
|
|
60
|
+
* `env` defaults to `process.env` and is threaded through for testability —
|
|
61
|
+
* production callers never pass it.
|
|
58
62
|
*/
|
|
59
|
-
export function locateClaude(config) {
|
|
60
|
-
const matches = runWhere("claude");
|
|
63
|
+
export function locateClaude(config, env = process.env) {
|
|
64
|
+
const matches = runWhere("claude", env);
|
|
61
65
|
const fromWhere = preferNative(matches);
|
|
62
66
|
if (fromWhere) {
|
|
63
67
|
return fromWhere;
|
|
64
68
|
}
|
|
65
|
-
const fromPath = searchPathFor("claude");
|
|
69
|
+
const fromPath = searchPathFor("claude", env);
|
|
66
70
|
if (fromPath) {
|
|
67
71
|
return fromPath;
|
|
68
72
|
}
|
|
@@ -80,12 +84,12 @@ export function locateClaude(config) {
|
|
|
80
84
|
* the tool must work with a Claude-only setup. `required` callers
|
|
81
85
|
* (glm commands that need codex) get a proper error.
|
|
82
86
|
*/
|
|
83
|
-
export function locateCodex(config) {
|
|
84
|
-
const matches = runWhere("codex");
|
|
87
|
+
export function locateCodex(config, env = process.env) {
|
|
88
|
+
const matches = runWhere("codex", env);
|
|
85
89
|
if (matches.length > 0) {
|
|
86
90
|
return preferNative(matches);
|
|
87
91
|
}
|
|
88
|
-
const fromPath = searchPathFor("codex");
|
|
92
|
+
const fromPath = searchPathFor("codex", env);
|
|
89
93
|
if (fromPath) {
|
|
90
94
|
return fromPath;
|
|
91
95
|
}
|
|
@@ -95,8 +99,8 @@ export function locateCodex(config) {
|
|
|
95
99
|
}
|
|
96
100
|
return undefined;
|
|
97
101
|
}
|
|
98
|
-
export function codexRequired(config) {
|
|
99
|
-
const found = locateCodex(config);
|
|
102
|
+
export function codexRequired(config, env = process.env) {
|
|
103
|
+
const found = locateCodex(config, env);
|
|
100
104
|
if (!found) {
|
|
101
105
|
throw Errors.codexNotFound();
|
|
102
106
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -6,6 +6,13 @@ import { configDir, configPath } from "./paths.js";
|
|
|
6
6
|
export const DEFAULT_ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic";
|
|
7
7
|
export const DEFAULT_MAIN_MODEL = "glm-5.3";
|
|
8
8
|
export const DEFAULT_FAST_MODEL = "glm-5.3-flash";
|
|
9
|
+
/** Named model/maxTurns overlay selected via --profile (specs/glm-fast-profiles.md). */
|
|
10
|
+
export const ProfileSchema = z.object({
|
|
11
|
+
main: z.string().min(1).optional(),
|
|
12
|
+
fast: z.string().min(1).optional(),
|
|
13
|
+
workerMaxTurns: z.number().int().positive().optional(),
|
|
14
|
+
reviewMaxTurns: z.number().int().positive().optional(),
|
|
15
|
+
});
|
|
9
16
|
export const ConfigSchema = z.object({
|
|
10
17
|
schemaVersion: z.literal(1),
|
|
11
18
|
provider: z.object({
|
|
@@ -30,6 +37,8 @@ export const ConfigSchema = z.object({
|
|
|
30
37
|
// Optional executable overrides used by discovery (spec §33, §34).
|
|
31
38
|
claudePath: z.string().min(1).optional(),
|
|
32
39
|
codexPath: z.string().min(1).optional(),
|
|
40
|
+
// Named overlays selected via --profile (specs/glm-fast-profiles.md).
|
|
41
|
+
profiles: z.record(z.string(), ProfileSchema).default({}),
|
|
33
42
|
});
|
|
34
43
|
export function defaultConfig() {
|
|
35
44
|
return {
|
|
@@ -49,6 +58,7 @@ export function defaultConfig() {
|
|
|
49
58
|
codex: true,
|
|
50
59
|
codexSkill: true,
|
|
51
60
|
},
|
|
61
|
+
profiles: {},
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Errors } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Remove the router's --profile flag from argv (specs/glm-fast-profiles.md).
|
|
4
|
+
* Accepts `--profile name` and `--profile=name`; the first occurrence wins,
|
|
5
|
+
* later ones are consumed too so they never leak into the forwarded args.
|
|
6
|
+
*/
|
|
7
|
+
export function extractProfileFlag(argv) {
|
|
8
|
+
const rest = [];
|
|
9
|
+
let profile;
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (arg === "--profile") {
|
|
13
|
+
const value = argv[i + 1];
|
|
14
|
+
if (value === undefined) {
|
|
15
|
+
throw Errors.configInvalid("--profile requires a profile name");
|
|
16
|
+
}
|
|
17
|
+
profile ??= value;
|
|
18
|
+
i++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg.startsWith("--profile=")) {
|
|
22
|
+
const value = arg.slice("--profile=".length);
|
|
23
|
+
if (value.length === 0) {
|
|
24
|
+
throw Errors.configInvalid("--profile requires a profile name");
|
|
25
|
+
}
|
|
26
|
+
profile ??= value;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
rest.push(arg);
|
|
30
|
+
}
|
|
31
|
+
return { rest, profile };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Overlay a named profile onto the config (specs/glm-fast-profiles.md):
|
|
35
|
+
* main/fast models plus worker/review maxTurns. Unknown names fail with
|
|
36
|
+
* ERROR [11] listing the defined profiles.
|
|
37
|
+
*/
|
|
38
|
+
export function applyProfile(config, name) {
|
|
39
|
+
if (!name) {
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
const profile = config.profiles[name];
|
|
43
|
+
if (!profile) {
|
|
44
|
+
const known = Object.keys(config.profiles);
|
|
45
|
+
const listed = known.length > 0 ? known.sort().join(", ") : "(none defined)";
|
|
46
|
+
throw Errors.configInvalid(`unknown profile "${name}" — available profiles: ${listed}`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...config,
|
|
50
|
+
models: {
|
|
51
|
+
main: profile.main ?? config.models.main,
|
|
52
|
+
fast: profile.fast ?? config.models.fast,
|
|
53
|
+
},
|
|
54
|
+
worker: { maxTurns: profile.workerMaxTurns ?? config.worker.maxTurns },
|
|
55
|
+
review: { maxTurns: profile.reviewMaxTurns ?? config.review.maxTurns },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
/** Managed block content for AGENTS.md (spec §24). Keep in sync with the spec. */
|
|
2
|
-
export const AGENTS_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
|
|
3
|
-
|
|
4
|
-
## GLM Worker Delegation
|
|
5
|
-
|
|
6
|
-
Available commands:
|
|
7
|
-
|
|
8
|
-
- \`glm-worker "<task>"\`
|
|
9
|
-
- \`glm-review "<task>"\`
|
|
10
|
-
|
|
11
|
-
Codex is the primary orchestrator.
|
|
12
|
-
|
|
13
|
-
Delegate:
|
|
14
|
-
- CRUD
|
|
15
|
-
- boilerplate
|
|
16
|
-
- tests
|
|
17
|
-
- documentation
|
|
18
|
-
- mechanical refactoring
|
|
19
|
-
- repository exploration
|
|
20
|
-
- straightforward implementation
|
|
21
|
-
|
|
22
|
-
Keep in Codex:
|
|
23
|
-
- requirements
|
|
24
|
-
- planning
|
|
25
|
-
- architecture
|
|
26
|
-
- ambiguous business logic
|
|
27
|
-
- complex debugging
|
|
28
|
-
- security decisions
|
|
29
|
-
- integration
|
|
30
|
-
- final review
|
|
31
|
-
|
|
32
|
-
Before delegation create a task packet:
|
|
33
|
-
|
|
34
|
-
TASK
|
|
35
|
-
SCOPE
|
|
36
|
-
FILES ALLOWED TO MODIFY
|
|
37
|
-
FILES NOT TO MODIFY
|
|
38
|
-
REQUIREMENTS
|
|
39
|
-
CONSTRAINTS
|
|
40
|
-
ACCEPTANCE CRITERIA
|
|
41
|
-
VALIDATION
|
|
42
|
-
EXPECTED OUTPUT
|
|
43
|
-
|
|
44
|
-
Never trust a worker's success report without inspecting the resulting changes.
|
|
45
|
-
|
|
2
|
+
export const AGENTS_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
|
|
3
|
+
|
|
4
|
+
## GLM Worker Delegation
|
|
5
|
+
|
|
6
|
+
Available commands:
|
|
7
|
+
|
|
8
|
+
- \`glm-worker "<task>"\`
|
|
9
|
+
- \`glm-review "<task>"\`
|
|
10
|
+
|
|
11
|
+
Codex is the primary orchestrator.
|
|
12
|
+
|
|
13
|
+
Delegate:
|
|
14
|
+
- CRUD
|
|
15
|
+
- boilerplate
|
|
16
|
+
- tests
|
|
17
|
+
- documentation
|
|
18
|
+
- mechanical refactoring
|
|
19
|
+
- repository exploration
|
|
20
|
+
- straightforward implementation
|
|
21
|
+
|
|
22
|
+
Keep in Codex:
|
|
23
|
+
- requirements
|
|
24
|
+
- planning
|
|
25
|
+
- architecture
|
|
26
|
+
- ambiguous business logic
|
|
27
|
+
- complex debugging
|
|
28
|
+
- security decisions
|
|
29
|
+
- integration
|
|
30
|
+
- final review
|
|
31
|
+
|
|
32
|
+
Before delegation create a task packet:
|
|
33
|
+
|
|
34
|
+
TASK
|
|
35
|
+
SCOPE
|
|
36
|
+
FILES ALLOWED TO MODIFY
|
|
37
|
+
FILES NOT TO MODIFY
|
|
38
|
+
REQUIREMENTS
|
|
39
|
+
CONSTRAINTS
|
|
40
|
+
ACCEPTANCE CRITERIA
|
|
41
|
+
VALIDATION
|
|
42
|
+
EXPECTED OUTPUT
|
|
43
|
+
|
|
44
|
+
Never trust a worker's success report without inspecting the resulting changes.
|
|
45
|
+
|
|
46
46
|
<!-- glm-coding-router:end -->`;
|
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
/** Managed block content for CLAUDE.md (spec §20). Keep in sync with the spec. */
|
|
2
|
-
export const CLAUDE_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
|
|
3
|
-
|
|
4
|
-
## GLM Worker Delegation
|
|
5
|
-
|
|
6
|
-
GLM workers available:
|
|
7
|
-
|
|
8
|
-
- \`glm-worker "<task>"\`
|
|
9
|
-
- \`glm-review "<task>"\`
|
|
10
|
-
|
|
11
|
-
Delegate well-scoped, implementation-heavy work to GLM.
|
|
12
|
-
|
|
13
|
-
Use GLM for:
|
|
14
|
-
- repository exploration
|
|
15
|
-
- CRUD
|
|
16
|
-
- boilerplate
|
|
17
|
-
- tests
|
|
18
|
-
- documentation
|
|
19
|
-
- mechanical refactoring
|
|
20
|
-
- straightforward implementation
|
|
21
|
-
|
|
22
|
-
Claude remains responsible for:
|
|
23
|
-
- requirements
|
|
24
|
-
- architecture
|
|
25
|
-
- ambiguous business rules
|
|
26
|
-
- security-sensitive decisions
|
|
27
|
-
- complex debugging
|
|
28
|
-
- integration
|
|
29
|
-
- final review
|
|
30
|
-
|
|
31
|
-
Before delegation, define:
|
|
32
|
-
- task
|
|
33
|
-
- scope
|
|
34
|
-
- allowed files
|
|
35
|
-
- forbidden files
|
|
36
|
-
- requirements
|
|
37
|
-
- constraints
|
|
38
|
-
- acceptance criteria
|
|
39
|
-
- validation command
|
|
40
|
-
- expected output
|
|
41
|
-
|
|
42
|
-
After worker completion:
|
|
43
|
-
1. inspect the actual diff
|
|
44
|
-
2. validate against requirements
|
|
45
|
-
3. run relevant tests
|
|
46
|
-
4. resolve integration problems
|
|
47
|
-
5. accept only after verification
|
|
48
|
-
|
|
2
|
+
export const CLAUDE_MANAGED_BLOCK = `<!-- glm-coding-router:start -->
|
|
3
|
+
|
|
4
|
+
## GLM Worker Delegation
|
|
5
|
+
|
|
6
|
+
GLM workers available:
|
|
7
|
+
|
|
8
|
+
- \`glm-worker "<task>"\`
|
|
9
|
+
- \`glm-review "<task>"\`
|
|
10
|
+
|
|
11
|
+
Delegate well-scoped, implementation-heavy work to GLM.
|
|
12
|
+
|
|
13
|
+
Use GLM for:
|
|
14
|
+
- repository exploration
|
|
15
|
+
- CRUD
|
|
16
|
+
- boilerplate
|
|
17
|
+
- tests
|
|
18
|
+
- documentation
|
|
19
|
+
- mechanical refactoring
|
|
20
|
+
- straightforward implementation
|
|
21
|
+
|
|
22
|
+
Claude remains responsible for:
|
|
23
|
+
- requirements
|
|
24
|
+
- architecture
|
|
25
|
+
- ambiguous business rules
|
|
26
|
+
- security-sensitive decisions
|
|
27
|
+
- complex debugging
|
|
28
|
+
- integration
|
|
29
|
+
- final review
|
|
30
|
+
|
|
31
|
+
Before delegation, define:
|
|
32
|
+
- task
|
|
33
|
+
- scope
|
|
34
|
+
- allowed files
|
|
35
|
+
- forbidden files
|
|
36
|
+
- requirements
|
|
37
|
+
- constraints
|
|
38
|
+
- acceptance criteria
|
|
39
|
+
- validation command
|
|
40
|
+
- expected output
|
|
41
|
+
|
|
42
|
+
After worker completion:
|
|
43
|
+
1. inspect the actual diff
|
|
44
|
+
2. validate against requirements
|
|
45
|
+
3. run relevant tests
|
|
46
|
+
4. resolve integration problems
|
|
47
|
+
5. accept only after verification
|
|
48
|
+
|
|
49
49
|
<!-- glm-coding-router:end -->`;
|
|
@@ -1,68 +1,68 @@
|
|
|
1
1
|
/** Codex skill definition (spec §26). Keep in sync with the spec. */
|
|
2
2
|
export const GLM_DELEGATION_SKILL_NAME = "glm-delegation";
|
|
3
|
-
export const GLM_DELEGATION_SKILL_MD = `---
|
|
4
|
-
name: glm-delegation
|
|
5
|
-
description: >
|
|
6
|
-
Delegate well-scoped implementation, testing,
|
|
7
|
-
repository exploration, boilerplate, CRUD,
|
|
8
|
-
documentation, and mechanical refactoring to
|
|
9
|
-
GLM Coding Plan workers.
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# GLM Delegation
|
|
13
|
-
|
|
14
|
-
Available commands:
|
|
15
|
-
|
|
16
|
-
glm-worker "<task>"
|
|
17
|
-
glm-review "<task>"
|
|
18
|
-
|
|
19
|
-
## Use glm-review for
|
|
20
|
-
|
|
21
|
-
- repository exploration
|
|
22
|
-
- dependency analysis
|
|
23
|
-
- locating implementations
|
|
24
|
-
- call-chain discovery
|
|
25
|
-
- code review
|
|
26
|
-
|
|
27
|
-
## Use glm-worker for
|
|
28
|
-
|
|
29
|
-
- CRUD
|
|
30
|
-
- unit tests
|
|
31
|
-
- implementation
|
|
32
|
-
- documentation
|
|
33
|
-
- repetitive changes
|
|
34
|
-
- mechanical refactoring
|
|
35
|
-
|
|
36
|
-
## Keep in primary Codex agent
|
|
37
|
-
|
|
38
|
-
- requirements
|
|
39
|
-
- architecture
|
|
40
|
-
- ambiguous rules
|
|
41
|
-
- security-sensitive design
|
|
42
|
-
- difficult debugging
|
|
43
|
-
- integration
|
|
44
|
-
- final acceptance
|
|
45
|
-
|
|
46
|
-
## Delegation packet
|
|
47
|
-
|
|
48
|
-
Always provide:
|
|
49
|
-
|
|
50
|
-
TASK
|
|
51
|
-
SCOPE
|
|
52
|
-
ALLOWED FILES
|
|
53
|
-
FORBIDDEN FILES
|
|
54
|
-
REQUIREMENTS
|
|
55
|
-
CONSTRAINTS
|
|
56
|
-
ACCEPTANCE CRITERIA
|
|
57
|
-
VALIDATION
|
|
58
|
-
EXPECTED OUTPUT
|
|
59
|
-
|
|
60
|
-
## Verification
|
|
61
|
-
|
|
62
|
-
After GLM finishes:
|
|
63
|
-
|
|
64
|
-
- inspect the actual diff
|
|
65
|
-
- independently run relevant validation
|
|
66
|
-
- compare implementation with requirements
|
|
67
|
-
- reject or correct worker output when needed
|
|
3
|
+
export const GLM_DELEGATION_SKILL_MD = `---
|
|
4
|
+
name: glm-delegation
|
|
5
|
+
description: >
|
|
6
|
+
Delegate well-scoped implementation, testing,
|
|
7
|
+
repository exploration, boilerplate, CRUD,
|
|
8
|
+
documentation, and mechanical refactoring to
|
|
9
|
+
GLM Coding Plan workers.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# GLM Delegation
|
|
13
|
+
|
|
14
|
+
Available commands:
|
|
15
|
+
|
|
16
|
+
glm-worker "<task>"
|
|
17
|
+
glm-review "<task>"
|
|
18
|
+
|
|
19
|
+
## Use glm-review for
|
|
20
|
+
|
|
21
|
+
- repository exploration
|
|
22
|
+
- dependency analysis
|
|
23
|
+
- locating implementations
|
|
24
|
+
- call-chain discovery
|
|
25
|
+
- code review
|
|
26
|
+
|
|
27
|
+
## Use glm-worker for
|
|
28
|
+
|
|
29
|
+
- CRUD
|
|
30
|
+
- unit tests
|
|
31
|
+
- implementation
|
|
32
|
+
- documentation
|
|
33
|
+
- repetitive changes
|
|
34
|
+
- mechanical refactoring
|
|
35
|
+
|
|
36
|
+
## Keep in primary Codex agent
|
|
37
|
+
|
|
38
|
+
- requirements
|
|
39
|
+
- architecture
|
|
40
|
+
- ambiguous rules
|
|
41
|
+
- security-sensitive design
|
|
42
|
+
- difficult debugging
|
|
43
|
+
- integration
|
|
44
|
+
- final acceptance
|
|
45
|
+
|
|
46
|
+
## Delegation packet
|
|
47
|
+
|
|
48
|
+
Always provide:
|
|
49
|
+
|
|
50
|
+
TASK
|
|
51
|
+
SCOPE
|
|
52
|
+
ALLOWED FILES
|
|
53
|
+
FORBIDDEN FILES
|
|
54
|
+
REQUIREMENTS
|
|
55
|
+
CONSTRAINTS
|
|
56
|
+
ACCEPTANCE CRITERIA
|
|
57
|
+
VALIDATION
|
|
58
|
+
EXPECTED OUTPUT
|
|
59
|
+
|
|
60
|
+
## Verification
|
|
61
|
+
|
|
62
|
+
After GLM finishes:
|
|
63
|
+
|
|
64
|
+
- inspect the actual diff
|
|
65
|
+
- independently run relevant validation
|
|
66
|
+
- compare implementation with requirements
|
|
67
|
+
- reject or correct worker output when needed
|
|
68
68
|
`;
|
package/package.json
CHANGED
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "glm-coding-router",
|
|
3
|
-
"version": "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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"test
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"@types/
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"typescript
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "glm-coding-router",
|
|
3
|
+
"version": "0.2.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
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsx src/cli.ts",
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"lint": "eslint src tests",
|
|
28
|
+
"prepublishOnly": "npm run build && npm test"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^15.0.0",
|
|
35
|
+
"prompts": "^2.4.2",
|
|
36
|
+
"zod": "^4.6.5"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.20.3",
|
|
40
|
+
"@types/prompts": "^2.4.9",
|
|
41
|
+
"eslint": "^9.39.5",
|
|
42
|
+
"tsx": "^4.23.13",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"typescript-eslint": "^8.70.0",
|
|
45
|
+
"vitest": "^5.0.1"
|
|
46
|
+
}
|
|
47
|
+
}
|