easyrouter-config 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +108 -0
  3. package/dist/index.js +311 -0
  4. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QuantumNous
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.
22
+
23
+ ---
24
+
25
+ This software is a thin wrapper around zcf (https://github.com/UfoMiao/zcf),
26
+ which is licensed under the MIT License. zcf is invoked as a separate process
27
+ via npx; no code from zcf is bundled or modified within this package.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # easyrouter-config
2
+
3
+ > 🚀 一键把 [EasyRouter](https://easyrouter.io) 接入 Claude Code & Codex —— 粘贴 Key 即用
4
+
5
+ ## 特性
6
+
7
+ - ✅ 一条命令同时配置 Claude Code 和 Codex
8
+ - ✅ 交互式 / 非交互式两种模式
9
+ - ✅ 内置 EasyRouter 按量付费 BaseURL,无需手动输入
10
+ - ✅ 配置完成后自动验证链路连通性
11
+ - ✅ 极薄封装,复用业界成熟的 [zcf](https://github.com/UfoMiao/zcf) 写入逻辑
12
+
13
+ ## 快速开始
14
+
15
+ ### 交互式(推荐新手)
16
+
17
+ ```bash
18
+ npx -y easyrouter-config
19
+ ```
20
+
21
+ 按提示粘贴 API Key、回车即可。
22
+
23
+ ### 一行命令(按量付费)
24
+
25
+ ```bash
26
+ npx -y easyrouter-config -k sk-你的token
27
+ ```
28
+
29
+ 仅此一种部署模式 —— 内置 `https://easyrouter.io`,按 token 实际用量计费。
30
+
31
+ #### 只配 Claude Code 或只配 Codex
32
+
33
+ ```bash
34
+ # 只配 Claude Code
35
+ npx -y easyrouter-config -k sk-你的token --only claude
36
+
37
+ # 只配 Codex
38
+ npx -y easyrouter-config -k sk-你的token --only codex
39
+ ```
40
+
41
+ 完成后直接运行:
42
+
43
+ ```bash
44
+ claude # 启动 Claude Code
45
+ codex # 启动 Codex
46
+ ```
47
+
48
+ ## 命令行参数
49
+
50
+ | 参数 | 简写 | 说明 |
51
+ |---|---|---|
52
+ | `--api-key <key>` | `-k` | EasyRouter API Key(可省略 `sk-` 前缀) |
53
+ | `--only <tool>` | | `claude` \| `codex` —— 只配置其一 |
54
+ | `--skip-verify` | | 跳过连通性验证 |
55
+ | `--verbose` | `-v` | 显示底层 zcf 子进程的详细输出 |
56
+ | `--help` | `-h` | 显示帮助 |
57
+ | `--version` | `-V` | 显示版本 |
58
+
59
+ ## 工作原理
60
+
61
+ `easyrouter-config` 是 [zcf](https://github.com/UfoMiao/zcf) 的薄封装:
62
+
63
+ 1. 通过交互或参数收集 API Key
64
+ 2. 内置 EasyRouter 按量付费 BaseURL(`https://easyrouter.io`)
65
+ 3. 调用 `npx zcf init` 两次(一次配 `cc`,一次配 `cx`)
66
+ 4. zcf 实际写入:
67
+ - Claude Code → `~/.claude/settings.json`(注入 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_API_KEY` 到 `env`)
68
+ - Codex → `~/.codex/config.toml` + `~/.codex/auth.json`
69
+ 5. 通过 `GET {BASE_URL}/v1/models` 探测连通性
70
+
71
+ 完全等价于手动执行:
72
+
73
+ ```bash
74
+ npx -y zcf init -s --code-type cc -p custom --api-type auth_token \
75
+ -u https://easyrouter.io -k sk-xxx
76
+ npx -y zcf init -s --code-type cx -p custom --api-type auth_token \
77
+ -u https://easyrouter.io/v1 -k sk-xxx
78
+ ```
79
+
80
+ 但你只需要记住一行:`npx -y easyrouter-config`。
81
+
82
+ ## EasyRouter 是什么
83
+
84
+ [EasyRouter](https://easyrouter.io) 是下一代 LLM 网关与 AI 资产管理平台:
85
+
86
+ - 统一接口聚合 40+ 上游模型供应商(OpenAI / Claude / Gemini / Azure / Bedrock …)
87
+ - 完整的用户、配额、计费、限流体系
88
+ - 支持按量付费
89
+
90
+ ## 致谢
91
+
92
+ 本工具的核心配置写入逻辑由 **[zcf](https://github.com/UfoMiao/zcf)** 提供。
93
+ 我们仅在其上做了 EasyRouter 专属的预设封装与交互简化。
94
+
95
+ 特别感谢 [@UfoMiao](https://github.com/UfoMiao) 与 zcf 团队的开源贡献。
96
+ 推荐对工作流定制有更多需求的用户直接使用 zcf 原生命令。
97
+
98
+ ## 开发
99
+
100
+ ```bash
101
+ npm install
102
+ npm run build
103
+ node ./dist/index.js --help
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT © QuantumNous
package/dist/index.js ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import {
5
+ intro,
6
+ outro,
7
+ text,
8
+ confirm,
9
+ spinner,
10
+ isCancel,
11
+ cancel,
12
+ log,
13
+ note
14
+ } from "@clack/prompts";
15
+ import { execa } from "execa";
16
+ import pc from "picocolors";
17
+ import { parseArgs } from "util";
18
+ var VERSION = "0.1.0";
19
+ var CLAUDE_BASE_URL = "https://easyrouter.io";
20
+ var CODEX_BASE_URL = "https://easyrouter.io/v1";
21
+ function parseCliArgs() {
22
+ try {
23
+ const { values } = parseArgs({
24
+ args: process.argv.slice(2),
25
+ options: {
26
+ "api-key": { type: "string", short: "k" },
27
+ only: { type: "string" },
28
+ "skip-verify": { type: "boolean" },
29
+ verbose: { type: "boolean", short: "v" },
30
+ help: { type: "boolean", short: "h" },
31
+ version: { type: "boolean", short: "V" }
32
+ },
33
+ allowPositionals: false,
34
+ strict: false
35
+ });
36
+ return {
37
+ apiKey: values["api-key"],
38
+ only: values.only,
39
+ skipVerify: values["skip-verify"],
40
+ verbose: values.verbose,
41
+ help: values.help,
42
+ version: values.version
43
+ };
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+ function printHelp() {
49
+ console.log(`
50
+ ${pc.bold(pc.cyan("easyrouter-config"))} ${pc.dim("v" + VERSION)}
51
+ \u4E00\u952E\u628A EasyRouter \u63A5\u5165 Claude Code & Codex\uFF08\u6309\u91CF\u4ED8\u8D39\uFF09
52
+
53
+ ${pc.bold("\u7528\u6CD5:")}
54
+ ${pc.green("npx -y easyrouter-config")} \u4EA4\u4E92\u5F0F\uFF08\u63A8\u8350\uFF09
55
+ ${pc.green("npx -y easyrouter-config -k sk-xxx")} \u4E00\u884C\u547D\u4EE4\u914D\u7F6E
56
+ ${pc.green("npx -y easyrouter-config -k sk-xxx --only claude")} \u53EA\u914D Claude Code
57
+
58
+ ${pc.bold("\u53C2\u6570:")}
59
+ -k, --api-key <key> EasyRouter API Key\uFF08\u53EF\u7701\u7565 sk- \u524D\u7F00\uFF09
60
+ --only <tool> claude | codex \uFF08\u53EA\u914D\u5176\u4E00\uFF09
61
+ --skip-verify \u8DF3\u8FC7\u8FDE\u901A\u6027\u9A8C\u8BC1
62
+ -v, --verbose \u663E\u793A zcf \u5B50\u8FDB\u7A0B\u8BE6\u7EC6\u8F93\u51FA
63
+ -h, --help \u663E\u793A\u5E2E\u52A9
64
+ -V, --version \u663E\u793A\u7248\u672C
65
+
66
+ ${pc.bold("\u5185\u7F6E BaseURL:")}
67
+ Claude Code \u2192 ${CLAUDE_BASE_URL}
68
+ Codex \u2192 ${CODEX_BASE_URL}
69
+
70
+ ${pc.bold("\u539F\u7406:")}
71
+ \u672C\u5DE5\u5177\u662F ${pc.underline("zcf")} (https://github.com/UfoMiao/zcf) \u7684\u8584\u5C01\u88C5\u3002
72
+ \u5B9E\u9645\u5199\u5165\u7684\u914D\u7F6E\u5B8C\u5168\u7531 zcf \u5B8C\u6210\uFF1A
73
+ \u2022 Claude Code \u2192 ~/.claude/settings.json
74
+ \u2022 Codex \u2192 ~/.codex/config.toml + ~/.codex/auth.json
75
+ `);
76
+ }
77
+ async function runZcf(opts) {
78
+ const args = [
79
+ "-y",
80
+ "zcf",
81
+ "init",
82
+ "-s",
83
+ // --skip-prompt
84
+ "--code-type",
85
+ opts.codeType,
86
+ "-p",
87
+ "custom",
88
+ "--api-type",
89
+ "auth_token",
90
+ // EasyRouter 走 Bearer
91
+ "-u",
92
+ opts.baseUrl,
93
+ "-k",
94
+ opts.apiKey,
95
+ "-r",
96
+ "backup",
97
+ // 已有配置自动备份
98
+ "--mcp-services",
99
+ "skip",
100
+ // 不装额外 MCP,加速
101
+ "--workflows",
102
+ "skip",
103
+ // 不装工作流模板
104
+ "--output-styles",
105
+ "skip",
106
+ // 不装输出样式
107
+ "--install-cometix-line",
108
+ "false"
109
+ // 不装状态栏
110
+ ];
111
+ let capturedOut = "";
112
+ let capturedErr = "";
113
+ try {
114
+ const child = execa("npx", args, {
115
+ stdio: opts.verbose ? "inherit" : "pipe",
116
+ env: { ...process.env, FORCE_COLOR: opts.verbose ? "1" : "0" },
117
+ reject: true
118
+ });
119
+ if (!opts.verbose) {
120
+ child.stdout?.on("data", (chunk) => {
121
+ capturedOut += chunk.toString();
122
+ });
123
+ child.stderr?.on("data", (chunk) => {
124
+ capturedErr += chunk.toString();
125
+ });
126
+ }
127
+ await child;
128
+ } catch (err) {
129
+ const detail = err?.stderr || err?.stdout || err?.message || String(err);
130
+ throw new Error(
131
+ `zcf \u6267\u884C\u5931\u8D25 (code-type=${opts.codeType}):
132
+ ${detail}
133
+
134
+ \u{1F4A1} \u7528 ${pc.green("--verbose")} \u91CD\u65B0\u8FD0\u884C\u53EF\u770B\u5230\u5B8C\u6574\u65E5\u5FD7`
135
+ );
136
+ }
137
+ const combined = capturedOut + "\n" + capturedErr;
138
+ if (/CACError|Unknown option/i.test(combined)) {
139
+ const match = combined.match(/(CACError:.*?)(?:\n|$)/);
140
+ throw new Error(
141
+ `zcf \u62D2\u7EDD\u4E86\u6211\u4EEC\u4F20\u7684\u53C2\u6570\uFF08code-type=${opts.codeType}\uFF09\uFF1A
142
+ ${match?.[1] || "Unknown option"}
143
+
144
+ \u8FD9\u901A\u5E38\u610F\u5473\u7740 zcf \u5347\u7EA7\u540E flag \u547D\u540D\u53D8\u4E86\u3002\u8BF7\u8054\u7CFB service@easyrouter.io\u3002`
145
+ );
146
+ }
147
+ }
148
+ async function verifyConnection(baseUrl, apiKey) {
149
+ const probeUrl = baseUrl.replace(/\/+$/, "") + "/v1/models";
150
+ try {
151
+ const ctrl = new AbortController();
152
+ const timer = setTimeout(() => ctrl.abort(), 1e4);
153
+ const res = await fetch(probeUrl, {
154
+ method: "GET",
155
+ headers: {
156
+ Authorization: `Bearer ${apiKey}`,
157
+ "anthropic-version": "2023-06-01"
158
+ },
159
+ signal: ctrl.signal
160
+ });
161
+ clearTimeout(timer);
162
+ return { ok: res.ok, status: res.status };
163
+ } catch (err) {
164
+ return { ok: false, error: err?.message || String(err) };
165
+ }
166
+ }
167
+ function normalizeApiKey(raw) {
168
+ const trimmed = raw.trim();
169
+ if (!trimmed) return trimmed;
170
+ if (trimmed.startsWith("sk-")) return trimmed;
171
+ return "sk-" + trimmed;
172
+ }
173
+ function maskKey(key) {
174
+ if (key.length <= 8) return "*".repeat(key.length);
175
+ return key.slice(0, 6) + "*".repeat(Math.max(4, key.length - 10)) + key.slice(-4);
176
+ }
177
+ async function main() {
178
+ const args = parseCliArgs();
179
+ if (args.help) {
180
+ printHelp();
181
+ return;
182
+ }
183
+ if (args.version) {
184
+ console.log(VERSION);
185
+ return;
186
+ }
187
+ console.log();
188
+ intro(pc.bgCyan(pc.black(" EasyRouter Config ")) + pc.dim(" v" + VERSION));
189
+ let apiKey;
190
+ if (args.apiKey) {
191
+ apiKey = normalizeApiKey(args.apiKey);
192
+ log.info(`API Key: ${pc.dim(maskKey(apiKey))}\uFF08\u6765\u81EA\u547D\u4EE4\u884C\u53C2\u6570\uFF09`);
193
+ } else {
194
+ const input = await text({
195
+ message: "\u8BF7\u8F93\u5165\u4F60\u7684 EasyRouter API Key",
196
+ placeholder: "sk-xxxxxxxxxxxxxxxx",
197
+ validate(value) {
198
+ if (!value) return "\u4E0D\u80FD\u4E3A\u7A7A";
199
+ if (value.trim().length < 8) return "Key \u957F\u5EA6\u770B\u8D77\u6765\u4E0D\u5BF9";
200
+ return void 0;
201
+ }
202
+ });
203
+ if (isCancel(input)) {
204
+ cancel("\u5DF2\u53D6\u6D88");
205
+ process.exit(0);
206
+ }
207
+ apiKey = normalizeApiKey(input);
208
+ }
209
+ let configClaude = true;
210
+ let configCodex = true;
211
+ if (args.only === "claude") {
212
+ configCodex = false;
213
+ } else if (args.only === "codex") {
214
+ configClaude = false;
215
+ }
216
+ note(
217
+ [
218
+ `${pc.bold("\u8BA1\u8D39\u6A21\u5F0F")} \u6309\u91CF\u4ED8\u8D39`,
219
+ configClaude ? `${pc.bold("Claude URL")} ${CLAUDE_BASE_URL}` : null,
220
+ configCodex ? `${pc.bold("Codex URL")} ${CODEX_BASE_URL}` : null,
221
+ `${pc.bold("API Key")} ${maskKey(apiKey)}`,
222
+ `${pc.bold("\u5C06\u914D\u7F6E")} ${[
223
+ configClaude && "Claude Code",
224
+ configCodex && "Codex"
225
+ ].filter(Boolean).join(" + ")}`
226
+ ].filter(Boolean).join("\n"),
227
+ "\u914D\u7F6E\u9884\u89C8"
228
+ );
229
+ if (!args.apiKey) {
230
+ const ok = await confirm({ message: "\u7EE7\u7EED\uFF1F", initialValue: true });
231
+ if (isCancel(ok) || !ok) {
232
+ cancel("\u5DF2\u53D6\u6D88");
233
+ process.exit(0);
234
+ }
235
+ }
236
+ const verbose = !!args.verbose;
237
+ if (configClaude) {
238
+ if (verbose) {
239
+ log.step("\u914D\u7F6E Claude Code\uFF08\u8BE6\u7EC6\u65E5\u5FD7\uFF09");
240
+ }
241
+ const s = verbose || !process.stdout.isTTY ? null : spinner();
242
+ s?.start("\u914D\u7F6E Claude Code");
243
+ if (!s && !verbose) log.info("\u6B63\u5728\u914D\u7F6E Claude Code\uFF08\u9996\u6B21\u8FD0\u884C\u9700\u4E0B\u8F7D zcf\uFF0C\u7EA6 30~60 \u79D2\uFF09...");
244
+ try {
245
+ await runZcf({
246
+ codeType: "cc",
247
+ baseUrl: CLAUDE_BASE_URL,
248
+ apiKey,
249
+ verbose
250
+ });
251
+ s?.stop(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.claude/settings.json"));
252
+ if (!s) log.success(pc.green("\u2713 Claude Code \u914D\u7F6E\u5B8C\u6210 \u2192 ~/.claude/settings.json"));
253
+ } catch (err) {
254
+ s?.stop(pc.red("\u2717 Claude Code \u914D\u7F6E\u5931\u8D25"));
255
+ console.error(err.message);
256
+ process.exit(1);
257
+ }
258
+ }
259
+ if (configCodex) {
260
+ if (verbose) {
261
+ log.step("\u914D\u7F6E Codex\uFF08\u8BE6\u7EC6\u65E5\u5FD7\uFF09");
262
+ }
263
+ const s = verbose || !process.stdout.isTTY ? null : spinner();
264
+ s?.start("\u914D\u7F6E Codex");
265
+ if (!s && !verbose) log.info("\u6B63\u5728\u914D\u7F6E Codex...");
266
+ try {
267
+ await runZcf({
268
+ codeType: "cx",
269
+ baseUrl: CODEX_BASE_URL,
270
+ apiKey,
271
+ verbose
272
+ });
273
+ s?.stop(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 ") + pc.dim("\u2192 ~/.codex/config.toml"));
274
+ if (!s) log.success(pc.green("\u2713 Codex \u914D\u7F6E\u5B8C\u6210 \u2192 ~/.codex/config.toml"));
275
+ } catch (err) {
276
+ s?.stop(pc.red("\u2717 Codex \u914D\u7F6E\u5931\u8D25"));
277
+ console.error(err.message);
278
+ process.exit(1);
279
+ }
280
+ }
281
+ if (!args.skipVerify) {
282
+ const s = process.stdout.isTTY ? spinner() : null;
283
+ s?.start("\u9A8C\u8BC1\u8FDE\u901A\u6027");
284
+ if (!s) log.info("\u6B63\u5728\u9A8C\u8BC1\u8FDE\u901A\u6027...");
285
+ const result = await verifyConnection(CLAUDE_BASE_URL, apiKey);
286
+ if (result.ok) {
287
+ const msg = pc.green("\u2713 \u94FE\u8DEF\u9A8C\u8BC1\u901A\u8FC7");
288
+ s?.stop(msg) ?? log.success(msg);
289
+ } else {
290
+ const msg = pc.yellow(
291
+ `\u26A0 \u9A8C\u8BC1\u672A\u901A\u8FC7${result.status ? ` (HTTP ${result.status})` : ""}` + (result.error ? `: ${result.error}` : "") + ` \u2014\u2014 \u914D\u7F6E\u5DF2\u5199\u5165\uFF0C\u53EF\u624B\u52A8\u6D4B\u8BD5`
292
+ );
293
+ s?.stop(msg) ?? log.warn(msg);
294
+ }
295
+ }
296
+ const tips = [];
297
+ if (configClaude) tips.push(pc.cyan("claude") + pc.dim(" # \u542F\u52A8 Claude Code"));
298
+ if (configCodex) tips.push(pc.cyan("codex") + pc.dim(" # \u542F\u52A8 Codex"));
299
+ note(tips.join("\n"), "\u{1F389} \u5168\u90E8\u5B8C\u6210\uFF01\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
300
+ outro(
301
+ pc.dim("\u611F\u8C22\u4F7F\u7528 EasyRouter ") + pc.underline(pc.cyan("https://easyrouter.io")) + pc.dim(" \xB7 powered by ") + pc.underline("zcf")
302
+ );
303
+ }
304
+ main().catch((err) => {
305
+ console.error();
306
+ console.error(pc.red("\u2717 \u81F4\u547D\u9519\u8BEF\uFF1A"), err?.message || err);
307
+ if (err?.stack && process.env.DEBUG) {
308
+ console.error(pc.dim(err.stack));
309
+ }
310
+ process.exit(1);
311
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "easyrouter-config",
3
+ "version": "1.0.1",
4
+ "description": "🚀 一键把 EasyRouter 接入 Claude Code & Codex —— 粘贴 Key 即用",
5
+ "type": "module",
6
+ "bin": {
7
+ "easyrouter-config": "./dist/index.js",
8
+ "erc": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "start": "node ./dist/index.js",
19
+ "prepublishOnly": "npm run build",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "keywords": [
23
+ "easyrouter",
24
+ "claude-code",
25
+ "codex",
26
+ "cli",
27
+ "ai-gateway",
28
+ "zcf"
29
+ ],
30
+ "author": "QuantumNous <service@easyrouter.io>",
31
+ "license": "MIT",
32
+ "homepage": "https://easyrouter.io",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://gitlab.liebaopay.com/easyrouter/easyrouter-cli-config.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://gitlab.liebaopay.com/easyrouter/easyrouter-cli-config/-/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "@clack/prompts": "^0.11.0",
45
+ "execa": "^9.5.2",
46
+ "picocolors": "^1.1.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.10.0",
50
+ "tsup": "^8.3.5",
51
+ "typescript": "^5.7.2"
52
+ }
53
+ }