yingclaw 1.6.3 → 1.7.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 +12 -2
- package/bin/cli.js +125 -15
- package/index.js +1 -0
- package/lib/config.js +59 -10
- package/lib/install.js +9 -0
- package/lib/panel.js +1 -1
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Claude Code × 国产大模型,一键接入。
|
|
4
4
|
|
|
5
|
-
支持 DeepSeek、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo
|
|
5
|
+
支持 DeepSeek、阿里云百炼(Qwen)、MiniMax、智谱 GLM、小米 MiMo,也支持自定义 Anthropic 兼容接口,无需梯子即可使用 Claude Code。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -14,6 +14,8 @@ npm install -g yingclaw
|
|
|
14
14
|
|
|
15
15
|
## 使用步骤
|
|
16
16
|
|
|
17
|
+
> 当前自动写入环境变量支持 macOS / Linux 的 zsh、bash。Windows 用户可参考“原理”中的环境变量手动配置,PowerShell 自动写入后续版本支持。
|
|
18
|
+
|
|
17
19
|
**第一步:安装 Claude Code**
|
|
18
20
|
```bash
|
|
19
21
|
claw install-claude
|
|
@@ -24,7 +26,14 @@ claw install-claude
|
|
|
24
26
|
```bash
|
|
25
27
|
claw setup
|
|
26
28
|
```
|
|
27
|
-
选择厂商 →
|
|
29
|
+
选择厂商 → 输入 API Key → 选择模型,自动写入环境变量,配置完成后自动启动 Claude。
|
|
30
|
+
|
|
31
|
+
选择“自定义 Anthropic 兼容接口”时,需要输入:
|
|
32
|
+
|
|
33
|
+
- `ANTHROPIC_BASE_URL`
|
|
34
|
+
- API Key
|
|
35
|
+
|
|
36
|
+
工具会根据 Base URL 自动尝试获取模型列表;如果获取失败,则手动输入主模型和快速模型。
|
|
28
37
|
|
|
29
38
|
**第三步:以后直接用**
|
|
30
39
|
```bash
|
|
@@ -40,6 +49,7 @@ claude
|
|
|
40
49
|
| MiniMax | M2.7、M2.7 Turbo、M2.5 |
|
|
41
50
|
| 智谱 GLM | GLM-4.7、GLM-5.1、GLM-5 Turbo、GLM-4.5 Air |
|
|
42
51
|
| 小米 MiMo | MiMo V2.5 Pro |
|
|
52
|
+
| 自定义接口 | 自动获取或手动输入 |
|
|
43
53
|
|
|
44
54
|
## 其他命令
|
|
45
55
|
|
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
saveConfig,
|
|
8
8
|
writeEnvToZshrc,
|
|
9
9
|
fetchModels,
|
|
10
|
+
fetchModelsFromBaseUrl,
|
|
10
11
|
resetConfig,
|
|
11
12
|
validateConfig,
|
|
12
13
|
resolveFastModel,
|
|
@@ -18,6 +19,7 @@ const { execSync, spawn, spawnSync } = require('child_process');
|
|
|
18
19
|
const https = require('https');
|
|
19
20
|
const pkg = require('../package.json');
|
|
20
21
|
const { buildMenuStatusLines, buildStatusView } = require('../lib/panel');
|
|
22
|
+
const { buildClaudeInstallCommand } = require('../lib/install');
|
|
21
23
|
|
|
22
24
|
const program = new Command();
|
|
23
25
|
|
|
@@ -81,6 +83,96 @@ function isClaudeInstalled() {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
function isValidUrl(value) {
|
|
87
|
+
try {
|
|
88
|
+
new URL(value);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function promptModelFromChoices({ chalk, choices, message, backLabel = '↩ 返回上一步', allowManual = true }) {
|
|
96
|
+
const selected = await select({ loop: false,
|
|
97
|
+
message: chalk.cyan(message),
|
|
98
|
+
choices: [
|
|
99
|
+
...choices,
|
|
100
|
+
...(allowManual ? [{ name: '手动输入模型名', value: '__MANUAL__' }] : []),
|
|
101
|
+
{ name: chalk.dim(backLabel), value: '__BACK__' },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
if (selected === '__BACK__') return '__BACK__';
|
|
105
|
+
if (selected !== '__MANUAL__') return selected;
|
|
106
|
+
|
|
107
|
+
return input({
|
|
108
|
+
message: chalk.cyan('输入模型名'),
|
|
109
|
+
validate: (v) => v.trim().length > 0 ? true : '模型名不能为空',
|
|
110
|
+
}).then(v => v.trim());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function promptManualModel(chalk, message, defaultValue) {
|
|
114
|
+
return input({
|
|
115
|
+
message: chalk.cyan(message),
|
|
116
|
+
default: defaultValue,
|
|
117
|
+
validate: (v) => v.trim().length > 0 ? true : '模型名不能为空',
|
|
118
|
+
}).then(v => v.trim());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function configureCustomProvider({ chalk, ora, existingConfig }) {
|
|
122
|
+
const baseUrl = await input({
|
|
123
|
+
message: chalk.cyan('Anthropic Base URL'),
|
|
124
|
+
default: existingConfig?.provider === 'custom' ? existingConfig.baseUrl : undefined,
|
|
125
|
+
validate: (v) => v.trim().length > 0 && isValidUrl(v.trim()) ? true : '请输入有效 URL',
|
|
126
|
+
}).then(v => v.trim().replace(/\/+$/, ''));
|
|
127
|
+
|
|
128
|
+
let apiKey = existingConfig?.provider === 'custom' ? existingConfig.apiKey : '';
|
|
129
|
+
if (apiKey) {
|
|
130
|
+
const keepKey = await confirm({ message: '沿用当前 API Key?', default: true });
|
|
131
|
+
if (!keepKey) apiKey = '';
|
|
132
|
+
}
|
|
133
|
+
if (!apiKey) {
|
|
134
|
+
apiKey = await input({
|
|
135
|
+
message: chalk.cyan('API Key'),
|
|
136
|
+
transformer: (v) => v ? chalk.dim('•'.repeat(v.length)) : '',
|
|
137
|
+
validate: (v) => v.trim().length > 0 ? true : 'API Key 不能为空',
|
|
138
|
+
}).then(v => v.trim());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let modelChoices = [];
|
|
142
|
+
let modelsUrl;
|
|
143
|
+
const fetchSpinner = ora('正在自动获取可用模型...').start();
|
|
144
|
+
const onlineResult = await fetchModelsFromBaseUrl('custom', apiKey, baseUrl);
|
|
145
|
+
if (onlineResult) {
|
|
146
|
+
modelsUrl = onlineResult.modelsUrl;
|
|
147
|
+
fetchSpinner.succeed(chalk.green(`已获取 ${onlineResult.models.length} 个可用模型`));
|
|
148
|
+
modelChoices = onlineResult.models.map(id => ({ name: id, value: id }));
|
|
149
|
+
} else {
|
|
150
|
+
fetchSpinner.warn(chalk.yellow('无法自动获取模型列表,改为手动输入模型'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let model;
|
|
154
|
+
let fastModel;
|
|
155
|
+
if (modelChoices.length > 0) {
|
|
156
|
+
model = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择主模型' });
|
|
157
|
+
if (model === '__BACK__') return null;
|
|
158
|
+
fastModel = await promptModelFromChoices({ chalk, choices: modelChoices, message: '选择快速模型 / Subagent 模型' });
|
|
159
|
+
if (fastModel === '__BACK__') return null;
|
|
160
|
+
} else {
|
|
161
|
+
model = await promptManualModel(chalk, '输入主模型名');
|
|
162
|
+
fastModel = await promptManualModel(chalk, '输入快速模型 / Subagent 模型名', model);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
provider: 'custom',
|
|
167
|
+
providerName: '自定义接口',
|
|
168
|
+
baseUrl,
|
|
169
|
+
modelsUrl: modelsUrl || undefined,
|
|
170
|
+
apiKey,
|
|
171
|
+
model,
|
|
172
|
+
fastModel,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
84
176
|
async function showStatus() {
|
|
85
177
|
const chalk = (await import('chalk')).default;
|
|
86
178
|
const boxen = (await import('boxen')).default;
|
|
@@ -176,14 +268,12 @@ program
|
|
|
176
268
|
});
|
|
177
269
|
if (network === '__BACK__') return;
|
|
178
270
|
|
|
179
|
-
const
|
|
180
|
-
? 'npm install -g @anthropic-ai/claude-code'
|
|
181
|
-
: 'npm install -g @anthropic-ai/claude-code --registry=https://registry.npmmirror.com';
|
|
271
|
+
const installCommand = buildClaudeInstallCommand(network);
|
|
182
272
|
|
|
183
273
|
console.log(chalk.dim('\n安装中,实时输出:\n'));
|
|
184
274
|
|
|
185
275
|
// 实时输出安装日志
|
|
186
|
-
const result = spawnSync(
|
|
276
|
+
const result = spawnSync(installCommand.command, installCommand.args, { stdio: 'inherit' });
|
|
187
277
|
|
|
188
278
|
if (result.status === 0) {
|
|
189
279
|
console.log(chalk.green('\n✔ Claude Code 安装成功!'));
|
|
@@ -237,7 +327,7 @@ program
|
|
|
237
327
|
if (!overwrite) return;
|
|
238
328
|
}
|
|
239
329
|
|
|
240
|
-
let providerKey, provider, apiKey, model;
|
|
330
|
+
let providerKey, provider, apiKey, model, customConfig;
|
|
241
331
|
let step = 'provider';
|
|
242
332
|
|
|
243
333
|
while (true) {
|
|
@@ -251,6 +341,11 @@ program
|
|
|
251
341
|
});
|
|
252
342
|
if (providerKey === '__BACK__') return;
|
|
253
343
|
provider = PROVIDERS[providerKey];
|
|
344
|
+
if (provider.custom) {
|
|
345
|
+
customConfig = await configureCustomProvider({ chalk, ora });
|
|
346
|
+
if (!customConfig) { step = 'provider'; continue; }
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
254
349
|
step = 'apikey';
|
|
255
350
|
} else if (step === 'apikey') {
|
|
256
351
|
const k = await input({
|
|
@@ -289,10 +384,10 @@ program
|
|
|
289
384
|
const spinner = ora('写入配置...').start();
|
|
290
385
|
let result, file;
|
|
291
386
|
try {
|
|
292
|
-
const fastModel = resolveFastModel(provider, model);
|
|
293
|
-
const cfg = { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
|
|
387
|
+
const fastModel = customConfig?.fastModel || resolveFastModel(provider, model);
|
|
388
|
+
const cfg = customConfig || { provider: providerKey, model, fastModel, apiKey, baseUrl: provider.baseUrl };
|
|
294
389
|
saveConfig(cfg);
|
|
295
|
-
({ result, file } = writeEnvToZshrc(
|
|
390
|
+
({ result, file } = writeEnvToZshrc(cfg.baseUrl, cfg.apiKey, cfg.model, cfg.fastModel));
|
|
296
391
|
spinner.succeed(chalk.green(result === 'updated' ? `环境变量已更新 → ${file}` : `环境变量已写入 → ${file}`));
|
|
297
392
|
} catch (e) {
|
|
298
393
|
spinner.fail(chalk.red(`写入失败: ${e.message}`));
|
|
@@ -302,7 +397,7 @@ program
|
|
|
302
397
|
|
|
303
398
|
console.log(boxen(
|
|
304
399
|
chalk.bold('配置完成!\n\n') +
|
|
305
|
-
chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan(provider.baseUrl) + '\n' +
|
|
400
|
+
chalk.dim('ANTHROPIC_BASE_URL ') + chalk.cyan((customConfig || { baseUrl: provider.baseUrl }).baseUrl) + '\n' +
|
|
306
401
|
chalk.dim('ANTHROPIC_API_KEY ') + chalk.cyan('已保存') + '\n\n' +
|
|
307
402
|
chalk.white('下次直接输入 ') + chalk.cyan.bold('claude') + chalk.white(' 即可使用'),
|
|
308
403
|
{ padding: { top: 0, bottom: 0, left: 2, right: 2 }, borderStyle: 'round', borderColor: 'green', margin: { top: 1, bottom: 1 } }
|
|
@@ -314,7 +409,7 @@ program
|
|
|
314
409
|
|
|
315
410
|
spawn('claude', [], {
|
|
316
411
|
stdio: 'inherit',
|
|
317
|
-
env: { ...process.env, ...buildClaudeEnv({ provider: providerKey, baseUrl: provider.baseUrl, apiKey, model, fastModel: resolveFastModel(provider, model) }) },
|
|
412
|
+
env: { ...process.env, ...buildClaudeEnv(customConfig || { provider: providerKey, baseUrl: provider.baseUrl, apiKey, model, fastModel: resolveFastModel(provider, model) }) },
|
|
318
413
|
}).on('error', () => {
|
|
319
414
|
console.log(chalk.yellow('\nClaude Code 未找到,请先运行: claw install-claude'));
|
|
320
415
|
});
|
|
@@ -351,6 +446,18 @@ program
|
|
|
351
446
|
if (providerKey === '__BACK__') return;
|
|
352
447
|
|
|
353
448
|
const provider = PROVIDERS[providerKey];
|
|
449
|
+
if (provider.custom) {
|
|
450
|
+
const customConfig = await configureCustomProvider({ chalk, ora, existingConfig: config });
|
|
451
|
+
if (!customConfig) return;
|
|
452
|
+
|
|
453
|
+
const spinner = ora('切换中...').start();
|
|
454
|
+
saveConfig(customConfig);
|
|
455
|
+
const { file } = writeEnvToZshrc(customConfig.baseUrl, customConfig.apiKey, customConfig.model, customConfig.fastModel);
|
|
456
|
+
await new Promise(r => setTimeout(r, 300));
|
|
457
|
+
spinner.succeed(chalk.green(`已切换至 ${customConfig.providerName} · ${customConfig.model}`));
|
|
458
|
+
console.log(chalk.dim(`运行 source ${file} 生效,或重新开一个终端`));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
354
461
|
|
|
355
462
|
// 切换厂商时询问是否更换 Key
|
|
356
463
|
let apiKey = config.apiKey;
|
|
@@ -562,11 +669,14 @@ async function runMenu() {
|
|
|
562
669
|
}
|
|
563
670
|
}
|
|
564
671
|
|
|
672
|
+
function handleCliError(e) {
|
|
673
|
+
if (e?.name === 'ExitPromptError') return; // Ctrl+C
|
|
674
|
+
console.error(e);
|
|
675
|
+
process.exitCode = 1;
|
|
676
|
+
}
|
|
677
|
+
|
|
565
678
|
if (process.argv.length === 2) {
|
|
566
|
-
runMenu().catch(
|
|
567
|
-
if (e?.name === 'ExitPromptError') return; // Ctrl+C
|
|
568
|
-
console.error(e);
|
|
569
|
-
});
|
|
679
|
+
runMenu().catch(handleCliError);
|
|
570
680
|
} else {
|
|
571
|
-
program.
|
|
681
|
+
program.parseAsync(process.argv).catch(handleCliError);
|
|
572
682
|
}
|
package/index.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -55,6 +55,11 @@ const PROVIDERS = {
|
|
|
55
55
|
{ name: 'MiMo V2.5 Pro(旗舰)', value: 'mimo-v2.5-pro' },
|
|
56
56
|
],
|
|
57
57
|
},
|
|
58
|
+
custom: {
|
|
59
|
+
name: '自定义 Anthropic 兼容接口',
|
|
60
|
+
custom: true,
|
|
61
|
+
models: [],
|
|
62
|
+
},
|
|
58
63
|
};
|
|
59
64
|
|
|
60
65
|
function normalizeModelIds(providerKey, ids) {
|
|
@@ -68,18 +73,63 @@ function normalizeModelIds(providerKey, ids) {
|
|
|
68
73
|
];
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
function parseModelIdsResponse(providerKey, data) {
|
|
77
|
+
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
78
|
+
const list = parsed.data || parsed.models || [];
|
|
79
|
+
const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
|
|
80
|
+
return normalizeModelIds(providerKey, ids);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildModelUrlCandidates(baseUrl) {
|
|
84
|
+
let url;
|
|
85
|
+
try { url = new URL(baseUrl); } catch { return []; }
|
|
86
|
+
|
|
87
|
+
const pathname = url.pathname.replace(/\/+$/, '');
|
|
88
|
+
const candidates = [];
|
|
89
|
+
const add = (pathPart) => {
|
|
90
|
+
const normalized = pathPart.startsWith('/') ? pathPart : `/${pathPart}`;
|
|
91
|
+
candidates.push(`${url.origin}${normalized}`);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
add(`${pathname}/v1/models`);
|
|
95
|
+
|
|
96
|
+
if (pathname.endsWith('/anthropic')) {
|
|
97
|
+
const withoutAnthropic = pathname.slice(0, -'/anthropic'.length);
|
|
98
|
+
add(`${withoutAnthropic}/v1/models`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
add('/v1/models');
|
|
102
|
+
|
|
103
|
+
return [...new Set(candidates)];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function fetchModelsFromBaseUrl(providerKey, apiKey, baseUrl, fetcher = fetchModels) {
|
|
107
|
+
const effectiveProviderKey = providerKey === 'custom'
|
|
108
|
+
? providerKeyFromBaseUrl(baseUrl) || providerKey
|
|
109
|
+
: providerKey;
|
|
110
|
+
|
|
111
|
+
for (const modelsUrl of buildModelUrlCandidates(baseUrl)) {
|
|
112
|
+
const models = await fetcher(effectiveProviderKey, apiKey, modelsUrl);
|
|
113
|
+
if (models && models.length > 0) {
|
|
114
|
+
return { modelsUrl, models: normalizeModelIds(effectiveProviderKey, models) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
71
120
|
// 联网拉取厂商支持的模型列表,失败返回 null
|
|
72
|
-
function fetchModels(providerKey, apiKey) {
|
|
121
|
+
function fetchModels(providerKey, apiKey, modelsUrlOverride) {
|
|
73
122
|
return new Promise((resolve) => {
|
|
74
123
|
const provider = PROVIDERS[providerKey];
|
|
75
|
-
|
|
124
|
+
const modelsUrl = modelsUrlOverride || provider?.modelsUrl;
|
|
125
|
+
if (!modelsUrl) return resolve(null);
|
|
76
126
|
|
|
77
127
|
let url;
|
|
78
|
-
try { url = new URL(
|
|
128
|
+
try { url = new URL(modelsUrl); } catch { return resolve(null); }
|
|
79
129
|
|
|
80
130
|
const req = https.request({
|
|
81
131
|
hostname: url.hostname,
|
|
82
|
-
path: url.pathname,
|
|
132
|
+
path: url.pathname + url.search,
|
|
83
133
|
method: 'GET',
|
|
84
134
|
timeout: 6000,
|
|
85
135
|
headers: {
|
|
@@ -91,13 +141,9 @@ function fetchModels(providerKey, apiKey) {
|
|
|
91
141
|
res.on('data', (c) => data += c);
|
|
92
142
|
res.on('end', () => {
|
|
93
143
|
try {
|
|
94
|
-
const
|
|
95
|
-
// OpenAI 格式: { data: [{ id, ... }] }
|
|
96
|
-
// GLM 格式: { data: [{ id, ... }] } 类似
|
|
97
|
-
const list = parsed.data || parsed.models || [];
|
|
98
|
-
const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
|
|
144
|
+
const ids = parseModelIdsResponse(providerKey, data);
|
|
99
145
|
if (ids.length === 0) return resolve(null);
|
|
100
|
-
resolve(
|
|
146
|
+
resolve(ids);
|
|
101
147
|
} catch {
|
|
102
148
|
resolve(null);
|
|
103
149
|
}
|
|
@@ -257,6 +303,9 @@ module.exports = {
|
|
|
257
303
|
resetConfig,
|
|
258
304
|
validateConfig,
|
|
259
305
|
normalizeModelIds,
|
|
306
|
+
parseModelIdsResponse,
|
|
307
|
+
buildModelUrlCandidates,
|
|
308
|
+
fetchModelsFromBaseUrl,
|
|
260
309
|
resolveFastModel,
|
|
261
310
|
providerKeyFromBaseUrl,
|
|
262
311
|
buildClaudeEnv,
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function buildClaudeInstallCommand(network) {
|
|
2
|
+
const args = ['install', '-g', '@anthropic-ai/claude-code'];
|
|
3
|
+
if (network === 'cn') {
|
|
4
|
+
args.push('--registry=https://registry.npmmirror.com');
|
|
5
|
+
}
|
|
6
|
+
return { command: 'npm', args };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { buildClaudeInstallCommand };
|
package/lib/panel.js
CHANGED
|
@@ -14,7 +14,7 @@ function isEnvActive(config, env) {
|
|
|
14
14
|
|
|
15
15
|
function buildStatusView(config, options = {}) {
|
|
16
16
|
const provider = PROVIDERS[config.provider];
|
|
17
|
-
const providerName = provider?.name || config.provider;
|
|
17
|
+
const providerName = config.providerName || provider?.name || config.provider;
|
|
18
18
|
const claudeInstalled = options.claudeInstalled === true;
|
|
19
19
|
const env = options.env || {};
|
|
20
20
|
const expectedEnv = buildClaudeEnv(config);
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yingclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Claude Code × 国产大模型一键接入:DeepSeek、Qwen、MiniMax、GLM、MiMo",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claw": "bin/cli.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"index.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
9
15
|
"scripts": {
|
|
10
16
|
"start": "node bin/cli.js",
|
|
11
17
|
"test": "node --test"
|