glm-coding-router 1.0.0 → 1.1.1
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 +11 -7
- package/dist/bin/glm-worker.js +37 -4
- package/dist/commands/doctor.js +21 -5
- package/dist/commands/init.js +5 -3
- package/dist/commands/key.js +46 -9
- package/dist/commands/status.js +3 -1
- package/dist/commands/uninstall.js +2 -1
- package/dist/commands/usage.js +1 -1
- package/dist/core/config.js +31 -2
- package/dist/core/errors.js +33 -10
- package/dist/core/platform.js +37 -8
- package/dist/core/profile.js +6 -1
- package/dist/core/user-env.js +183 -0
- package/dist/core/zai-key.js +15 -59
- package/dist/mcp/server.js +2 -2
- package/package.json +48 -48
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GLM Coding Router
|
|
2
2
|
|
|
3
|
-
GLM Coding Plan workers for Claude Code and Codex
|
|
3
|
+
GLM Coding Plan workers for Claude Code and Codex — on Windows, Linux, and (experimentally) macOS.
|
|
4
4
|
|
|
5
5
|
Claude Code and Codex stay your orchestrators — they keep responsibility for requirements,
|
|
6
6
|
architecture, review, and integration. `glm-coding-router` delegates well-scoped
|
|
@@ -43,7 +43,8 @@ Claude / Codex → shell → glm-worker → claude.exe harness → Z.ai endpoint
|
|
|
43
43
|
|
|
44
44
|
## Requirements
|
|
45
45
|
|
|
46
|
-
- Windows 10/11 (
|
|
46
|
+
- Windows 10/11 or Linux (both verified); macOS is experimental — the suite has not been
|
|
47
|
+
run on a Mac
|
|
47
48
|
- Node.js >= 20
|
|
48
49
|
- Claude Code (`claude.exe`) — the GLM commands run on the Claude Code harness
|
|
49
50
|
- Codex (optional — Claude-only setups are fully supported)
|
|
@@ -292,7 +293,7 @@ except JSON-RPC frames.
|
|
|
292
293
|
glm-router init guided setup
|
|
293
294
|
glm-router doctor [--network] full runtime diagnosis
|
|
294
295
|
glm-router status quick offline overview
|
|
295
|
-
glm-router key set store ZAI_API_KEY
|
|
296
|
+
glm-router key set store ZAI_API_KEY in this platform's per-user store
|
|
296
297
|
glm-router key check key configured? from which source?
|
|
297
298
|
glm-router config show
|
|
298
299
|
glm-router config set models.main glm-5.3
|
|
@@ -343,14 +344,15 @@ after Orca starts is invisible to those terminals. Every GLM command therefore r
|
|
|
343
344
|
the key in this order:
|
|
344
345
|
|
|
345
346
|
1. `process.env.ZAI_API_KEY`
|
|
346
|
-
2. Windows User Environment (
|
|
347
|
+
2. This platform's per-user store — Windows User Environment (PowerShell), macOS login
|
|
348
|
+
keychain (`security`), or libsecret (`secret-tool`, when installed)
|
|
347
349
|
3. fail with an actionable error
|
|
348
350
|
|
|
349
351
|
The key is never cached to disk.
|
|
350
352
|
|
|
351
353
|
## Security model
|
|
352
354
|
|
|
353
|
-
- The key lives only in
|
|
355
|
+
- The key lives only in that per-user store; it is never written to
|
|
354
356
|
`config.json`, the repo, logs, or stack traces. Debug output redacts
|
|
355
357
|
`ZAI_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, and Authorization headers.
|
|
356
358
|
- Z.ai routing environment variables (`ANTHROPIC_AUTH_TOKEN`,
|
|
@@ -368,7 +370,9 @@ The key is never cached to disk.
|
|
|
368
370
|
| --- | --- |
|
|
369
371
|
| `ERROR [ZAI_KEY_MISSING]` | `glm-router key set`, then open a **new** terminal |
|
|
370
372
|
| `ERROR [CLAUDE_NOT_FOUND]` | Install Claude Code, or `glm-router config set claudePath C:\path\to\claude.exe` |
|
|
371
|
-
| Key works in a new terminal but not inside Orca | Expected — workers re-read the
|
|
373
|
+
| Key works in a new terminal but not inside Orca | Expected — workers re-read the per-user store automatically; run `glm-router doctor` to confirm |
|
|
374
|
+
| `glm-router key set` prints an `export` line instead of saving | This platform has no secret store (e.g. Linux without `secret-tool`). Add the line to your shell profile; `glm-router key check` verifies it |
|
|
375
|
+
| The worker creates files but never runs the tests | Its Bash allowlist is empty. `glm-router config show` → `worker.allowedBash`; the default list covers common test commands |
|
|
372
376
|
| `glm-*` not on PATH after install | Reopen the terminal; check `npm config get prefix` is on PATH |
|
|
373
377
|
| `ERROR [MANAGED_BLOCK_CORRUPT]` | Fix the marker pair in the named file manually, then re-run |
|
|
374
378
|
|
|
@@ -396,7 +400,7 @@ npm run dev # tsx src/cli.ts <args>
|
|
|
396
400
|
|
|
397
401
|
Integration tests spawn `tests/fixtures/fake-agent.mjs` (via `node.exe`) to verify
|
|
398
402
|
argument passing, environment injection, and exit-code propagation without spending
|
|
399
|
-
API quota. See the `GLM Coding Router — Technical Specification v0.1.md` for the full
|
|
403
|
+
API quota. See the `docs/GLM Coding Router — Technical Specification v0.1.md` for the full
|
|
400
404
|
v0.1 contract (exit codes, managed-block test matrix, acceptance criteria).
|
|
401
405
|
|
|
402
406
|
## Publishing
|
package/dist/bin/glm-worker.js
CHANGED
|
@@ -11,8 +11,20 @@ import { spawnAgent } from "../core/process.js";
|
|
|
11
11
|
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
12
12
|
/** Worker tool surface (spec §16). */
|
|
13
13
|
export const WORKER_TOOLS = "Read,Glob,Grep,Edit,Write,Bash";
|
|
14
|
+
/** Same surface minus Bash, used when no Bash command is allowed. */
|
|
15
|
+
export const WORKER_TOOLS_NO_BASH = "Read,Glob,Grep,Edit,Write";
|
|
16
|
+
/**
|
|
17
|
+
* Build the child arguments (spec §16, specs/worker-bash-permissions.md).
|
|
18
|
+
*
|
|
19
|
+
* `--permission-mode acceptEdits` auto-approves file edits but NOT shell
|
|
20
|
+
* commands, and headless `-p` has no prompt to answer — so without an explicit
|
|
21
|
+
* `--allowedTools` every Bash call comes back "This command requires
|
|
22
|
+
* approval". When the allowlist is empty we drop Bash from `--tools` entirely
|
|
23
|
+
* rather than advertising a tool the worker can never use.
|
|
24
|
+
*/
|
|
14
25
|
export function buildWorkerArgs(prompt, config) {
|
|
15
|
-
|
|
26
|
+
const allowedBash = config.worker.allowedBash;
|
|
27
|
+
const args = [
|
|
16
28
|
"-p",
|
|
17
29
|
prompt,
|
|
18
30
|
"--max-turns",
|
|
@@ -20,8 +32,25 @@ export function buildWorkerArgs(prompt, config) {
|
|
|
20
32
|
"--permission-mode",
|
|
21
33
|
"acceptEdits",
|
|
22
34
|
"--tools",
|
|
23
|
-
WORKER_TOOLS,
|
|
35
|
+
allowedBash.length > 0 ? WORKER_TOOLS : WORKER_TOOLS_NO_BASH,
|
|
24
36
|
];
|
|
37
|
+
if (allowedBash.length > 0) {
|
|
38
|
+
args.push("--allowedTools", ...allowedBash.map((pattern) => `Bash(${pattern})`));
|
|
39
|
+
}
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
42
|
+
/** Strip `--no-bash`, which empties the allowlist for one invocation. */
|
|
43
|
+
export function extractNoBashFlag(argv) {
|
|
44
|
+
const rest = [];
|
|
45
|
+
let noBash = false;
|
|
46
|
+
for (const arg of argv) {
|
|
47
|
+
if (arg === "--no-bash") {
|
|
48
|
+
noBash = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
rest.push(arg);
|
|
52
|
+
}
|
|
53
|
+
return { rest, noBash };
|
|
25
54
|
}
|
|
26
55
|
/**
|
|
27
56
|
* glm-worker (spec §15, §16): headless implementation worker.
|
|
@@ -29,9 +58,13 @@ export function buildWorkerArgs(prompt, config) {
|
|
|
29
58
|
* --dangerously-skip-permissions.
|
|
30
59
|
*/
|
|
31
60
|
export async function runWorker(argv) {
|
|
32
|
-
const { rest, profile } = extractProfileFlag(argv);
|
|
61
|
+
const { rest: withoutProfile, profile } = extractProfileFlag(argv);
|
|
62
|
+
const { rest, noBash } = extractNoBashFlag(withoutProfile);
|
|
33
63
|
const prompt = await resolvePrompt(rest);
|
|
34
|
-
const
|
|
64
|
+
const loaded = applyProfile(loadConfig(), profile);
|
|
65
|
+
const config = noBash
|
|
66
|
+
? { ...loaded, worker: { ...loaded.worker, allowedBash: [] } }
|
|
67
|
+
: loaded;
|
|
35
68
|
const resolved = resolveZaiApiKey();
|
|
36
69
|
if (!resolved) {
|
|
37
70
|
throw Errors.zaiKeyMissing();
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,8 @@ import os from "node:os";
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { defaultConfig, loadConfig } from "../core/config.js";
|
|
4
4
|
import { locateClaude, locateCodex, searchPathFor } from "../core/claude.js";
|
|
5
|
-
import {
|
|
5
|
+
import { platformName, platformSupport } from "../core/platform.js";
|
|
6
|
+
import { describeKeyStore, detectUserEnvStore } from "../core/user-env.js";
|
|
6
7
|
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
7
8
|
import { configPath } from "../core/paths.js";
|
|
8
9
|
import fs from "node:fs";
|
|
@@ -28,8 +29,13 @@ export function runDoctorChecks(options = {}) {
|
|
|
28
29
|
const results = [];
|
|
29
30
|
const home = options.home ?? os.homedir();
|
|
30
31
|
const env = options.env ?? process.env;
|
|
31
|
-
// --- System ---
|
|
32
|
-
|
|
32
|
+
// --- System (specs/cross-platform.md) ---
|
|
33
|
+
const support = platformSupport();
|
|
34
|
+
results.push(check("System", platformName(), support === "supported" ? "ok" : support === "experimental" ? "warn" : "fail", undefined, support === "experimental"
|
|
35
|
+
? "macOS support is experimental — the suite has not been run on a Mac."
|
|
36
|
+
: support === "unsupported"
|
|
37
|
+
? "Supported: Windows, Linux. macOS is experimental."
|
|
38
|
+
: undefined));
|
|
33
39
|
const nodeMajor = nodeVersionMajor();
|
|
34
40
|
results.push(check("System", `Node.js ${process.versions.node}`, nodeMajor >= 20 ? "ok" : "fail", undefined, nodeMajor >= 20 ? undefined : "glm-coding-router requires Node.js >= 20."));
|
|
35
41
|
results.push(check("System", "Git", gitFound() ? "ok" : "warn"));
|
|
@@ -60,9 +66,19 @@ export function runDoctorChecks(options = {}) {
|
|
|
60
66
|
const codexPath = locateCodex(config, env);
|
|
61
67
|
results.push(check("Agents", "Codex", codexPath ? "ok" : "warn", codexPath, codexPath ? undefined : "Optional — Claude-only setups are supported."));
|
|
62
68
|
// --- Z.ai key ---
|
|
69
|
+
const store = options.store ?? detectUserEnvStore();
|
|
63
70
|
const resolved = resolveZaiApiKey({ env, readUserEnv: options.readUserEnv });
|
|
64
|
-
|
|
71
|
+
const sourceLabel = resolved?.source === "process-env" ? "process environment" : describeKeyStore(store);
|
|
72
|
+
results.push(check("Z.ai", "ZAI_API_KEY", resolved ? "ok" : "fail", resolved ? `configured (${sourceLabel})` : "not found", resolved
|
|
73
|
+
? undefined
|
|
74
|
+
: store === "none"
|
|
75
|
+
? 'Set it: export ZAI_API_KEY="<your-key>" (see: glm-router key set)'
|
|
76
|
+
: "Run: glm-router key set"));
|
|
65
77
|
results.push(check("Z.ai", "Anthropic endpoint", "ok", config.provider.anthropicBaseUrl));
|
|
78
|
+
// specs/worker-bash-permissions.md — what the worker is allowed to execute.
|
|
79
|
+
results.push(check("Z.ai", "Worker shell access", "ok", config.worker.allowedBash.length > 0
|
|
80
|
+
? `${config.worker.allowedBash.length} allowed Bash patterns`
|
|
81
|
+
: "disabled (Bash not offered to the worker)"));
|
|
66
82
|
// --- Commands (PATH shims; a dev checkout warns instead of failing) ---
|
|
67
83
|
for (const command of ["glm-chat", "glm-worker", "glm-review"]) {
|
|
68
84
|
const found = searchPathFor(command);
|
|
@@ -86,7 +102,7 @@ export function runDoctorChecks(options = {}) {
|
|
|
86
102
|
results.push(check("Environment", "Process environment", "ok", "ZAI_API_KEY visible in current process"));
|
|
87
103
|
}
|
|
88
104
|
else if (resolved) {
|
|
89
|
-
results.push(check("Environment", "Process environment", "warn", "Current process does not contain ZAI_API_KEY",
|
|
105
|
+
results.push(check("Environment", "Process environment", "warn", "Current process does not contain ZAI_API_KEY", `This is safe. GLM workers reload the key automatically from ${describeKeyStore(store)}.`));
|
|
90
106
|
}
|
|
91
107
|
return { results, config, keySource: resolved?.source };
|
|
92
108
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -6,7 +6,8 @@ import { configPath } from "../core/paths.js";
|
|
|
6
6
|
import { ExitCode } from "../core/errors.js";
|
|
7
7
|
import { runDoctorChecks } from "./doctor.js";
|
|
8
8
|
import { setWindowsUserEnv, ZAI_API_KEY_ENV } from "../core/zai-key.js";
|
|
9
|
-
import {
|
|
9
|
+
import { detectUserEnvStore } from "../core/user-env.js";
|
|
10
|
+
import { printShellKeyGuidance } from "./key.js";
|
|
10
11
|
import { CodexSkillInstaller, glmDelegationSkill } from "../integrations/skill.js";
|
|
11
12
|
import { version } from "../core/version.js";
|
|
12
13
|
function renderEnvironment(results) {
|
|
@@ -84,8 +85,9 @@ export async function initCommand(options, deps = {}) {
|
|
|
84
85
|
process.stdout.write(`✓ ${ZAI_API_KEY_ENV} already configured (${report.keySource})\n`);
|
|
85
86
|
}
|
|
86
87
|
else if (choices.configureKey) {
|
|
87
|
-
if (
|
|
88
|
-
|
|
88
|
+
if ((deps.store ?? detectUserEnvStore()) === "none") {
|
|
89
|
+
// No persistent store here; tell the user the one line that works.
|
|
90
|
+
printShellKeyGuidance({ env: deps.env });
|
|
89
91
|
}
|
|
90
92
|
else {
|
|
91
93
|
const keyResponse = await prompt({
|
package/dist/commands/key.js
CHANGED
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
import prompts from "prompts";
|
|
2
|
-
import {
|
|
3
|
-
import { Errors } from "../core/errors.js";
|
|
2
|
+
import { Errors, formatGlmError } from "../core/errors.js";
|
|
4
3
|
import { logger } from "../core/logging.js";
|
|
4
|
+
import { describeKeyStore, describeShellExport, detectUserEnvStore, } from "../core/user-env.js";
|
|
5
5
|
import { ZAI_API_KEY_ENV, deleteWindowsUserEnv, resolveZaiApiKey, setWindowsUserEnv, } from "../core/zai-key.js";
|
|
6
6
|
import { emitJson } from "./context.js";
|
|
7
|
+
/**
|
|
8
|
+
* Print the shell-profile guidance used when the platform has no persistent
|
|
9
|
+
* secret store (the common Linux case: `secret-tool` is not installed by
|
|
10
|
+
* default). specs/cross-platform.md.
|
|
11
|
+
*/
|
|
12
|
+
export function printShellKeyGuidance(deps = {}) {
|
|
13
|
+
const { line, profile } = describeShellExport(ZAI_API_KEY_ENV, {
|
|
14
|
+
env: deps.env,
|
|
15
|
+
home: deps.home,
|
|
16
|
+
});
|
|
17
|
+
process.stdout.write([
|
|
18
|
+
`This platform has ${describeKeyStore("none")} that ${ZAI_API_KEY_ENV} can be saved to,`,
|
|
19
|
+
"so set it yourself — it takes one line:",
|
|
20
|
+
"",
|
|
21
|
+
` ${line}`,
|
|
22
|
+
"",
|
|
23
|
+
`Add that to ${profile} so new shells inherit it, then run:`,
|
|
24
|
+
"",
|
|
25
|
+
" glm-router key check",
|
|
26
|
+
"",
|
|
27
|
+
"The current shell keeps its old environment until you re-source that file",
|
|
28
|
+
"or open a new terminal.",
|
|
29
|
+
"",
|
|
30
|
+
].join("\n"));
|
|
31
|
+
}
|
|
7
32
|
/** glm-router key set (spec §11): prompt, save to Windows User Environment. */
|
|
8
33
|
export async function keySetCommand(_options, deps = {}) {
|
|
9
|
-
|
|
34
|
+
const store = deps.store ?? detectUserEnvStore();
|
|
35
|
+
if (store === "none") {
|
|
36
|
+
printShellKeyGuidance(deps);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
10
39
|
const prompt = deps.prompt ?? prompts;
|
|
11
40
|
const setEnv = deps.setEnv ?? setWindowsUserEnv;
|
|
12
41
|
const response = await prompt({
|
|
@@ -24,10 +53,10 @@ export async function keySetCommand(_options, deps = {}) {
|
|
|
24
53
|
setEnv(ZAI_API_KEY_ENV, key);
|
|
25
54
|
}
|
|
26
55
|
catch (error) {
|
|
27
|
-
logger.error(`Failed to write
|
|
56
|
+
logger.error(`Failed to write ${describeKeyStore(store)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
57
|
return 1;
|
|
29
58
|
}
|
|
30
|
-
process.stdout.write(`\n✓ Saved to
|
|
59
|
+
process.stdout.write(`\n✓ Saved to ${describeKeyStore(store)}:\n ${ZAI_API_KEY_ENV}\n`);
|
|
31
60
|
process.stdout.write("\nOpen a NEW terminal (or restart your Orca terminal) so the current process picks it up.\n");
|
|
32
61
|
return 0;
|
|
33
62
|
}
|
|
@@ -42,18 +71,26 @@ export function keyCheckCommand(options, deps = {}) {
|
|
|
42
71
|
return resolved ? 0 : 10;
|
|
43
72
|
}
|
|
44
73
|
if (!resolved) {
|
|
45
|
-
// Formatted per spec §36; exit 10 (spec §35).
|
|
74
|
+
// Formatted per spec §36; exit 10 (spec §35). The hint is platform-aware,
|
|
75
|
+
// so it never points at a command that cannot finish the job here.
|
|
46
76
|
const error = Errors.zaiKeyMissing();
|
|
47
|
-
process.stderr.write(
|
|
77
|
+
process.stderr.write(formatGlmError(error) + "\n");
|
|
48
78
|
return error.exitCode;
|
|
49
79
|
}
|
|
50
|
-
const sourceLabel = resolved.source === "process-env"
|
|
80
|
+
const sourceLabel = resolved.source === "process-env"
|
|
81
|
+
? "process environment"
|
|
82
|
+
: describeKeyStore(deps.store ?? detectUserEnvStore());
|
|
51
83
|
process.stdout.write(`${ZAI_API_KEY_ENV}: configured\nSource: ${sourceLabel}\n`);
|
|
52
84
|
return 0;
|
|
53
85
|
}
|
|
54
86
|
/** Used by uninstall; keeps the key by default (spec §44). */
|
|
55
87
|
export async function keyRemoveCommand(deps = {}) {
|
|
56
|
-
|
|
88
|
+
const store = deps.store ?? detectUserEnvStore();
|
|
89
|
+
if (store === "none") {
|
|
90
|
+
// Nothing this tool owns; the key lives in a shell profile the user wrote.
|
|
91
|
+
process.stdout.write(`${ZAI_API_KEY_ENV} is not stored by glm-router on this platform — remove the export line from your shell profile.\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
57
94
|
const deleteEnv = deps.deleteEnv ?? deleteWindowsUserEnv;
|
|
58
95
|
deleteEnv(ZAI_API_KEY_ENV);
|
|
59
96
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
|
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");
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { configDir } from "../core/paths.js";
|
|
5
5
|
import { ExitCode } from "../core/errors.js";
|
|
6
|
+
import { describeKeyStore, detectUserEnvStore } from "../core/user-env.js";
|
|
6
7
|
import { CodexSkillInstaller, glmDelegationSkill } from "../integrations/skill.js";
|
|
7
8
|
import { keyRemoveCommand } from "./key.js";
|
|
8
9
|
import { removeClaudeIntegration, removeCodexIntegration } from "../integrations/index.js";
|
|
@@ -85,7 +86,7 @@ export async function uninstallCommand(options, deps = {}) {
|
|
|
85
86
|
}
|
|
86
87
|
if (choices.removeKey) {
|
|
87
88
|
await keyRemoveCommand({ deleteEnv: deps.deleteEnv });
|
|
88
|
-
process.stdout.write(
|
|
89
|
+
process.stdout.write(`✓ ZAI_API_KEY removed from ${describeKeyStore(detectUserEnvStore())}\n`);
|
|
89
90
|
}
|
|
90
91
|
else {
|
|
91
92
|
process.stdout.write("✓ ZAI_API_KEY kept\n");
|
package/dist/commands/usage.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/config.js
CHANGED
|
@@ -6,6 +6,34 @@ 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
|
+
/**
|
|
10
|
+
* Bash commands the worker may run without an approval prompt
|
|
11
|
+
* (specs/worker-bash-permissions.md). Headless `-p` has no prompt to answer,
|
|
12
|
+
* so without this every Bash call is denied and the worker cannot validate its
|
|
13
|
+
* own work. Validation commands only: no git writes, no rm, no network, no
|
|
14
|
+
* package installs.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_ALLOWED_BASH = [
|
|
17
|
+
"npm test",
|
|
18
|
+
"npm run *",
|
|
19
|
+
"npx vitest *",
|
|
20
|
+
"go test *",
|
|
21
|
+
"pytest*",
|
|
22
|
+
"python3 *",
|
|
23
|
+
"cargo test*",
|
|
24
|
+
"git status",
|
|
25
|
+
"git diff*",
|
|
26
|
+
];
|
|
27
|
+
/** Shell metacharacters defeat pattern matching, so they may not appear. */
|
|
28
|
+
const BASH_PATTERN_FORBIDDEN = /[&;|`$><\n]/;
|
|
29
|
+
const AllowedBashSchema = z
|
|
30
|
+
.array(z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1)
|
|
33
|
+
.refine((value) => !BASH_PATTERN_FORBIDDEN.test(value), {
|
|
34
|
+
message: "must not contain shell metacharacters (& ; | ` $ > <)",
|
|
35
|
+
}))
|
|
36
|
+
.default([...DEFAULT_ALLOWED_BASH]);
|
|
9
37
|
/** Named model/maxTurns overlay selected via --profile (specs/glm-fast-profiles.md). */
|
|
10
38
|
export const ProfileSchema = z.object({
|
|
11
39
|
main: z.string().min(1).optional(),
|
|
@@ -25,7 +53,8 @@ export const ConfigSchema = z.object({
|
|
|
25
53
|
}),
|
|
26
54
|
worker: z.object({
|
|
27
55
|
maxTurns: z.number().int().positive(),
|
|
28
|
-
|
|
56
|
+
allowedBash: AllowedBashSchema,
|
|
57
|
+
}).default({ maxTurns: 20, allowedBash: [...DEFAULT_ALLOWED_BASH] }),
|
|
29
58
|
review: z.object({
|
|
30
59
|
maxTurns: z.number().int().positive(),
|
|
31
60
|
}).default({ maxTurns: 15 }),
|
|
@@ -51,7 +80,7 @@ export function defaultConfig() {
|
|
|
51
80
|
main: DEFAULT_MAIN_MODEL,
|
|
52
81
|
fast: DEFAULT_FAST_MODEL,
|
|
53
82
|
},
|
|
54
|
-
worker: { maxTurns: 20 },
|
|
83
|
+
worker: { maxTurns: 20, allowedBash: [...DEFAULT_ALLOWED_BASH] },
|
|
55
84
|
review: { maxTurns: 15 },
|
|
56
85
|
integrations: {
|
|
57
86
|
claude: true,
|
package/dist/core/errors.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This module deliberately does NOT import platform.ts: platform.ts imports
|
|
3
|
+
* Errors, so the dependency would be circular. The couple of platform checks
|
|
4
|
+
* below read process.platform directly (specs/cross-platform.md).
|
|
5
|
+
*/
|
|
6
|
+
const onWindows = () => process.platform === "win32";
|
|
7
|
+
/** Config dir as the user's own platform spells it. */
|
|
8
|
+
function configPathHint() {
|
|
9
|
+
return onWindows()
|
|
10
|
+
? "%USERPROFILE%\\.glm-coding-router\\config.json"
|
|
11
|
+
: "~/.glm-coding-router/config.json";
|
|
12
|
+
}
|
|
1
13
|
/** Standard exit codes (spec §35). */
|
|
2
14
|
export const ExitCode = {
|
|
3
15
|
Success: 0,
|
|
@@ -29,17 +41,26 @@ export const Errors = {
|
|
|
29
41
|
zaiKeyMissing: () => new GlmRouterError({
|
|
30
42
|
name: "ZAI_KEY_MISSING",
|
|
31
43
|
message: "ZAI_API_KEY was not found.",
|
|
32
|
-
|
|
44
|
+
// On platforms without a persistent store `key set` can only print
|
|
45
|
+
// guidance, so the export line is shown here too — otherwise this hint
|
|
46
|
+
// points at a command that cannot finish the job.
|
|
47
|
+
hint: onWindows()
|
|
48
|
+
? ["Run:", "", " glm-router key set"]
|
|
49
|
+
: [
|
|
50
|
+
"Run:",
|
|
51
|
+
"",
|
|
52
|
+
" glm-router key set",
|
|
53
|
+
"",
|
|
54
|
+
"or set it in this shell and your shell profile:",
|
|
55
|
+
"",
|
|
56
|
+
' export ZAI_API_KEY="<your-key>"',
|
|
57
|
+
],
|
|
33
58
|
exitCode: ExitCode.ZaiKeyMissing,
|
|
34
59
|
}),
|
|
35
60
|
configInvalid: (detail) => new GlmRouterError({
|
|
36
61
|
name: "CONFIG_INVALID",
|
|
37
62
|
message: `Configuration is invalid: ${detail}`,
|
|
38
|
-
hint: [
|
|
39
|
-
"Fix or remove the config file:",
|
|
40
|
-
"",
|
|
41
|
-
" %USERPROFILE%\\.glm-coding-router\\config.json",
|
|
42
|
-
],
|
|
63
|
+
hint: ["Fix or remove the config file:", "", ` ${configPathHint()}`],
|
|
43
64
|
exitCode: ExitCode.ConfigInvalid,
|
|
44
65
|
}),
|
|
45
66
|
claudeNotFound: (detail) => new GlmRouterError({
|
|
@@ -49,11 +70,13 @@ export const Errors = {
|
|
|
49
70
|
hint: [
|
|
50
71
|
"Expected:",
|
|
51
72
|
"",
|
|
52
|
-
" claude.exe",
|
|
73
|
+
onWindows() ? " claude.exe" : " claude",
|
|
53
74
|
"",
|
|
54
75
|
"Install Claude Code or set an override:",
|
|
55
76
|
"",
|
|
56
|
-
|
|
77
|
+
onWindows()
|
|
78
|
+
? " glm-router config set claudePath C:\\path\\to\\claude.exe"
|
|
79
|
+
: " glm-router config set claudePath /path/to/claude",
|
|
57
80
|
],
|
|
58
81
|
exitCode: ExitCode.ClaudeNotFound,
|
|
59
82
|
}),
|
|
@@ -81,8 +104,8 @@ export const Errors = {
|
|
|
81
104
|
}),
|
|
82
105
|
unsupportedPlatform: (platform) => new GlmRouterError({
|
|
83
106
|
name: "UNSUPPORTED_PLATFORM",
|
|
84
|
-
message: `This
|
|
85
|
-
hint: ["Linux
|
|
107
|
+
message: `This platform is not supported (detected: ${platform}).`,
|
|
108
|
+
hint: ["Supported: Windows, Linux. macOS is experimental."],
|
|
86
109
|
exitCode: ExitCode.UnsupportedPlatform,
|
|
87
110
|
}),
|
|
88
111
|
promptRequired: (command = "glm-worker") => new GlmRouterError({
|
package/dist/core/platform.js
CHANGED
|
@@ -3,18 +3,47 @@ import { Errors } from "./errors.js";
|
|
|
3
3
|
export function isWindows() {
|
|
4
4
|
return process.platform === "win32";
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Support level per platform:
|
|
8
|
+
* win32 — verified through the registry-verification ritual
|
|
9
|
+
* linux — verified 2026-09-20 on Ubuntu 24.04
|
|
10
|
+
* darwin — designed but never executed; no Mac has run the suite
|
|
11
|
+
*/
|
|
12
|
+
export function platformSupport(platform = process.platform) {
|
|
13
|
+
if (platform === "win32" || platform === "linux")
|
|
14
|
+
return "supported";
|
|
15
|
+
if (platform === "darwin")
|
|
16
|
+
return "experimental";
|
|
17
|
+
return "unsupported";
|
|
18
|
+
}
|
|
19
|
+
export function isSupportedPlatform(platform = process.platform) {
|
|
20
|
+
return platformSupport(platform) !== "unsupported";
|
|
21
|
+
}
|
|
22
|
+
/** Human-readable platform name for doctor/status. */
|
|
23
|
+
export function platformName(platform = process.platform, release = os.release()) {
|
|
24
|
+
if (platform === "win32") {
|
|
25
|
+
const build = Number.parseInt(release.split(".")[2] ?? "0", 10);
|
|
26
|
+
return build >= 22000 ? "Windows 11" : "Windows 10";
|
|
10
27
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
if (platform === "darwin")
|
|
29
|
+
return `macOS (darwin ${release.split(".")[0] ?? "?"})`;
|
|
30
|
+
if (platform === "linux")
|
|
31
|
+
return `Linux ${release.split("-")[0] ?? release}`;
|
|
32
|
+
return `Platform ${platform}`;
|
|
14
33
|
}
|
|
15
|
-
/**
|
|
34
|
+
/** @deprecated use platformName(); kept so existing callers keep compiling. */
|
|
35
|
+
export function windowsVersionName() {
|
|
36
|
+
return platformName();
|
|
37
|
+
}
|
|
38
|
+
/** Guard for the few code paths that are genuinely Windows-only. */
|
|
16
39
|
export function assertWindows() {
|
|
17
40
|
if (!isWindows()) {
|
|
18
41
|
throw Errors.unsupportedPlatform(process.platform);
|
|
19
42
|
}
|
|
20
43
|
}
|
|
44
|
+
/** Guard for commands that need a platform this package supports at all. */
|
|
45
|
+
export function assertSupportedPlatform() {
|
|
46
|
+
if (!isSupportedPlatform()) {
|
|
47
|
+
throw Errors.unsupportedPlatform(process.platform);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/core/profile.js
CHANGED
|
@@ -51,7 +51,12 @@ export function applyProfile(config, name) {
|
|
|
51
51
|
main: profile.main ?? config.models.main,
|
|
52
52
|
fast: profile.fast ?? config.models.fast,
|
|
53
53
|
},
|
|
54
|
-
worker: {
|
|
54
|
+
worker: {
|
|
55
|
+
maxTurns: profile.workerMaxTurns ?? config.worker.maxTurns,
|
|
56
|
+
// Profiles tune models and turn budgets, never the Bash allowlist —
|
|
57
|
+
// that is a security setting, not a performance knob.
|
|
58
|
+
allowedBash: config.worker.allowedBash,
|
|
59
|
+
},
|
|
55
60
|
review: { maxTurns: profile.reviewMaxTurns ?? config.review.maxTurns },
|
|
56
61
|
};
|
|
57
62
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Keychain/libsecret service name; the variable name is the account. */
|
|
5
|
+
export const KEY_STORE_SERVICE = "glm-coding-router";
|
|
6
|
+
/** Only well-formed variable names may reach a child process (spec §38). */
|
|
7
|
+
function assertEnvVarName(name) {
|
|
8
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
9
|
+
throw new Error(`Invalid environment variable name: ${name}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function defaultRun(file, args, options) {
|
|
13
|
+
const result = execFileSync(file, [...args], {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
input: options.input,
|
|
17
|
+
stdio: options.input === undefined
|
|
18
|
+
? ["ignore", options.capture ? "pipe" : "ignore", "ignore"]
|
|
19
|
+
: ["pipe", options.capture ? "pipe" : "ignore", "ignore"],
|
|
20
|
+
});
|
|
21
|
+
return typeof result === "string" ? result : "";
|
|
22
|
+
}
|
|
23
|
+
/** Is `name` an executable file somewhere on PATH? */
|
|
24
|
+
function defaultHasCommand(name, env = process.env) {
|
|
25
|
+
const dirs = (env.PATH ?? "").split(path.delimiter).filter((d) => d.length > 0);
|
|
26
|
+
return dirs.some((dir) => {
|
|
27
|
+
try {
|
|
28
|
+
return fs.statSync(path.join(dir, name)).isFile();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Which store this machine has. `libsecret` requires `secret-tool` on PATH —
|
|
37
|
+
* it is NOT installed by default on Ubuntu, so "none" is a normal Linux
|
|
38
|
+
* outcome, not an error (specs/cross-platform.md).
|
|
39
|
+
*/
|
|
40
|
+
export function detectUserEnvStore(deps = {}) {
|
|
41
|
+
const platform = deps.platform ?? process.platform;
|
|
42
|
+
const env = deps.env ?? process.env;
|
|
43
|
+
const hasCommand = deps.hasCommand ?? ((name) => defaultHasCommand(name, env));
|
|
44
|
+
if (platform === "win32")
|
|
45
|
+
return "windows-user-env";
|
|
46
|
+
if (platform === "darwin")
|
|
47
|
+
return hasCommand("security") ? "macos-keychain" : "none";
|
|
48
|
+
if (platform === "linux")
|
|
49
|
+
return hasCommand("secret-tool") ? "libsecret" : "none";
|
|
50
|
+
return "none";
|
|
51
|
+
}
|
|
52
|
+
/** Human wording for the store, used by key/doctor/uninstall messages. */
|
|
53
|
+
export function describeKeyStore(store) {
|
|
54
|
+
switch (store) {
|
|
55
|
+
case "windows-user-env":
|
|
56
|
+
return "Windows User Environment";
|
|
57
|
+
case "macos-keychain":
|
|
58
|
+
return "macOS login keychain";
|
|
59
|
+
case "libsecret":
|
|
60
|
+
return "the system keyring (libsecret)";
|
|
61
|
+
case "none":
|
|
62
|
+
return "no persistent store";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read a variable from the per-user store. Returns undefined on any failure —
|
|
67
|
+
* callers fall back or raise their own error.
|
|
68
|
+
*/
|
|
69
|
+
export function readUserEnv(name, deps = {}) {
|
|
70
|
+
assertEnvVarName(name);
|
|
71
|
+
const store = detectUserEnvStore(deps);
|
|
72
|
+
const run = deps.run ?? defaultRun;
|
|
73
|
+
try {
|
|
74
|
+
let value;
|
|
75
|
+
switch (store) {
|
|
76
|
+
case "windows-user-env":
|
|
77
|
+
value = run("powershell.exe", [
|
|
78
|
+
"-NoProfile",
|
|
79
|
+
"-NonInteractive",
|
|
80
|
+
"-Command",
|
|
81
|
+
`[Environment]::GetEnvironmentVariable('${name}','User')`,
|
|
82
|
+
], { capture: true });
|
|
83
|
+
break;
|
|
84
|
+
case "macos-keychain":
|
|
85
|
+
value = run("security", ["find-generic-password", "-s", KEY_STORE_SERVICE, "-a", name, "-w"], { capture: true });
|
|
86
|
+
break;
|
|
87
|
+
case "libsecret":
|
|
88
|
+
value = run("secret-tool", ["lookup", "service", KEY_STORE_SERVICE, "account", name], { capture: true });
|
|
89
|
+
break;
|
|
90
|
+
case "none":
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
return trimmed || undefined;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Write a variable to the per-user store. Throws when there is no store —
|
|
102
|
+
* callers print platform-appropriate guidance instead.
|
|
103
|
+
*
|
|
104
|
+
* The value never goes through a shell. On Windows and Linux it is passed via
|
|
105
|
+
* a child environment variable / stdin respectively, so it never appears in
|
|
106
|
+
* any process's argv. The macOS backend has no stdin form, so the value is in
|
|
107
|
+
* `security`'s argv for the duration of that call — a known limitation of the
|
|
108
|
+
* experimental darwin support (specs/cross-platform.md).
|
|
109
|
+
*/
|
|
110
|
+
export function writeUserEnv(name, value, deps = {}) {
|
|
111
|
+
assertEnvVarName(name);
|
|
112
|
+
const store = detectUserEnvStore(deps);
|
|
113
|
+
const run = deps.run ?? defaultRun;
|
|
114
|
+
switch (store) {
|
|
115
|
+
case "windows-user-env":
|
|
116
|
+
execFileSync("powershell.exe", [
|
|
117
|
+
"-NoProfile",
|
|
118
|
+
"-NonInteractive",
|
|
119
|
+
"-Command",
|
|
120
|
+
`[Environment]::SetEnvironmentVariable('${name}', $env:GLM_ROUTER_VALUE, 'User')`,
|
|
121
|
+
], {
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
124
|
+
env: { ...process.env, GLM_ROUTER_VALUE: value },
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
case "macos-keychain":
|
|
128
|
+
run("security", ["add-generic-password", "-U", "-s", KEY_STORE_SERVICE, "-a", name, "-w", value], { capture: false });
|
|
129
|
+
return;
|
|
130
|
+
case "libsecret":
|
|
131
|
+
run("secret-tool", ["store", "--label", `${KEY_STORE_SERVICE} ${name}`, "service", KEY_STORE_SERVICE, "account", name], { input: value, capture: false });
|
|
132
|
+
return;
|
|
133
|
+
case "none":
|
|
134
|
+
throw new Error("no persistent secret store is available on this platform");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Remove the variable from the per-user store. No store → nothing to do. */
|
|
138
|
+
export function deleteUserEnv(name, deps = {}) {
|
|
139
|
+
assertEnvVarName(name);
|
|
140
|
+
const store = detectUserEnvStore(deps);
|
|
141
|
+
const run = deps.run ?? defaultRun;
|
|
142
|
+
switch (store) {
|
|
143
|
+
case "windows-user-env":
|
|
144
|
+
run("powershell.exe", [
|
|
145
|
+
"-NoProfile",
|
|
146
|
+
"-NonInteractive",
|
|
147
|
+
"-Command",
|
|
148
|
+
`[Environment]::SetEnvironmentVariable('${name}', $null, 'User')`,
|
|
149
|
+
], { capture: false });
|
|
150
|
+
return;
|
|
151
|
+
case "macos-keychain":
|
|
152
|
+
run("security", ["delete-generic-password", "-s", KEY_STORE_SERVICE, "-a", name], { capture: false });
|
|
153
|
+
return;
|
|
154
|
+
case "libsecret":
|
|
155
|
+
run("secret-tool", ["clear", "service", KEY_STORE_SERVICE, "account", name], {
|
|
156
|
+
capture: false,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
case "none":
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The shell line a user must add when there is no store, plus the profile file
|
|
165
|
+
* to put it in. Chosen from $SHELL, preferring a file that already exists.
|
|
166
|
+
*/
|
|
167
|
+
export function describeShellExport(name, deps = {}) {
|
|
168
|
+
const env = deps.env ?? process.env;
|
|
169
|
+
const home = deps.home ?? env.HOME ?? "~";
|
|
170
|
+
const exists = deps.exists ?? ((file) => fs.existsSync(file));
|
|
171
|
+
const shell = path.posix.basename(env.SHELL ?? "bash");
|
|
172
|
+
const candidates = shell === "zsh"
|
|
173
|
+
? [".zshrc", ".zprofile", ".profile"]
|
|
174
|
+
: shell === "fish"
|
|
175
|
+
? [".config/fish/config.fish"]
|
|
176
|
+
: [".bashrc", ".bash_profile", ".profile"];
|
|
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]);
|
|
181
|
+
const line = shell === "fish" ? `set -gx ${name} <your-key>` : `export ${name}="<your-key>"`;
|
|
182
|
+
return { line, profile };
|
|
183
|
+
}
|
package/dist/core/zai-key.js
CHANGED
|
@@ -1,69 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deleteUserEnv, detectUserEnvStore, readUserEnv as readUserEnvStore, writeUserEnv, } from "./user-env.js";
|
|
2
2
|
export const ZAI_API_KEY_ENV = "ZAI_API_KEY";
|
|
3
|
-
/** Only well-formed variable names may reach powershell.exe (spec §38). */
|
|
4
|
-
function assertEnvVarName(name) {
|
|
5
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
6
|
-
throw new Error(`Invalid environment variable name: ${name}`);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
3
|
/**
|
|
10
|
-
* Read
|
|
11
|
-
*
|
|
4
|
+
* Read the variable from this platform's per-user store (spec §10).
|
|
5
|
+
* The Windows implementation is unchanged; it now lives in user-env.ts.
|
|
12
6
|
*/
|
|
13
|
-
export function readWindowsUserEnv(name) {
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const result = execFileSync("powershell.exe", [
|
|
17
|
-
"-NoProfile",
|
|
18
|
-
"-NonInteractive",
|
|
19
|
-
"-Command",
|
|
20
|
-
`[Environment]::GetEnvironmentVariable('${name}','User')`,
|
|
21
|
-
], {
|
|
22
|
-
encoding: "utf8",
|
|
23
|
-
windowsHide: true,
|
|
24
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
25
|
-
});
|
|
26
|
-
const value = result.trim();
|
|
27
|
-
return value || undefined;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
7
|
+
export function readWindowsUserEnv(name, deps = {}) {
|
|
8
|
+
return readUserEnvStore(name, deps);
|
|
32
9
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* PowerShell string escaping (spec §38: no unescaped user data in commands).
|
|
37
|
-
*/
|
|
38
|
-
export function setWindowsUserEnv(name, value) {
|
|
39
|
-
assertEnvVarName(name);
|
|
40
|
-
execFileSync("powershell.exe", [
|
|
41
|
-
"-NoProfile",
|
|
42
|
-
"-NonInteractive",
|
|
43
|
-
"-Command",
|
|
44
|
-
`[Environment]::SetEnvironmentVariable('${name}', $env:GLM_ROUTER_VALUE, 'User')`,
|
|
45
|
-
], {
|
|
46
|
-
windowsHide: true,
|
|
47
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
48
|
-
env: { ...process.env, GLM_ROUTER_VALUE: value },
|
|
49
|
-
});
|
|
10
|
+
/** Write the variable to this platform's per-user store (spec §11). */
|
|
11
|
+
export function setWindowsUserEnv(name, value, deps = {}) {
|
|
12
|
+
writeUserEnv(name, value, deps);
|
|
50
13
|
}
|
|
51
|
-
export function deleteWindowsUserEnv(name) {
|
|
52
|
-
|
|
53
|
-
execFileSync("powershell.exe", [
|
|
54
|
-
"-NoProfile",
|
|
55
|
-
"-NonInteractive",
|
|
56
|
-
"-Command",
|
|
57
|
-
`[Environment]::SetEnvironmentVariable('${name}', $null, 'User')`,
|
|
58
|
-
], {
|
|
59
|
-
windowsHide: true,
|
|
60
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
61
|
-
});
|
|
14
|
+
export function deleteWindowsUserEnv(name, deps = {}) {
|
|
15
|
+
deleteUserEnv(name, deps);
|
|
62
16
|
}
|
|
17
|
+
/** Re-exported so callers do not need two imports. */
|
|
18
|
+
export { detectUserEnvStore };
|
|
63
19
|
/**
|
|
64
20
|
* Resolve the Z.ai key with the mandatory fallback order (spec §10):
|
|
65
21
|
* 1. process.env.ZAI_API_KEY
|
|
66
|
-
* 2.
|
|
22
|
+
* 2. this platform's per-user store
|
|
67
23
|
* 3. fail (undefined)
|
|
68
24
|
*
|
|
69
25
|
* The fallback exists because Orca terminals snapshot a stale environment and
|
|
@@ -71,14 +27,14 @@ export function deleteWindowsUserEnv(name) {
|
|
|
71
27
|
*/
|
|
72
28
|
export function resolveZaiApiKey(options = {}) {
|
|
73
29
|
const env = options.env ?? process.env;
|
|
74
|
-
const readUserEnv = options.readUserEnv ??
|
|
30
|
+
const readUserEnv = options.readUserEnv ?? ((name) => readUserEnvStore(name));
|
|
75
31
|
const fromProcess = env[ZAI_API_KEY_ENV];
|
|
76
32
|
if (fromProcess && fromProcess.trim()) {
|
|
77
33
|
return { key: fromProcess.trim(), source: "process-env" };
|
|
78
34
|
}
|
|
79
35
|
const fromUserEnv = readUserEnv(ZAI_API_KEY_ENV);
|
|
80
36
|
if (fromUserEnv && fromUserEnv.trim()) {
|
|
81
|
-
return { key: fromUserEnv.trim(), source: "
|
|
37
|
+
return { key: fromUserEnv.trim(), source: "user-store" };
|
|
82
38
|
}
|
|
83
39
|
return undefined;
|
|
84
40
|
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -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(` ${
|
|
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.
|
|
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.1",
|
|
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
|
+
}
|