@wavespeed/cli 0.4.18 → 0.4.19

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.
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import crypto from 'node:crypto';
2
3
  import fs from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
@@ -8,19 +9,31 @@ import JSON5 from 'json5';
8
9
  import { getApiKey, userConfig } from '../lib/config.js';
9
10
  import { clientAttributionHeaders } from '../lib/client-headers.js';
10
11
  import { emitJson, emitJsonError, info, setJsonMode } from '../lib/log.js';
11
- import { DEFAULT_MODEL, KEY_ENV, LLM_BASE_URL, applyCodexToml, applyHermesYaml, applyJson, applyYamlScalars, claudeEdits, miniAgentValues, openclawEdits, opencodeEdits, qwenEdits, revertCodexToml, revertHermesYaml, revertJson, revertYamlScalars, validateModel, } from '../lib/setup-agents.js';
12
+ import { DEFAULT_MODEL, KEY_ENV, LLM_BASE_URL, aiderValues, applyCodexToml, applyContinueYaml, applyDshPatch, applyHermesYaml, applyJson, applyYamlScalars, claudeEdits, crushEdits, droidEdits, gooseProviderFile, goosePlainValues, miniAgentValues, openclawEdits, opencodeEdits, qwenEdits, revertCodexToml, revertContinueYaml, revertDshPatch, revertHermesYaml, revertJson, revertYamlScalars, validateModel, } from '../lib/setup-agents.js';
12
13
  const home = os.homedir();
