glm-coding-router 0.5.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +419 -381
- package/dist/bin/glm-mcp.js +25 -0
- package/dist/bin/glm-worker.js +37 -4
- package/dist/cli.js +13 -0
- package/dist/commands/doctor.js +31 -14
- package/dist/commands/init.js +5 -3
- package/dist/commands/key.js +46 -9
- package/dist/commands/mcp.js +72 -0
- package/dist/commands/skill.js +48 -36
- package/dist/commands/status.js +21 -8
- package/dist/commands/uninstall.js +2 -1
- package/dist/commands/usage.js +1 -1
- package/dist/core/config.js +31 -2
- package/dist/core/errors.js +33 -10
- package/dist/core/platform.js +37 -8
- package/dist/core/profile.js +6 -1
- package/dist/core/user-env.js +181 -0
- package/dist/core/zai-key.js +15 -59
- package/dist/integrations/skill.js +31 -12
- package/dist/mcp/server.js +232 -0
- package/package.json +3 -2
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,181 @@
|
|
|
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.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
|
+
const found = candidates.find((rel) => exists(path.join(home, rel)));
|
|
178
|
+
const profile = path.join(home, found ?? candidates[0]);
|
|
179
|
+
const line = shell === "fish" ? `set -gx ${name} <your-key>` : `export ${name}="<your-key>"`;
|
|
180
|
+
return { line, profile };
|
|
181
|
+
}
|
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
|
}
|
|
@@ -3,33 +3,52 @@ import path from "node:path";
|
|
|
3
3
|
import { atomicWriteFile } from "../project/atomic-write.js";
|
|
4
4
|
import { GLM_DELEGATION_SKILL_MD, GLM_DELEGATION_SKILL_NAME, } from "../templates/glm-delegation-skill.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Detection
|
|
8
|
-
* supported
|
|
6
|
+
* Shared SKILL.md-folder mechanics; subclasses only pick the agent home dir
|
|
7
|
+
* (specs/v1-architecture.md). Detection stays conservative: a missing home
|
|
8
|
+
* means no supported installation to enhance — return null, callers warn+skip.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
this.
|
|
10
|
+
class HomeDirSkillInstaller {
|
|
11
|
+
agentHome;
|
|
12
|
+
constructor(agentHome) {
|
|
13
|
+
this.agentHome = agentHome;
|
|
14
14
|
}
|
|
15
15
|
detect() {
|
|
16
|
-
if (!fs.existsSync(this.
|
|
16
|
+
if (!fs.existsSync(this.agentHome)) {
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
-
return { skillsDir: path.join(this.
|
|
19
|
+
return { skillsDir: path.join(this.agentHome, "skills") };
|
|
20
20
|
}
|
|
21
21
|
install(skill) {
|
|
22
|
-
const skillDir = path.join(this.
|
|
22
|
+
const skillDir = path.join(this.agentHome, "skills", skill.name);
|
|
23
23
|
atomicWriteFile(path.join(skillDir, "SKILL.md"), skill.content);
|
|
24
24
|
}
|
|
25
25
|
remove(name) {
|
|
26
|
-
const skillDir = path.join(this.
|
|
26
|
+
const skillDir = path.join(this.agentHome, "skills", name);
|
|
27
27
|
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
28
28
|
}
|
|
29
29
|
isInstalled(name) {
|
|
30
|
-
return fs.existsSync(path.join(this.
|
|
30
|
+
return fs.existsSync(path.join(this.agentHome, "skills", name, "SKILL.md"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Installs skills into the Codex home (~/.codex/skills). */
|
|
34
|
+
export class CodexSkillInstaller extends HomeDirSkillInstaller {
|
|
35
|
+
constructor(home) {
|
|
36
|
+
super(path.join(home, ".codex"));
|
|
31
37
|
}
|
|
32
38
|
}
|
|
39
|
+
/** Installs skills into the Claude Code home (~/.claude/skills, specs/v1-architecture.md). */
|
|
40
|
+
export class ClaudeSkillInstaller extends HomeDirSkillInstaller {
|
|
41
|
+
constructor(home) {
|
|
42
|
+
super(path.join(home, ".claude"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Every agent the delegation skill supports, in stable display order. */
|
|
46
|
+
export function skillTargets(home) {
|
|
47
|
+
return [
|
|
48
|
+
{ agent: "Claude", installer: new ClaudeSkillInstaller(home) },
|
|
49
|
+
{ agent: "Codex", installer: new CodexSkillInstaller(home) },
|
|
50
|
+
];
|
|
51
|
+
}
|
|
33
52
|
export function glmDelegationSkill() {
|
|
34
53
|
return { name: GLM_DELEGATION_SKILL_NAME, content: GLM_DELEGATION_SKILL_MD };
|
|
35
54
|
}
|