@wavespeed/cli 0.4.16 → 0.4.17
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 +1 -1
- package/dist/commands/setup.js +70 -17
- package/dist/lib/setup-agents.js +173 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ wavespeed logout Clear stored API key
|
|
|
141
141
|
wavespeed status Show masked key, base URL, useful links
|
|
142
142
|
wavespeed config [--default-model …] View / update CLI defaults
|
|
143
143
|
wavespeed upgrade [--check] [--json] Upgrade to the latest version (--check only reports)
|
|
144
|
-
wavespeed setup [claude
|
|
144
|
+
wavespeed setup [agent] Point claude, codex, opencode, openclaw, hermes or cline at the WaveSpeed LLM gateway (--dry-run, --undo, --model)
|
|
145
145
|
|
|
146
146
|
# generation
|
|
147
147
|
wavespeed run [model|alias] -p "…" Run any model or alias (uses defaultModel if omitted)
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,16 +2,22 @@ import chalk from 'chalk';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
5
6
|
import { confirm } from '@inquirer/prompts';
|
|
7
|
+
import JSON5 from 'json5';
|
|
6
8
|
import { getApiKey, userConfig } from '../lib/config.js';
|
|
7
9
|
import { clientAttributionHeaders } from '../lib/client-headers.js';
|
|
8
10
|
import { emitJson, emitJsonError, info, setJsonMode } from '../lib/log.js';
|
|
9
|
-
import { DEFAULT_MODEL, KEY_ENV, LLM_BASE_URL, applyCodexToml, applyJson, claudeEdits, opencodeEdits, revertCodexToml, revertJson, validateModel, } from '../lib/setup-agents.js';
|
|
11
|
+
import { DEFAULT_MODEL, KEY_ENV, LLM_BASE_URL, applyCodexToml, applyHermesYaml, applyJson, claudeEdits, openclawEdits, opencodeEdits, revertCodexToml, revertHermesYaml, revertJson, validateModel, } from '../lib/setup-agents.js';
|
|
10
12
|
const home = os.homedir();
|
|
11
13
|
const AGENTS = [
|
|
12
14
|
{ id: 'claude', name: 'Claude Code', file: path.join(home, '.claude', 'settings.json'), command: 'claude', kind: 'json', keyInFile: true },
|
|
13
15
|
{ id: 'codex', name: 'Codex', file: path.join(home, '.codex', 'config.toml'), command: 'codex', kind: 'toml', keyInFile: false },
|
|
14
16
|
{ id: 'opencode', name: 'OpenCode', file: path.join(home, '.config', 'opencode', 'opencode.json'), command: 'opencode', kind: 'json', keyInFile: false },
|
|
17
|
+
{ id: 'openclaw', name: 'OpenClaw', file: path.join(home, '.openclaw', 'openclaw.json'), command: 'openclaw', kind: 'json5', keyInFile: false },
|
|
18
|
+
{ id: 'hermes', name: 'Hermes', file: path.join(home, '.hermes', 'config.yaml'), command: 'hermes', kind: 'yaml', keyInFile: false },
|
|
19
|
+
// Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
|
|
20
|
+
{ id: 'cline', name: 'Cline', file: path.join(home, '.cline', 'data', 'settings', 'providers.json'), command: 'cline', kind: 'cline', keyInFile: true },
|
|
15
21
|
];
|
|
16
22
|
const shown = (p) => p.replace(home, '~');
|
|
17
23
|
function onPath(cmd) {
|
|
@@ -43,14 +49,14 @@ export async function selfCheck(apiKey, model) {
|
|
|
43
49
|
return { ok: false, detail: err.message ?? String(err) };
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
|
-
function readJson(file) {
|
|
52
|
+
function readJson(file, json5 = false) {
|
|
47
53
|
if (!fs.existsSync(file))
|
|
48
54
|
return {};
|
|
49
55
|
const raw = fs.readFileSync(file, 'utf8');
|
|
50
56
|
if (raw.trim() === '')
|
|
51
57
|
return {};
|
|
52
58
|
try {
|
|
53
|
-
const v = JSON.parse(raw);
|
|
59
|
+
const v = json5 ? JSON5.parse(raw) : JSON.parse(raw);
|
|
54
60
|
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
55
61
|
return v;
|
|
56
62
|
}
|
|
@@ -76,7 +82,7 @@ function envHint() {
|
|
|
76
82
|
export function registerSetup(program) {
|
|
77
83
|
program
|
|
78
84
|
.command('setup [agent]')
|
|
79
|
-
.description('Point a coding agent (claude, codex, opencode) at the WaveSpeed LLM gateway')
|
|
85
|
+
.description('Point a coding agent (claude, codex, opencode, openclaw, hermes, cline) at the WaveSpeed LLM gateway')
|
|
80
86
|
.option('--model <id>', `Model to use (default: ${DEFAULT_MODEL})`)
|
|
81
87
|
.option('--dry-run', 'Show what would change without writing anything')
|
|
82
88
|
.option('--undo', "Restore the agent's config to what it was before setup")
|
|
@@ -108,6 +114,12 @@ export function registerSetup(program) {
|
|
|
108
114
|
// ----- undo -----
|
|
109
115
|
if (opts.undo) {
|
|
110
116
|
try {
|
|
117
|
+
const state = userConfig.get('setup')?.[agent.id];
|
|
118
|
+
const forget = () => {
|
|
119
|
+
const rest = { ...(userConfig.get('setup') ?? {}) };
|
|
120
|
+
delete rest[agent.id];
|
|
121
|
+
userConfig.set('setup', rest);
|
|
122
|
+
};
|
|
111
123
|
if (agent.kind === 'toml') {
|
|
112
124
|
if (!fs.existsSync(agent.file))
|
|
113
125
|
return fail(`${shown(agent.file)} does not exist; nothing to undo`);
|
|
@@ -116,17 +128,33 @@ export function registerSetup(program) {
|
|
|
116
128
|
if (!opts.dryRun && after !== before)
|
|
117
129
|
fs.writeFileSync(agent.file, after);
|
|
118
130
|
}
|
|
131
|
+
else if (!state) {
|
|
132
|
+
return fail(`No recorded ${agent.name} setup to undo`);
|
|
133
|
+
}
|
|
134
|
+
else if (agent.kind === 'cline') {
|
|
135
|
+
const bak = `${agent.file}.wavespeed.bak`;
|
|
136
|
+
if (!opts.dryRun) {
|
|
137
|
+
if (state.previous.existed && fs.existsSync(bak))
|
|
138
|
+
fs.copyFileSync(bak, agent.file);
|
|
139
|
+
else
|
|
140
|
+
fs.rmSync(agent.file, { force: true });
|
|
141
|
+
fs.rmSync(bak, { force: true });
|
|
142
|
+
forget();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (agent.kind === 'yaml') {
|
|
146
|
+
const after = revertHermesYaml(fs.readFileSync(state.file, 'utf8'), state.previous);
|
|
147
|
+
if (!opts.dryRun) {
|
|
148
|
+
fs.writeFileSync(state.file, after);
|
|
149
|
+
forget();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
119
152
|
else {
|
|
120
|
-
const
|
|
121
|
-
if (!state)
|
|
122
|
-
return fail(`No recorded ${agent.name} setup to undo`);
|
|
123
|
-
const doc = readJson(state.file);
|
|
153
|
+
const doc = readJson(state.file, agent.kind === 'json5');
|
|
124
154
|
const after = JSON.stringify(revertJson(doc, state.previous), null, 2) + '\n';
|
|
125
155
|
if (!opts.dryRun) {
|
|
126
156
|
fs.writeFileSync(state.file, after);
|
|
127
|
-
|
|
128
|
-
delete rest[agent.id];
|
|
129
|
-
userConfig.set('setup', rest);
|
|
157
|
+
forget();
|
|
130
158
|
}
|
|
131
159
|
}
|
|
132
160
|
}
|
|
@@ -149,16 +177,28 @@ export function registerSetup(program) {
|
|
|
149
177
|
catch (err) {
|
|
150
178
|
return fail(err.message);
|
|
151
179
|
}
|
|
152
|
-
let next;
|
|
180
|
+
let next = '';
|
|
153
181
|
let previous = [];
|
|
154
182
|
try {
|
|
155
183
|
if (agent.kind === 'toml') {
|
|
156
184
|
next = applyCodexToml(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', model);
|
|
157
185
|
}
|
|
186
|
+
else if (agent.kind === 'yaml') {
|
|
187
|
+
const r = applyHermesYaml(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', model);
|
|
188
|
+
next = r.text;
|
|
189
|
+
previous = r.previous;
|
|
190
|
+
}
|
|
191
|
+
else if (agent.kind === 'cline') {
|
|
192
|
+
previous = { existed: fs.existsSync(agent.file) };
|
|
193
|
+
if (!onPath('cline'))
|
|
194
|
+
throw new Error('Cline CLI not found. Install it with `npm i -g cline`. (The VS Code extension has no config file; set it up in its settings UI.)');
|
|
195
|
+
}
|
|
158
196
|
else {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
const base = readJson(agent.file, agent.kind === 'json5');
|
|
198
|
+
const edits = agent.id === 'claude' ? claudeEdits(model, apiKey) : agent.id === 'openclaw' ? openclawEdits(model, base) : opencodeEdits(model);
|
|
199
|
+
const r = applyJson(base, edits);
|
|
200
|
+
previous = r.previous;
|
|
201
|
+
next = JSON.stringify(r.doc, null, 2) + '\n';
|
|
162
202
|
}
|
|
163
203
|
}
|
|
164
204
|
catch (err) {
|
|
@@ -180,8 +220,21 @@ export function registerSetup(program) {
|
|
|
180
220
|
if (!(await confirm({ message: 'Continue?', default: false })))
|
|
181
221
|
return fail('Cancelled; nothing written.');
|
|
182
222
|
}
|
|
183
|
-
|
|
184
|
-
|
|
223
|
+
if (agent.kind === 'cline') {
|
|
224
|
+
const bak = `${agent.file}.wavespeed.bak`;
|
|
225
|
+
if (fs.existsSync(agent.file) && !fs.existsSync(bak))
|
|
226
|
+
fs.copyFileSync(agent.file, bak);
|
|
227
|
+
const r = spawnSync('cline', ['auth', '-p', 'openai-compatible', '-k', apiKey, '-b', LLM_BASE_URL, '-m', model], { stdio: ['ignore', 'ignore', 'pipe'], encoding: 'utf8', shell: process.platform === 'win32' });
|
|
228
|
+
if (r.status !== 0)
|
|
229
|
+
return fail(`cline auth failed: ${(r.stderr || r.error?.message || '').trim().slice(0, 300)}`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const hadComments = agent.kind === 'json5' && exists && /^\s*\/\/|\/\*/m.test(fs.readFileSync(agent.file, 'utf8'));
|
|
233
|
+
writeFile(agent.file, next, agent.keyInFile);
|
|
234
|
+
if (hadComments)
|
|
235
|
+
info(chalk.yellow(`Comments in ${shown(agent.file)} were not kept; the original is saved as ${shown(agent.file)}.wavespeed.bak`));
|
|
236
|
+
}
|
|
237
|
+
if (agent.kind !== 'toml')
|
|
185
238
|
userConfig.set('setup', { ...(userConfig.get('setup') ?? {}), [agent.id]: { file: agent.file, previous } });
|
|
186
239
|
const check = opts.check ? await selfCheck(apiKey, model) : null;
|
|
187
240
|
if (opts.json) {
|
package/dist/lib/setup-agents.js
CHANGED
|
@@ -12,6 +12,7 @@ export function validateModel(model) {
|
|
|
12
12
|
}
|
|
13
13
|
return model;
|
|
14
14
|
}
|
|
15
|
+
import { parseDocument } from 'yaml';
|
|
15
16
|
function isObj(v) {
|
|
16
17
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
17
18
|
}
|
|
@@ -86,6 +87,178 @@ export function opencodeEdits(model) {
|
|
|
86
87
|
{ path: ['model'], value: `wavespeed/${m}` },
|
|
87
88
|
];
|
|
88
89
|
}
|
|
90
|
+
export function openclawEdits(model, doc) {
|
|
91
|
+
const m = validateModel(model);
|
|
92
|
+
const edits = [
|
|
93
|
+
{ path: ['agents', 'defaults', 'model', 'primary'], value: `wavespeed/${m}` },
|
|
94
|
+
{ path: ['models', 'mode'], value: 'merge' },
|
|
95
|
+
{
|
|
96
|
+
path: ['models', 'providers', 'wavespeed'],
|
|
97
|
+
value: {
|
|
98
|
+
baseUrl: LLM_BASE_URL,
|
|
99
|
+
apiKey: `\${${KEY_ENV}}`,
|
|
100
|
+
api: 'openai-completions',
|
|
101
|
+
models: [{ id: m, name: m, contextWindow: 200000, maxTokens: 32000 }],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
// A user-maintained model allowlist would otherwise reject the new model.
|
|
106
|
+
const agents = doc.agents;
|
|
107
|
+
const defaults = agents?.defaults;
|
|
108
|
+
if (isObj(defaults?.models))
|
|
109
|
+
edits.push({ path: ['agents', 'defaults', 'models', `wavespeed/${m}`], value: {} });
|
|
110
|
+
return edits;
|
|
111
|
+
}
|
|
112
|
+
const lineStart = (t, off) => t.lastIndexOf('\n', off - 1) + 1;
|
|
113
|
+
const lineEnd = (t, off) => {
|
|
114
|
+
const i = t.indexOf('\n', off);
|
|
115
|
+
return i < 0 ? t.length : i + 1;
|
|
116
|
+
};
|
|
117
|
+
/** End of the line holding the last real character before `off` (a node's range can run over trailing blank lines). */
|
|
118
|
+
const contentLineEnd = (t, off) => lineEnd(t, t.slice(0, off).trimEnd().length - 1);
|
|
119
|
+
function applyEdits(text, edits) {
|
|
120
|
+
return [...edits].sort((x, y) => y.start - x.start).reduce((t, e) => t.slice(0, e.start) + e.text + t.slice(e.end), text);
|
|
121
|
+
}
|
|
122
|
+
function entryLines(pad) {
|
|
123
|
+
return [
|
|
124
|
+
`${pad}- name: wavespeed`,
|
|
125
|
+
`${pad} base_url: ${LLM_BASE_URL}`,
|
|
126
|
+
`${pad} key_env: ${KEY_ENV}`,
|
|
127
|
+
`${pad} api_mode: chat_completions`,
|
|
128
|
+
'',
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
131
|
+
function dashPad(text, itemStart) {
|
|
132
|
+
const dash = text.lastIndexOf('-', itemStart - 1);
|
|
133
|
+
return { dash, pad: ' '.repeat(dash - lineStart(text, dash)) };
|
|
134
|
+
}
|
|
135
|
+
function parseYaml(text) {
|
|
136
|
+
const doc = parseDocument(text);
|
|
137
|
+
if (doc.errors.length)
|
|
138
|
+
throw new Error(`config.yaml is not valid YAML: ${doc.errors[0].message}`);
|
|
139
|
+
return doc;
|
|
140
|
+
}
|
|
141
|
+
export function applyHermesYaml(input, model) {
|
|
142
|
+
const m = validateModel(model);
|
|
143
|
+
let text = input;
|
|
144
|
+
if (text !== '' && !text.endsWith('\n'))
|
|
145
|
+
text += '\n';
|
|
146
|
+
const doc = parseYaml(text);
|
|
147
|
+
const edits = [];
|
|
148
|
+
let tail = '';
|
|
149
|
+
const previous = { hadModelBlock: false, hadList: false };
|
|
150
|
+
const want = { default: JSON.stringify(m), provider: '"wavespeed"' };
|
|
151
|
+
const root = doc.contents;
|
|
152
|
+
const modelNode = root?.get?.('model', true);
|
|
153
|
+
if (modelNode === undefined || modelNode === null) {
|
|
154
|
+
tail += `model:\n default: ${want.default}\n provider: ${want.provider}\n`;
|
|
155
|
+
}
|
|
156
|
+
else if (modelNode.items) {
|
|
157
|
+
previous.hadModelBlock = true;
|
|
158
|
+
const missing = [];
|
|
159
|
+
for (const key of ['default', 'provider']) {
|
|
160
|
+
const v = modelNode.get?.(key, true);
|
|
161
|
+
if (v && v.range) {
|
|
162
|
+
const raw = text.slice(v.range[0], v.range[1]);
|
|
163
|
+
if (key === 'default')
|
|
164
|
+
previous.defaultRaw = raw;
|
|
165
|
+
else
|
|
166
|
+
previous.providerRaw = raw;
|
|
167
|
+
edits.push({ start: v.range[0], end: v.range[1], text: want[key] });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
missing.push(key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (missing.length) {
|
|
174
|
+
const first = modelNode.items[0]?.key?.range?.[0];
|
|
175
|
+
if (first === undefined)
|
|
176
|
+
throw new Error('model: block in config.yaml is empty or inline; edit it by hand');
|
|
177
|
+
const pad = ' '.repeat(first - lineStart(text, first));
|
|
178
|
+
edits.push({ start: lineStart(text, first), end: lineStart(text, first), text: missing.map((k) => `${pad}${k}: ${want[k]}\n`).join('') });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw new Error('model: in config.yaml is not a block; edit it by hand');
|
|
183
|
+
}
|
|
184
|
+
const list = root?.get?.('custom_providers', true);
|
|
185
|
+
if (list === undefined || list === null) {
|
|
186
|
+
tail += `custom_providers:\n${entryLines(' ')}`;
|
|
187
|
+
}
|
|
188
|
+
else if (list.items && list.flow !== true) {
|
|
189
|
+
previous.hadList = true;
|
|
190
|
+
const mine = list.items.find((it) => it?.get?.('name') === 'wavespeed');
|
|
191
|
+
if (mine?.range) {
|
|
192
|
+
const { dash, pad } = dashPad(text, mine.range[0]);
|
|
193
|
+
const end = contentLineEnd(text, mine.range[1]);
|
|
194
|
+
previous.entryRaw = text.slice(lineStart(text, dash), end);
|
|
195
|
+
edits.push({ start: lineStart(text, dash), end, text: entryLines(pad) });
|
|
196
|
+
}
|
|
197
|
+
else if (list.items.length > 0) {
|
|
198
|
+
const last = list.items[list.items.length - 1];
|
|
199
|
+
const { pad } = dashPad(text, list.items[0].range[0]);
|
|
200
|
+
const at = contentLineEnd(text, last.range[1]);
|
|
201
|
+
edits.push({ start: at, end: at, text: entryLines(pad) });
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
throw new Error('custom_providers: in config.yaml is empty; edit it by hand');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (list.range && list.value === null) {
|
|
208
|
+
// `custom_providers:` with nothing under it
|
|
209
|
+
previous.hadList = false;
|
|
210
|
+
const at = lineEnd(text, list.range[0]);
|
|
211
|
+
edits.push({ start: at, end: at, text: entryLines(' ') });
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
throw new Error('custom_providers: in config.yaml is not a block list; edit it by hand');
|
|
215
|
+
}
|
|
216
|
+
return { text: applyEdits(text, edits) + tail, previous };
|
|
217
|
+
}
|
|
218
|
+
export function revertHermesYaml(input, previous) {
|
|
219
|
+
let text = input;
|
|
220
|
+
if (!previous.hadModelBlock) {
|
|
221
|
+
text = text.replace(/^model:\n {2}default: "[^\n]*"\n {2}provider: "wavespeed"\n/m, '');
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const doc = parseYaml(text);
|
|
225
|
+
const modelNode = doc.contents?.get?.('model', true);
|
|
226
|
+
const edits = [];
|
|
227
|
+
for (const [key, raw] of [['default', previous.defaultRaw], ['provider', previous.providerRaw]]) {
|
|
228
|
+
const v = modelNode?.get?.(key, true);
|
|
229
|
+
if (!v?.range)
|
|
230
|
+
continue;
|
|
231
|
+
if (raw !== undefined) {
|
|
232
|
+
edits.push({ start: v.range[0], end: v.range[1], text: raw });
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const pair = modelNode.items.find((p) => p.key?.value === key);
|
|
236
|
+
const ks = pair.key.range[0];
|
|
237
|
+
edits.push({ start: lineStart(text, ks), end: lineEnd(text, v.range[1]), text: '' });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
text = applyEdits(text, edits);
|
|
241
|
+
}
|
|
242
|
+
const doc = parseYaml(text);
|
|
243
|
+
const list = doc.contents?.get?.('custom_providers', true);
|
|
244
|
+
if (list?.items) {
|
|
245
|
+
const mine = list.items.find((it) => it?.get?.('name') === 'wavespeed');
|
|
246
|
+
if (mine?.range) {
|
|
247
|
+
const { dash } = dashPad(text, mine.range[0]);
|
|
248
|
+
const start = lineStart(text, dash);
|
|
249
|
+
const end = contentLineEnd(text, mine.range[1]);
|
|
250
|
+
text = text.slice(0, start) + (previous.entryRaw ?? '') + text.slice(end);
|
|
251
|
+
}
|
|
252
|
+
if (!previous.hadList) {
|
|
253
|
+
const again = parseYaml(text);
|
|
254
|
+
const l2 = again.contents?.get?.('custom_providers', true);
|
|
255
|
+
if (l2?.items?.length === 0 || (l2 && l2.items === undefined)) {
|
|
256
|
+
text = text.replace(/^custom_providers:[ \t]*\n?/m, '');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return text;
|
|
261
|
+
}
|
|
89
262
|
// ---------- Codex (TOML, edited as text so the user's comments survive) ----------
|
|
90
263
|
const TOP_OPEN = '# >>> wavespeed (managed by `wavespeed setup`) >>>';
|
|
91
264
|
const TOP_CLOSE = '# <<< wavespeed <<<';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wavespeed/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
4
4
|
"description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -54,9 +54,11 @@
|
|
|
54
54
|
"clipboardy": "^4.0.0",
|
|
55
55
|
"commander": "^12.1.0",
|
|
56
56
|
"conf": "^13.0.0",
|
|
57
|
+
"json5": "^2.2.3",
|
|
57
58
|
"open": "^10.1.0",
|
|
58
59
|
"ora": "^8.1.0",
|
|
59
|
-
"wavespeed": "^0.3.4"
|
|
60
|
+
"wavespeed": "^0.3.4",
|
|
61
|
+
"yaml": "^2.9.1"
|
|
60
62
|
},
|
|
61
63
|
"devDependencies": {
|
|
62
64
|
"@types/node": "^20.14.0",
|