14
+ // Goose: ~/.config/goose on macOS and Linux, %APPDATA%\Block\goose\config on Windows (per its docs; Windows not tested here).
15
+ const GOOSE_DIR = process.platform === 'win32' ? path.join(process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming'), 'Block', 'goose', 'config') : path.join(home, '.config', 'goose');
16
+ const GOOSE_PROVIDER_FILE = path.join(GOOSE_DIR, 'custom_providers', 'wavespeed.json');
13
17
  const AGENTS = [
14
- { id: 'claude', name: 'Claude Code', file: path.join(home, '.claude', 'settings.json'), command: 'claude', kind: 'json', keyInFile: true },
15
18
  { id: 'codex', name: 'Codex', file: path.join(home, '.codex', 'config.toml'), command: 'codex', kind: 'toml', keyInFile: false },
19
+ { id: 'claude', name: 'Claude Code', file: path.join(home, '.claude', 'settings.json'), command: 'claude', kind: 'json', keyInFile: true },
16
20
  { id: 'opencode', name: 'OpenCode', file: path.join(home, '.config', 'opencode', 'opencode.json'), command: 'opencode', kind: 'json', keyInFile: false },
17
21
  { id: 'openclaw', name: 'OpenClaw', file: path.join(home, '.openclaw', 'openclaw.json'), command: 'openclaw', kind: 'json5', keyInFile: false },
18
22
  { id: 'hermes', name: 'Hermes', file: path.join(home, '.hermes', 'config.yaml'), command: 'hermes', kind: 'yaml', keyInFile: false },
23
+ { id: 'kilo', name: 'Kilo Code', file: path.join(home, '.config', 'kilo', 'kilo.json'), command: 'kilo', kind: 'json5', keyInFile: false },
19
24
  { id: 'qwen', name: 'Qwen Code', file: path.join(home, '.qwen', 'settings.json'), command: 'qwen', kind: 'json', keyInFile: false },
20
25
  { id: 'miniagent', name: 'Mini-Agent', file: path.join(home, '.mini-agent', 'config', 'config.yaml'), command: 'mini-agent', kind: 'yamlscalars', keyInFile: true },
26
+ { id: 'crush', name: 'Crush', file: path.join(home, '.config', 'crush', 'crush.json'), command: 'crush', kind: 'json', keyInFile: false },
27
+ { id: 'droid', name: 'Factory Droid', file: path.join(home, '.factory', 'settings.json'), command: 'droid', kind: 'json', keyInFile: false },
28
+ { id: 'aider', name: 'Aider', file: path.join(home, '.aider.conf.yml'), command: 'aider', kind: 'yamlscalars', keyInFile: true },
29
+ { id: 'continue', name: 'Continue CLI', file: path.join(home, '.continue', 'config.yaml'), command: 'cn', kind: 'yamlseq', keyInFile: false },
30
+ { id: 'goose', name: 'Goose', file: path.join(GOOSE_DIR, 'config.yaml'), command: 'goose', kind: 'goose', keyInFile: false },
31
+ { id: 'dsh', name: 'DeepSeek Harness', file: path.join(process.env.DSH_HOME || path.join(home, '.dsh'), 'cordis.patch.yml'), command: 'dsh', kind: 'dsh', keyInFile: false },
21
32
  // Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
22
33
  { id: 'cline', name: 'Cline', file: path.join(home, '.cline', 'data', 'settings', 'providers.json'), command: 'cline', kind: 'cline', keyInFile: true },
34
+ // Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
23
35
  ];
36
+ const sha256 = (data) => crypto.createHash('sha256').update(data).digest('hex');
24
37
  const shown = (p) => p.replace(home, '~');
25
38
  function onPath(cmd) {
26
39
  const exts = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
@@ -67,10 +80,11 @@ function readJson(file, json5 = false) {
67
80
  }
68
81
  throw new Error(`${shown(file)} is not plain JSON (comments or trailing commas?). Edit it by hand using the snippet at https://wavespeed.ai/llm/agents`);
69
82
  }
70
- function writeFile(file, content, secret) {
83
+ /** `keepOriginalBackup`: setup already ran for this agent, so an existing .bak is the true original. */
84
+ function writeFile(file, content, secret, keepOriginalBackup) {
71
85
  fs.mkdirSync(path.dirname(file), { recursive: true });
72
86
  const bak = `${file}.wavespeed.bak`;
73
- if (fs.existsSync(file) && !fs.existsSync(bak))
87
+ if (fs.existsSync(file) && (!keepOriginalBackup || !fs.existsSync(bak)))
74
88
  fs.copyFileSync(file, bak);
75
89
  fs.writeFileSync(file, content, secret ? { mode: 0o600 } : undefined);
76
90
  if (secret)
@@ -84,7 +98,7 @@ function envHint() {
84
98
  export function registerSetup(program) {
85
99
  program
86
100
  .command('setup [agent]')
87
- .description('Point a coding agent (claude, codex, opencode, openclaw, hermes, cline, qwen, miniagent) at the WaveSpeed LLM gateway')
101
+ .description('Point a coding agent (claude, codex, opencode, openclaw, hermes, cline, qwen, miniagent, kilo, droid, crush, aider, continue, goose, dsh) at the WaveSpeed LLM gateway')
88
102
  .option('--model <id>', `Model to use (default: ${DEFAULT_MODEL})`)
89
103
  .option('--dry-run', 'Show what would change without writing anything')
90
104
  .option('--undo', "Restore the agent's config to what it was before setup")
@@ -122,7 +136,29 @@ export function registerSetup(program) {
122
136
  delete rest[agent.id];
123
137
  userConfig.set('setup', rest);
124
138
  };
125
- if (agent.kind === 'toml') {
139
+ if (state && agent.kind !== 'cline' && !fs.existsSync(state.file)) {
140
+ if (!opts.dryRun)
141
+ forget();
142
+ info(chalk.yellow(`${shown(state.file)} is already gone; cleared the saved setup state.`));
143
+ return;
144
+ }
145
+ // Untouched since setup wrote it: put the original bytes back (or remove a file we created).
146
+ const exact = !!state?.hash && agent.kind !== 'cline' && agent.kind !== 'goose' &&
147
+ fs.existsSync(state.file) && sha256(fs.readFileSync(state.file)) === state.hash;
148
+ if (exact && state) {
149
+ const bak = `${state.file}.wavespeed.bak`;
150
+ if (!opts.dryRun) {
151
+ if (state.existed === false)
152
+ fs.rmSync(state.file, { force: true });
153
+ else if (fs.existsSync(bak)) {
154
+ fs.copyFileSync(bak, state.file);
155
+ fs.chmodSync(state.file, fs.statSync(bak).mode & 0o777);
156
+ }
157
+ fs.rmSync(bak, { force: true });
158
+ forget();
159
+ }
160
+ }
161
+ else if (agent.kind === 'toml') {
126
162
  if (!fs.existsSync(agent.file))
127
163
  return fail(`${shown(agent.file)} does not exist; nothing to undo`);
128
164
  const before = fs.readFileSync(agent.file, 'utf8');
@@ -154,6 +190,47 @@ export function registerSetup(program) {
154
190
  forget();
155
191
  }
156
192
  }
193
+ else if (agent.kind === 'yamlseq') {
194
+ const after = revertContinueYaml(fs.readFileSync(state.file, 'utf8'), state.previous);
195
+ if (!opts.dryRun) {
196
+ if (after.trim() === '')
197
+ fs.rmSync(state.file, { force: true });
198
+ else
199
+ fs.writeFileSync(state.file, after);
200
+ forget();
201
+ }
202
+ }
203
+ else if (agent.kind === 'dsh') {
204
+ const after = revertDshPatch(fs.readFileSync(state.file, 'utf8'));
205
+ if (!opts.dryRun) {
206
+ if (!state.previous.existed)
207
+ fs.rmSync(state.file, { force: true });
208
+ else
209
+ fs.writeFileSync(state.file, after);
210
+ forget();
211
+ }
212
+ }
213
+ else if (agent.kind === 'goose') {
214
+ const prev = state.previous;
215
+ const after = revertYamlScalars(fs.readFileSync(state.file, 'utf8'), prev.scalars);
216
+ if (!opts.dryRun) {
217
+ if (after.trim() === '')
218
+ fs.rmSync(state.file, { force: true });
219
+ else
220
+ fs.writeFileSync(state.file, after);
221
+ fs.rmSync(`${state.file}.wavespeed.bak`, { force: true });
222
+ if (prev.providerRaw !== null)
223
+ fs.writeFileSync(GOOSE_PROVIDER_FILE, prev.providerRaw);
224
+ else {
225
+ fs.rmSync(GOOSE_PROVIDER_FILE, { force: true });
226
+ try {
227
+ fs.rmdirSync(path.dirname(GOOSE_PROVIDER_FILE));
228
+ }
229
+ catch { /* not empty or already gone */ }
230
+ }
231
+ forget();
232
+ }
233
+ }
157
234
  else if (agent.kind === 'yaml') {
158
235
  const after = revertHermesYaml(fs.readFileSync(state.file, 'utf8'), state.previous);
159
236
  if (!opts.dryRun) {
@@ -201,10 +278,25 @@ export function registerSetup(program) {
201
278
  previous = r.previous;
202
279
  }
203
280
  else if (agent.kind === 'yamlscalars') {
204
- const r = applyYamlScalars(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', miniAgentValues(model, apiKey));
281
+ const values = agent.id === 'aider' ? aiderValues(model, apiKey) : miniAgentValues(model, apiKey);
282
+ const r = applyYamlScalars(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', values);
205
283
  next = r.text;
206
284
  previous = r.previous;
207
285
  }
286
+ else if (agent.kind === 'yamlseq') {
287
+ const r = applyContinueYaml(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', model);
288
+ next = r.text;
289
+ previous = r.previous;
290
+ }
291
+ else if (agent.kind === 'dsh') {
292
+ previous = { existed: fs.existsSync(agent.file) };
293
+ next = applyDshPatch(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', model);
294
+ }
295
+ else if (agent.kind === 'goose') {
296
+ const r = applyYamlScalars(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', goosePlainValues(model));
297
+ next = r.text;
298
+ previous = { scalars: r.previous, providerRaw: fs.existsSync(GOOSE_PROVIDER_FILE) ? fs.readFileSync(GOOSE_PROVIDER_FILE, 'utf8') : null };
299
+ }
208
300
  else if (agent.kind === 'cline') {
209
301
  previous = { existed: fs.existsSync(agent.file) };
210
302
  if (!onPath('cline'))
@@ -212,7 +304,7 @@ export function registerSetup(program) {
212
304
  }
213
305
  else {
214
306
  const base = readJson(agent.file, agent.kind === 'json5');
215
- const edits = agent.id === 'claude' ? claudeEdits(model, apiKey) : agent.id === 'openclaw' ? openclawEdits(model, base) : agent.id === 'qwen' ? qwenEdits(model) : opencodeEdits(model);
307
+ const edits = agent.id === 'claude' ? claudeEdits(model, apiKey) : agent.id === 'openclaw' ? openclawEdits(model, base) : agent.id === 'qwen' ? qwenEdits(model) : agent.id === 'crush' ? crushEdits(model) : agent.id === 'droid' ? droidEdits(model, base) : opencodeEdits(model);
216
308
  const r = applyJson(base, edits);
217
309
  previous = r.previous;
218
310
  next = JSON.stringify(r.doc, null, 2) + '\n';
@@ -225,7 +317,7 @@ export function registerSetup(program) {
225
317
  if (opts.dryRun) {
226
318
  if (opts.json)
227
319
  return emitJson({ agent: agent.id, model, file: agent.file, dry_run: true, will_create: !exists });
228
- info(chalk.bold(`Would ${exists ? 'update' : 'create'} ${shown(agent.file)}`) + chalk.gray(` (model ${model}${agent.keyInFile ? '; your API key would be stored in this file' : ''})`));
320
+ info(chalk.bold(`Would ${exists ? 'update' : 'create'} ${shown(agent.file)}${agent.kind === 'goose' ? ` and ${shown(GOOSE_PROVIDER_FILE)}` : ''}`) + chalk.gray(` (model ${model}${agent.keyInFile ? '; your API key would be stored in this file' : ''})`));
229
321
  info(chalk.gray('Nothing written.'));
230
322
  return;
231
323
  }
@@ -247,12 +339,21 @@ export function registerSetup(program) {
247
339
  }
248
340
  else {
249
341
  const hadComments = agent.kind === 'json5' && exists && /^\s*\/\/|\/\*/m.test(fs.readFileSync(agent.file, 'utf8'));
250
- writeFile(agent.file, next, agent.keyInFile);
342
+ writeFile(agent.file, next, agent.keyInFile, !!userConfig.get('setup')?.[agent.id]);
343
+ if (agent.kind === 'goose') {
344
+ fs.mkdirSync(path.dirname(GOOSE_PROVIDER_FILE), { recursive: true });
345
+ fs.writeFileSync(GOOSE_PROVIDER_FILE, gooseProviderFile(model));
346
+ }
251
347
  if (hadComments)
252
348
  info(chalk.yellow(`Comments in ${shown(agent.file)} were not kept; the original is saved as ${shown(agent.file)}.wavespeed.bak`));
253
349
  }
254
- if (agent.kind !== 'toml')
255
- userConfig.set('setup', { ...(userConfig.get('setup') ?? {}), [agent.id]: { file: agent.file, previous } });
350
+ {
351
+ const prior = userConfig.get('setup')?.[agent.id];
352
+ const record = { file: agent.file, previous, hash: agent.kind === 'cline' ? undefined : sha256(next), existed: prior ? prior.existed : exists };
353
+ // Re-running setup on an already configured agent keeps the first run's undo data.
354
+ const keep = prior && agent.kind !== 'toml' && agent.kind !== 'cline' ? { ...record, previous: prior.previous } : record;
355
+ userConfig.set('setup', { ...(userConfig.get('setup') ?? {}), [agent.id]: keep });
356
+ }
256
357
  const check = opts.check ? await selfCheck(apiKey, model) : null;
257
358
  if (opts.json) {
258
359
  emitJson({ agent: agent.id, model, file: agent.file, written: true, check: check ? { ok: check.ok, detail: check.detail } : 'skipped', needs_env: agent.keyInFile ? null : KEY_ENV });
@@ -260,7 +361,7 @@ export function registerSetup(program) {
260
361
  process.exit(1);
261
362
  return;
262
363
  }
263
- info(chalk.green(`Updated ${shown(agent.file)}`) + chalk.gray(` (model ${model})`));
364
+ info(chalk.green(`${exists ? 'Updated' : 'Created'} ${shown(agent.file)}`) + chalk.gray(` (model ${model})`));
264
365
  if (check)
265
366
  info(check.ok ? chalk.green(`Verified: streamed reply from ${model} (${check.detail}).`) : chalk.red(`Check failed: ${check.detail}`));
266
367
  if (!agent.keyInFile) {
@@ -16,13 +16,17 @@ import { parseDocument } from 'yaml';
16
16
  function isObj(v) {
17
17
  return typeof v === 'object' && v !== null && !Array.isArray(v);
18
18
  }
19
+ /** Objects and arrays can both hold a path segment (array elements are addressed by index). */
20
+ function isContainer(v) {
21
+ return typeof v === 'object' && v !== null;
22
+ }
19
23
  /** Set each path, remembering what was there so `revertJson` can put it back. */
20
24
  export function applyJson(doc, edits) {
21
25
  const previous = [];
22
26
  for (const { path, value } of edits) {
23
27
  let cur = doc;
24
28
  for (const key of path.slice(0, -1)) {
25
- if (!isObj(cur[key]))
29
+ if (!isContainer(cur[key]))
26
30
  cur[key] = {};
27
31
  cur = cur[key];
28
32
  }
@@ -38,8 +42,8 @@ export function revertJson(doc, previous) {
38
42
  const parents = [doc];
39
43
  let cur = doc;
40
44
  for (const key of path.slice(0, -1)) {
41
- cur = isObj(cur) ? cur[key] : undefined;
42
- if (!isObj(cur))
45
+ cur = isContainer(cur) ? cur[key] : undefined;
46
+ if (!isContainer(cur))
43
47
  break;
44
48
  parents.push(cur);
45
49
  }
@@ -49,6 +53,8 @@ export function revertJson(doc, previous) {
49
53
  const last = path[path.length - 1];
50
54
  if (existed)
51
55
  holder[last] = value;
56
+ else if (Array.isArray(holder))
57
+ holder.splice(Number(last), 1);
52
58
  else
53
59
  delete holder[last];
54
60
  // Drop containers we created and that are now empty.
@@ -118,6 +124,69 @@ export function qwenEdits(model) {
118
124
  { path: ['model', 'name'], value: m },
119
125
  ];
120
126
  }
127
+ export function crushEdits(model) {
128
+ const m = validateModel(model);
129
+ const sel = { provider: 'wavespeed', model: m };
130
+ return [
131
+ {
132
+ path: ['providers', 'wavespeed'],
133
+ value: {
134
+ name: 'WaveSpeed',
135
+ type: 'openai-compat',
136
+ base_url: LLM_BASE_URL,
137
+ api_key: `$${KEY_ENV}`,
138
+ models: [{ id: m, name: m, context_window: 128000, default_max_tokens: 16000 }],
139
+ },
140
+ },
141
+ { path: ['models', 'large'], value: sel },
142
+ { path: ['models', 'small'], value: { ...sel } },
143
+ ];
144
+ }
145
+ /**
146
+ * Factory Droid keeps custom models in one array that also holds the user's other
147
+ * providers (and their keys): touch only our own element so the saved undo state
148
+ * never copies anyone else's entry.
149
+ */
150
+ export function droidEdits(model, doc) {
151
+ const m = validateModel(model);
152
+ const existing = Array.isArray(doc.customModels) ? doc.customModels : [];
153
+ const at = existing.findIndex((e) => isObj(e) && e.baseUrl === LLM_BASE_URL);
154
+ const ours = {
155
+ model: m,
156
+ displayName: `WaveSpeed ${m}`,
157
+ baseUrl: LLM_BASE_URL,
158
+ apiKey: `\${${KEY_ENV}}`,
159
+ provider: 'generic-chat-completion-api',
160
+ maxOutputTokens: 16000,
161
+ };
162
+ return [
163
+ { path: ['customModels', String(at >= 0 ? at : existing.length)], value: ours },
164
+ // Without a default, `droid exec` asks for a Factory login unless -m custom:<model> is given.
165
+ { path: ['sessionDefaultSettings', 'model'], value: `custom:${m}` },
166
+ ];
167
+ }
168
+ // ---------- Goose (custom provider file + two scalars in config.yaml) ----------
169
+ export function goosePlainValues(model) {
170
+ return { GOOSE_PROVIDER: 'wavespeed', GOOSE_MODEL: validateModel(model) };
171
+ }
172
+ export function gooseProviderFile(model) {
173
+ const m = validateModel(model);
174
+ return (JSON.stringify({
175
+ name: 'wavespeed',
176
+ engine: 'openai',
177
+ display_name: 'WaveSpeed',
178
+ description: 'WaveSpeed LLM gateway',
179
+ api_key_env: KEY_ENV,
180
+ base_url: `${LLM_BASE_URL}/chat/completions`,
181
+ models: [{ name: m, context_limit: 128000 }],
182
+ supports_streaming: true,
183
+ }, null, 2) + '\n');
184
+ }
185
+ // ---------- Aider (flat YAML scalars; the key lives in the file) ----------
186
+ export function aiderValues(model, apiKey) {
187
+ // litellm needs the openai/ prefix to treat the gateway as a generic OpenAI-compatible endpoint.
188
+ return { 'openai-api-base': LLM_BASE_URL, 'openai-api-key': apiKey, model: `openai/${validateModel(model)}` };
189
+ }
121
190
  // ---------- Mini-Agent (flat YAML scalars; the key lives in the file) ----------
122
191
  export function miniAgentValues(model, apiKey) {
123
192
  return { api_key: apiKey, api_base: LLM_BASE_URL, model: validateModel(model), provider: 'openai' };
@@ -319,6 +388,134 @@ export function revertHermesYaml(input, previous) {
319
388
  }
320
389
  return text;
321
390
  }
391
+ export function continueEntry(model, pad) {
392
+ const m = validateModel(model);
393
+ return [
394
+ `${pad}- name: ${JSON.stringify(`WaveSpeed ${m}`)}`,
395
+ `${pad} provider: openai`,
396
+ `${pad} model: ${JSON.stringify(m)}`,
397
+ `${pad} apiBase: ${LLM_BASE_URL}`,
398
+ `${pad} apiKey: "\${{ secrets.${KEY_ENV} }}"`,
399
+ `${pad} roles:`,
400
+ `${pad} - chat`,
401
+ `${pad} defaultCompletionOptions:`,
402
+ `${pad} contextLength: 128000`,
403
+ `${pad} maxTokens: 16000`,
404
+ '',
405
+ ].join('\n');
406
+ }
407
+ function isOurContinueEntry(it) {
408
+ return typeof it?.get === 'function' && it.get('apiBase') === LLM_BASE_URL && it.get('provider') === 'openai';
409
+ }
410
+ export function applyContinueYaml(input, model) {
411
+ validateModel(model);
412
+ const created = input.trim() === '';
413
+ let text = input;
414
+ if (created) {
415
+ text = 'name: WaveSpeed\nversion: 1.0.0\nschema: v1\n';
416
+ }
417
+ else if (!text.endsWith('\n')) {
418
+ text += '\n';
419
+ }
420
+ const doc = parseYaml(text);
421
+ const root = doc.contents;
422
+ const previous = { created, hadKey: false };
423
+ const list = root?.get?.('models', true);
424
+ if (list === undefined || list === null) {
425
+ return { text: `${text}models:\n${continueEntry(model, ' ')}`, previous };
426
+ }
427
+ if (!list.items || list.flow === true)
428
+ throw new Error('models: in config.yaml is not a block list; edit it by hand');
429
+ previous.hadKey = true;
430
+ const mine = list.items.find(isOurContinueEntry);
431
+ if (mine?.range) {
432
+ const { dash, pad } = dashPad(text, mine.range[0]);
433
+ const end = contentLineEnd(text, mine.range[1]);
434
+ previous.entryRaw = text.slice(lineStart(text, dash), end);
435
+ return { text: text.slice(0, lineStart(text, dash)) + continueEntry(model, pad) + text.slice(end), previous };
436
+ }
437
+ if (list.items.length === 0)
438
+ throw new Error('models: in config.yaml is empty; edit it by hand');
439
+ // First in the list: Continue's CLI has no default-model key and uses the first chat-role entry.
440
+ const { dash, pad } = dashPad(text, list.items[0].range[0]);
441
+ const at = lineStart(text, dash);
442
+ return { text: text.slice(0, at) + continueEntry(model, pad) + text.slice(at), previous };
443
+ }
444
+ export function revertContinueYaml(input, previous) {
445
+ let text = input;
446
+ const doc = parseYaml(text);
447
+ const list = doc.contents?.get?.('models', true);
448
+ const mine = list?.items?.find(isOurContinueEntry);
449
+ if (mine?.range) {
450
+ const { dash } = dashPad(text, mine.range[0]);
451
+ const start = lineStart(text, dash);
452
+ text = text.slice(0, start) + (previous.entryRaw ?? '') + text.slice(contentLineEnd(text, mine.range[1]));
453
+ }
454
+ if (!previous.hadKey) {
455
+ const again = parseYaml(text);
456
+ const l2 = again.contents?.get?.('models', true);
457
+ if (l2 && (l2.items === undefined || l2.items.length === 0))
458
+ text = text.replace(/^models:[ \t]*\n?/m, '');
459
+ }
460
+ if (previous.created && text.trim() === 'name: WaveSpeed\nversion: 1.0.0\nschema: v1')
461
+ return '';
462
+ return text;
463
+ }
464
+ // ---------- DeepSeek Harness (user patch layer; a managed block in cordis.patch.yml) ----------
465
+ const DSH_OPEN = '# >>> wavespeed (managed by `wavespeed setup`) >>>';
466
+ const DSH_CLOSE = '# <<< wavespeed <<<';
467
+ export function dshBlock(model) {
468
+ const m = validateModel(model);
469
+ return [
470
+ DSH_OPEN,
471
+ '- id: llm-pi-ai',
472
+ ' config:',
473
+ ' providers:',
474
+ ' wavespeed:',
475
+ ` apiKeyEnv: ${KEY_ENV}`,
476
+ ' api: openai-completions',
477
+ ` baseURL: ${LLM_BASE_URL}`,
478
+ ' models:',
479
+ ` - id: ${m}`,
480
+ '- id: agent-default-model',
481
+ ' config:',
482
+ ' provider: wavespeed',
483
+ ` model: ${m}`,
484
+ DSH_CLOSE,
485
+ '',
486
+ ].join('\n');
487
+ }
488
+ export function revertDshPatch(text) {
489
+ const out = [];
490
+ let skipping = false;
491
+ for (const line of text.split('\n')) {
492
+ if (line.trim() === DSH_OPEN) {
493
+ skipping = true;
494
+ continue;
495
+ }
496
+ if (skipping) {
497
+ if (line.trim() === DSH_CLOSE)
498
+ skipping = false;
499
+ continue;
500
+ }
501
+ out.push(line);
502
+ }
503
+ const rest = out.join('\n').replace(/\n{3,}/g, '\n\n');
504
+ if (/^\s*-\s+id:/m.test(rest))
505
+ return rest.replace(/\n+$/, '\n');
506
+ // No entries left: dsh's own file is an empty list (plus any comments).
507
+ const noEmpty = rest.replace(/^\s*\[\]\s*$/m, '').replace(/\n+$/, '');
508
+ return (noEmpty ? noEmpty + '\n' : '') + '[]\n';
509
+ }
510
+ export function applyDshPatch(input, model) {
511
+ const base = revertDshPatch(input);
512
+ if (/^\s*-\s+id:\s*(llm-pi-ai|agent-default-model)\b/m.test(base)) {
513
+ throw new Error('cordis.patch.yml already patches llm-pi-ai or agent-default-model; merge the WaveSpeed provider by hand');
514
+ }
515
+ const withoutEmpty = base.replace(/^\s*\[\]\s*$/m, '').replace(/\n{3,}/g, '\n\n');
516
+ const head = withoutEmpty.trim() === '' ? '' : withoutEmpty.replace(/\n*$/, '\n\n');
517
+ return head + dshBlock(model);
518
+ }
322
519
  // ---------- Codex (TOML, edited as text so the user's comments survive) ----------
323
520
  const TOP_OPEN = '# >>> wavespeed (managed by `wavespeed setup`) >>>';
324
521
  const TOP_CLOSE = '# <<< wavespeed <<<';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/cli",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"