@wavespeed/cli 0.4.19 → 0.4.21

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 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 [agent] Point claude, codex, opencode, openclaw, hermes, cline, qwen or miniagent at the WaveSpeed LLM gateway (--dry-run, --undo, --model)
144
+ wavespeed setup [agent] Point a coding agent (claude, codex, gemini, opencode, openclaw, hermes, kilo, cline, qwen, droid, crush, aider, continue, goose, miniagent, dsh) 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)
@@ -9,10 +9,11 @@ import JSON5 from 'json5';
9
9
  import { getApiKey, userConfig } from '../lib/config.js';
10
10
  import { clientAttributionHeaders } from '../lib/client-headers.js';
11
11
  import { emitJson, emitJsonError, info, setJsonMode } from '../lib/log.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
+ import { DEFAULT_MODEL, KEY_ENV, LLM_BASE_URL, aiderValues, applyCodexToml, applyContinueYaml, applyDshPatch, applyHermesYaml, applyGeminiEnv, applyJson, applyYamlScalars, claudeEdits, crushEdits, droidEdits, geminiEdits, gooseProviderFile, goosePlainValues, miniAgentValues, openclawEdits, opencodeEdits, qwenEdits, revertCodexToml, revertContinueYaml, revertDshPatch, revertGeminiEnv, revertHermesYaml, revertJson, revertYamlScalars, validateModel, } from '../lib/setup-agents.js';
13
13
  const home = os.homedir();
14
14
  // Goose: ~/.config/goose on macOS and Linux, %APPDATA%\Block\goose\config on Windows (per its docs; Windows not tested here).
15
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 GEMINI_SETTINGS_FILE = path.join(home, '.gemini', 'settings.json');
16
17
  const GOOSE_PROVIDER_FILE = path.join(GOOSE_DIR, 'custom_providers', 'wavespeed.json');
17
18
  const AGENTS = [
18
19
  { id: 'codex', name: 'Codex', file: path.join(home, '.codex', 'config.toml'), command: 'codex', kind: 'toml', keyInFile: false },
@@ -29,6 +30,8 @@ const AGENTS = [
29
30
  { id: 'continue', name: 'Continue CLI', file: path.join(home, '.continue', 'config.yaml'), command: 'cn', kind: 'yamlseq', keyInFile: false },
30
31
  { id: 'goose', name: 'Goose', file: path.join(GOOSE_DIR, 'config.yaml'), command: 'goose', kind: 'goose', keyInFile: false },
31
32
  { 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 },
33
+ // Gemini CLI reads the base URL and key from ~/.gemini/.env (trusted folders only); the model and auth type live in settings.json.
34
+ { id: 'gemini', name: 'Gemini CLI', file: path.join(home, '.gemini', '.env'), command: 'gemini', kind: 'gemini', keyInFile: true },
32
35
  // Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
33
36
  { id: 'cline', name: 'Cline', file: path.join(home, '.cline', 'data', 'settings', 'providers.json'), command: 'cline', kind: 'cline', keyInFile: true },
34
37
  // Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
@@ -98,7 +101,7 @@ function envHint() {
98
101
  export function registerSetup(program) {
99
102
  program
100
103
  .command('setup [agent]')
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')
104
+ .description('Point a coding agent (claude, codex, opencode, openclaw, hermes, cline, qwen, miniagent, kilo, droid, crush, aider, continue, goose, dsh, gemini) at the WaveSpeed LLM gateway')
102
105
  .option('--model <id>', `Model to use (default: ${DEFAULT_MODEL})`)
103
106
  .option('--dry-run', 'Show what would change without writing anything')
104
107
  .option('--undo', "Restore the agent's config to what it was before setup")
@@ -143,7 +146,7 @@ export function registerSetup(program) {
143
146
  return;
144
147
  }
145
148
  // 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' &&
149
+ const exact = !!state?.hash && agent.kind !== 'cline' && agent.kind !== 'goose' && agent.kind !== 'gemini' &&
147
150
  fs.existsSync(state.file) && sha256(fs.readFileSync(state.file)) === state.hash;
148
151
  if (exact && state) {
149
152
  const bak = `${state.file}.wavespeed.bak`;
@@ -210,6 +213,26 @@ export function registerSetup(program) {
210
213
  forget();
211
214
  }
212
215
  }
216
+ else if (agent.kind === 'gemini') {
217
+ const prev = state.previous;
218
+ const envAfter = revertGeminiEnv(fs.readFileSync(state.file, 'utf8'));
219
+ if (!opts.dryRun) {
220
+ if (envAfter.trim() === '' && !state.existed)
221
+ fs.rmSync(state.file, { force: true });
222
+ else
223
+ fs.writeFileSync(state.file, envAfter);
224
+ fs.rmSync(`${state.file}.wavespeed.bak`, { force: true });
225
+ if (fs.existsSync(GEMINI_SETTINGS_FILE)) {
226
+ const doc = revertJson(readJson(GEMINI_SETTINGS_FILE, false), prev.settings);
227
+ if (!prev.settingsExisted && Object.keys(doc).length === 0)
228
+ fs.rmSync(GEMINI_SETTINGS_FILE, { force: true });
229
+ else
230
+ fs.writeFileSync(GEMINI_SETTINGS_FILE, JSON.stringify(doc, null, 2) + '\n');
231
+ }
232
+ fs.rmSync(`${GEMINI_SETTINGS_FILE}.wavespeed.bak`, { force: true });
233
+ forget();
234
+ }
235
+ }
213
236
  else if (agent.kind === 'goose') {
214
237
  const prev = state.previous;
215
238
  const after = revertYamlScalars(fs.readFileSync(state.file, 'utf8'), prev.scalars);
@@ -268,6 +291,7 @@ export function registerSetup(program) {
268
291
  }
269
292
  let next = '';
270
293
  let previous = [];
294
+ let geminiSettings = '';
271
295
  try {
272
296
  if (agent.kind === 'toml') {
273
297
  next = applyCodexToml(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', model);
@@ -297,6 +321,12 @@ export function registerSetup(program) {
297
321
  next = r.text;
298
322
  previous = { scalars: r.previous, providerRaw: fs.existsSync(GOOSE_PROVIDER_FILE) ? fs.readFileSync(GOOSE_PROVIDER_FILE, 'utf8') : null };
299
323
  }
324
+ else if (agent.kind === 'gemini') {
325
+ const r = applyJson(readJson(GEMINI_SETTINGS_FILE, false), geminiEdits(model));
326
+ geminiSettings = JSON.stringify(r.doc, null, 2) + '\n';
327
+ previous = { settings: r.previous, settingsExisted: fs.existsSync(GEMINI_SETTINGS_FILE) };
328
+ next = applyGeminiEnv(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', apiKey);
329
+ }
300
330
  else if (agent.kind === 'cline') {
301
331
  previous = { existed: fs.existsSync(agent.file) };
302
332
  if (!onPath('cline'))
@@ -317,7 +347,7 @@ export function registerSetup(program) {
317
347
  if (opts.dryRun) {
318
348
  if (opts.json)
319
349
  return emitJson({ agent: agent.id, model, file: agent.file, dry_run: true, will_create: !exists });
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' : ''})`));
350
+ info(chalk.bold(`Would ${exists ? 'update' : 'create'} ${shown(agent.file)}${agent.kind === 'goose' ? ` and ${shown(GOOSE_PROVIDER_FILE)}` : agent.kind === 'gemini' ? ` and ${shown(GEMINI_SETTINGS_FILE)}` : ''}`) + chalk.gray(` (model ${model}${agent.keyInFile ? '; your API key would be stored in this file' : ''})`));
321
351
  info(chalk.gray('Nothing written.'));
322
352
  return;
323
353
  }
@@ -340,6 +370,12 @@ export function registerSetup(program) {
340
370
  else {
341
371
  const hadComments = agent.kind === 'json5' && exists && /^\s*\/\/|\/\*/m.test(fs.readFileSync(agent.file, 'utf8'));
342
372
  writeFile(agent.file, next, agent.keyInFile, !!userConfig.get('setup')?.[agent.id]);
373
+ if (agent.kind === 'gemini') {
374
+ const bak = `${GEMINI_SETTINGS_FILE}.wavespeed.bak`;
375
+ if (fs.existsSync(GEMINI_SETTINGS_FILE) && !fs.existsSync(bak))
376
+ fs.copyFileSync(GEMINI_SETTINGS_FILE, bak);
377
+ fs.writeFileSync(GEMINI_SETTINGS_FILE, geminiSettings);
378
+ }
343
379
  if (agent.kind === 'goose') {
344
380
  fs.mkdirSync(path.dirname(GOOSE_PROVIDER_FILE), { recursive: true });
345
381
  fs.writeFileSync(GOOSE_PROVIDER_FILE, gooseProviderFile(model));
@@ -367,6 +403,9 @@ export function registerSetup(program) {
367
403
  if (!agent.keyInFile) {
368
404
  info(`\nThe key is read from ${chalk.bold(KEY_ENV)}; set it in the shell that launches ${agent.command}:\n ${chalk.cyan(envHint())}\n Keys: https://wavespeed.ai/accesskey`);
369
405
  }
406
+ if (agent.kind === 'gemini') {
407
+ info(chalk.yellow('\nGemini CLI reads ~/.gemini/.env only in folders it trusts: accept the trust prompt on first start, or set GEMINI_CLI_TRUST_WORKSPACE=true for scripts and CI.'));
408
+ }
370
409
  info(`\nStart it with ${chalk.cyan(agent.command)}. Revert with ${chalk.cyan(`wavespeed setup ${agent.id} --undo`)}.`);
371
410
  if (check && !check.ok)
372
411
  process.exit(1);
@@ -576,3 +576,38 @@ export function applyCodexToml(text, model) {
576
576
  const body = edited.join('\n').replace(/\n+$/, '');
577
577
  return [...top, body, ...tail].join('\n').replace(/\n{3,}/g, '\n\n');
578
578
  }
579
+ // ---------- Gemini CLI (managed block in ~/.gemini/.env + two keys in settings.json) ----------
580
+ const ENV_OPEN = '# >>> wavespeed (managed by `wavespeed setup`) >>>';
581
+ const ENV_CLOSE = '# <<< wavespeed <<<';
582
+ export function geminiEdits(model) {
583
+ const m = validateModel(model);
584
+ return [
585
+ { path: ['security', 'auth', 'selectedType'], value: 'gemini-api-key' },
586
+ { path: ['model', 'name'], value: m },
587
+ ];
588
+ }
589
+ export function revertGeminiEnv(text) {
590
+ const out = [];
591
+ let skipping = false;
592
+ for (const line of text.split('\n')) {
593
+ if (line.trim() === ENV_OPEN) {
594
+ skipping = true;
595
+ continue;
596
+ }
597
+ if (skipping) {
598
+ if (line.trim() === ENV_CLOSE)
599
+ skipping = false;
600
+ continue;
601
+ }
602
+ out.push(line);
603
+ }
604
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '\n').replace(/^\n$/, '');
605
+ }
606
+ /** dotenv lets the last duplicate win, so the managed block goes at the end. */
607
+ export function applyGeminiEnv(input, apiKey) {
608
+ if (/[\r\n]/.test(apiKey))
609
+ throw new Error('API key contains a line break');
610
+ const base = revertGeminiEnv(input);
611
+ const head = base.trim() === '' ? '' : base.replace(/\n*$/, '\n\n');
612
+ return head + [ENV_OPEN, `GOOGLE_GEMINI_BASE_URL=${CLAUDE_BASE_URL}`, `GEMINI_API_KEY=${apiKey}`, ENV_CLOSE, ''].join('\n');
613
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/cli",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/skills/SKILL.md CHANGED
@@ -77,4 +77,5 @@ The CLI never modifies the user's prompt or inputs. The single exception is expl
77
77
  - Don't invent model IDs. Always confirm via `wavespeed models` or `wavespeed schema <id>` before running.
78
78
  - Use `--json` on every `run` so you can read `outputs[0]` programmatically.
79
79
  - `wavespeed delete` requires `--yes` when run non-interactively (that includes you).
80
- - Spend questions: `wavespeed usage` (totals, per-model) and `wavespeed billings` (per-charge records).
80
+ - Cost and spend: `wavespeed price <id> -i k=v --json` estimates a run without generating or charging; `wavespeed balance` shows remaining credit; `wavespeed usage` (totals, per-model) and `wavespeed billings` (per-charge records) show what was spent.
81
+ - A `run` that fails with a gateway or network error (502/503/504, connection reset, timeout) does NOT mean the task failed, and the submit request is never retried automatically, so re-running can create a duplicate paid task. Do not re-run immediately. If the error contains a task id (`task_id` / prediction id), keep checking it with `wavespeed show <id> --json` until its status is terminal (`completed`, `failed`, `cancelled`, `timeout`, `deleted`). If no id was returned, check recent predictions with `wavespeed history --json` (add `--model <id>`; default window is the last 24h) and re-run once only if none matches. A client-side wait timeout or an interrupted `run` does not mean the task failed: tasks can run for a long time, so do not give up after a fixed time.