agentvibes 5.7.7 → 5.10.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 (46) hide show
  1. package/.agentvibes/config.json +0 -2
  2. package/.claude/config/audio-effects.cfg +4 -4
  3. package/.claude/config/background-music-enabled.txt +1 -0
  4. package/.claude/github-star-reminder.txt +1 -1
  5. package/.claude/hooks/play-tts-piper.sh +20 -13
  6. package/.claude/hooks/play-tts-ssh-remote.sh +2 -2
  7. package/.claude/hooks/voice-manager.sh +6 -0
  8. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  9. package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
  10. package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
  11. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
  12. package/.mcp.json +13 -9
  13. package/README.md +33 -2
  14. package/RELEASE_NOTES.md +80 -0
  15. package/mcp-server/server.py +17 -7
  16. package/package.json +2 -2
  17. package/src/commands/install-mcp.js +270 -16
  18. package/src/console/app.js +3 -3
  19. package/src/console/audio-env.js +4 -1
  20. package/src/console/tabs/agents-tab.js +89 -66
  21. package/src/console/tabs/music-tab.js +4 -3
  22. package/src/console/tabs/receiver-tab.js +13 -13
  23. package/src/console/tabs/settings-tab.js +2 -2
  24. package/src/console/tabs/setup-tab.js +291 -47
  25. package/src/console/tabs/voices-tab.js +17 -5
  26. package/src/console/widgets/personality-picker.js +2 -2
  27. package/src/console/widgets/reverb-picker.js +1 -1
  28. package/src/installer.js +32 -27
  29. package/src/services/provider-service.js +1 -1
  30. package/src/services/tts-engine-service.js +2 -2
  31. package/src/utils/audio-duration-validator.js +2 -2
  32. package/src/utils/list-formatter.js +9 -3
  33. package/src/utils/platform-resolver.js +369 -0
  34. package/src/utils/provider-validator.js +9 -9
  35. package/.agentvibes/install-manifest.json +0 -442
  36. package/.claude/config/background-music-position.txt +0 -27
  37. package/.claude/config/background-music-volume.txt +0 -1
  38. package/.claude/config/background-music.cfg +0 -1
  39. package/.claude/config/background-music.txt +0 -1
  40. package/.claude/config/reverb-level.txt +0 -1
  41. package/.claude/config/tts-speech-rate.txt +0 -1
  42. package/.claude/config/tts-verbosity.txt +0 -1
  43. package/.claude/hooks/bmad-party-manager.sh +0 -225
  44. package/.claude/hooks/stop.sh +0 -38
  45. package/.claude/piper-voices-dir.txt +0 -1
  46. /package/.claude/audio/tracks/{CelestialVelvet.mp3 → celestial_velvet.mp3} +0 -0
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # AgentVibes Release Notes
2
2
 
3
+ ## šŸ”§ v5.9.0 — SSH Remote + Windows Home Directory Fixes
4
+
5
+ **Released:** 2026-05-18
6
+
7
+ ### šŸ› SSH Remote: Connection Timeout Added
8
+
9
+ The SSH remote transport could hang indefinitely if the remote host was unreachable or
10
+ slow to respond. A `ConnectTimeout=10` option is now applied to all SSH connections, so
11
+ a stuck session surfaces an error within 10 seconds instead of blocking forever.
12
+
13
+ The SSH subshell structure was also cleaned up so the process exit code is reliably
14
+ captured — a previous formatting issue could cause `wait` to report "pid N is not a
15
+ child of this shell" in some shell environments.
16
+
17
+ ### šŸ› Windows: Home Directory Detection Fixed
18
+
19
+ `detectRemoteLlm()` used `process.env.HOME || os.homedir()` to find the AgentVibes
20
+ config directory. On Windows, `HOME` is typically unset, but `||` would fall through to
21
+ `os.homedir()` correctly — however `??` (null-coalescing) is strictly safer since it
22
+ only falls back on `null`/`undefined`, not on an empty string. The fix also adds test
23
+ injectability: passing a fake `HOME` in tests now reliably overrides the system value on
24
+ all platforms.
25
+
26
+ ---
27
+
28
+ ## šŸŽø v5.8.0 — Soprano Now Works + Voice Picker Fixed for All Engines
29
+
30
+ **Released:** 2026-05-18
31
+
32
+ ### šŸ› Soprano TTS Was Broken — Now Fixed
33
+
34
+ Soprano (our 80M-parameter neural TTS engine, introduced in v5.6) was silently failing on
35
+ Windows. Several issues combined to break it end-to-end:
36
+
37
+ - The Windows voice picker showed Soprano as an option but launched it with the wrong binary
38
+ name (`soprano-tts` instead of `soprano`)
39
+ - `play-tts-soprano.ps1` was called from Node.js with a stripped PATH, so the `soprano`
40
+ and `soprano-webui` executables couldn't be found even when installed
41
+ - The wav file path was written to PowerShell's Information stream (`Write-Host`) instead
42
+ of stdout, so the reverb/background-music processor couldn't find it and exited with an error
43
+ - The Gradio WebUI was never auto-started — you had to manually run `soprano-webui` before
44
+ every session
45
+
46
+ All of these are now fixed. AgentVibes auto-detects whether the Soprano WebUI server is
47
+ running on port 7860, starts it if not, and polls until it's ready (up to 90 seconds).
48
+ Three modes work in priority order: WebUI (fastest — model stays loaded) → OpenAI-compatible
49
+ API → direct `soprano` CLI.
50
+
51
+ ### šŸ› Voice Picker Ignored Windows SAPI and macOS Say
52
+
53
+ When opening the voice picker for an LLM configured to use **Windows SAPI** or **macOS Say**,
54
+ the picker displayed the full list of Piper voices instead of the engine's built-in voice.
55
+ This was confusing — selecting a Piper voice while using SAPI or macOS Say had no effect,
56
+ and the Space-bar preview played through the wrong engine.
57
+
58
+ The picker now adapts to whichever engine is selected:
59
+
60
+ - **Windows SAPI / macOS Say / Soprano:** shows exactly one item (the engine's built-in voice),
61
+ auto-selects it, and the Space-bar preview speaks through the correct engine binary
62
+ - **Piper:** shows the full installed-voice catalog as before
63
+
64
+ Additionally, saving the config no longer silently overwrites the `ttsEngine` field to `piper`
65
+ when a native engine is in use.
66
+
67
+ ### šŸ”’ Soprano Reliability (9 Adversarial-Review Fixes)
68
+
69
+ - **Crash fix:** socket `destroy()` could emit a late `error` event with no listener,
70
+ crashing the Node.js process — an absorber handler is now in place
71
+ - **Loop cancellation:** the 90-second WebUI polling loop now stops immediately when
72
+ the modal or voice picker is closed (via AbortController)
73
+ - **No unhandled rejections:** `.catch()` handlers added to all async WebUI-check calls
74
+ - **No duplicate processes:** a 10-second cooldown prevents spawning two `soprano-webui`
75
+ instances when Preview is clicked rapidly
76
+ - **Better error feedback:** spawn failures and non-zero exit codes now surface a visible
77
+ error label in the voice picker instead of silently resetting
78
+ - **PATH preserved:** the PowerShell PATH refresh now appends registry entries rather than
79
+ replacing the whole PATH, so nvm, conda, and pyenv shims continue to work
80
+
81
+ ---
82
+
3
83
  ## šŸŽ­ v5.7.7 — Party Mode Voice Restore + Polish
4
84
 
5
85
  **Released:** 2026-05-17
@@ -71,6 +71,7 @@ class AgentVibesServer:
71
71
  """Initialize the AgentVibes MCP server"""
72
72
  # Detect native Windows (not WSL)
73
73
  self.is_windows = platform.system() == "Windows" and not os.environ.get("WSL_DISTRO_NAME")
74
+ self.is_darwin = platform.system() == "Darwin"
74
75
 
75
76
  # Script name constants — Windows uses .ps1, Unix uses .sh
76
77
  if self.is_windows:
@@ -1167,15 +1168,24 @@ class AgentVibesServer:
1167
1168
  if (cwd / ".claude").is_dir() and cwd != self.agentvibes_root:
1168
1169
  env["CLAUDE_PROJECT_DIR"] = str(cwd)
1169
1170
 
1170
- # Add common locations for piper to PATH (Unix only)
1171
+ # Augment PATH with platform-specific binary locations (Unix only).
1172
+ # MCP servers launched by Claude Desktop inherit a sanitized launchd/dbus PATH
1173
+ # that omits Homebrew (Mac) and pipx (all POSIX) locations.
1171
1174
  if not self.is_windows:
1172
1175
  home_dir = Path.home()
1173
- local_bin = str(home_dir / ".local" / "bin")
1174
- if "PATH" in env:
1175
- if local_bin not in env["PATH"]:
1176
- env["PATH"] = f"{local_bin}:{env['PATH']}"
1177
- else:
1178
- env["PATH"] = local_bin
1176
+ extra_paths = [
1177
+ str(home_dir / ".local" / "bin"),
1178
+ str(home_dir / ".local" / "share" / "pipx" / "venvs" / "piper-tts" / "bin"),
1179
+ ]
1180
+ # Mac: add Homebrew prefix for both Apple Silicon (/opt/homebrew) and Intel (/usr/local)
1181
+ if self.is_darwin:
1182
+ extra_paths = ["/opt/homebrew/bin", "/usr/local/bin"] + extra_paths
1183
+
1184
+ current_path = env.get("PATH", "")
1185
+ path_parts = current_path.split(os.pathsep) if current_path else []
1186
+ new_dirs = [p for p in extra_paths if p not in path_parts]
1187
+ if new_dirs:
1188
+ env["PATH"] = os.pathsep.join(new_dirs) + os.pathsep + current_path
1179
1189
 
1180
1190
  return env
1181
1191
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "agentvibes",
4
- "version": "5.7.7",
4
+ "version": "5.10.1",
5
5
  "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
6
  "homepage": "https://agentvibes.org",
7
7
  "keywords": [
@@ -83,7 +83,7 @@
83
83
  "test:syntax": "node -c src/installer.js && node -c mcp-server/install-deps.js",
84
84
  "test:bats": "AGENTVIBES_TEST_MODE=true bats test/unit/*.bats",
85
85
  "test:node": "node --test test/unit/*.test.js",
86
- "test:coverage": "c8 --reporter=lcov --reporter=text node --test test/unit/*.test.js",
86
+ "test:coverage": "c8 --reporter=lcov --reporter=text node --experimental-test-module-mocks --test-force-exit --test test/unit/*.test.js",
87
87
  "test:verbose": "AGENTVIBES_TEST_MODE=true bats -t test/unit/*.bats"
88
88
  },
89
89
  "dependencies": {
@@ -16,6 +16,99 @@ import ora from 'ora';
16
16
  import boxen from 'boxen';
17
17
  import { checkDependencies, displayMissingDependencies } from '../utils/dependency-checker.js';
18
18
 
19
+ // ─── Platform helpers ──────────────────────────────────────────────────────────
20
+
21
+ function commandExists(cmd) {
22
+ try {
23
+ const finder = process.platform === 'win32' ? 'where' : 'which';
24
+ execFileSync(finder, [cmd], { stdio: 'pipe' });
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ function brewExists() {
32
+ return commandExists('brew');
33
+ }
34
+
35
+ // Add Homebrew bin dirs to the current process's PATH so newly installed
36
+ // binaries are discoverable without restarting the shell.
37
+ function augmentPathForBrew() {
38
+ const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
39
+ const existing = new Set((process.env.PATH || '').split(path.delimiter));
40
+ const toAdd = brewPaths.filter(p => !existing.has(p));
41
+ if (toAdd.length > 0) {
42
+ process.env.PATH = [...toAdd, process.env.PATH].join(path.delimiter);
43
+ }
44
+ }
45
+
46
+ // Install pipx via the platform package manager and augment PATH afterward.
47
+ async function ensurePipx(platform) {
48
+ if (platform === 'darwin') {
49
+ if (!brewExists()) {
50
+ console.log(chalk.yellow('Homebrew not found. Install it from https://brew.sh, then re-run.\n'));
51
+ return false;
52
+ }
53
+ console.log(chalk.cyan('šŸ“¦ Installing pipx via Homebrew...\n'));
54
+ try {
55
+ execFileSync('brew', ['install', 'pipx'], { stdio: 'inherit', env: process.env }); // NOSONAR
56
+ augmentPathForBrew();
57
+ try { execFileSync('pipx', ['ensurepath'], { stdio: 'pipe', env: process.env }); } catch { /* non-fatal */ } // NOSONAR
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ } else if (platform === 'linux') {
63
+ console.log(chalk.cyan('šŸ“¦ Installing pipx via apt-get...\n'));
64
+ try {
65
+ execFileSync('sudo', ['apt-get', 'install', '-y', 'pipx'], { stdio: 'inherit', env: process.env }); // NOSONAR
66
+ // Augment PATH with ~/.local/bin so pipx and piper are findable in this process
67
+ const localBin = path.join(os.homedir(), '.local', 'bin');
68
+ const existingParts = new Set((process.env.PATH || '').split(path.delimiter));
69
+ if (!existingParts.has(localBin)) {
70
+ process.env.PATH = localBin + path.delimiter + process.env.PATH;
71
+ }
72
+ try { execFileSync('pipx', ['ensurepath'], { stdio: 'pipe', env: process.env }); } catch { /* non-fatal */ } // NOSONAR
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+
81
+ // Auto-install missing optional deps (ffmpeg, sox, pipx, etc.) via brew / apt.
82
+ async function autoInstallOptionalDeps(missing, platform) {
83
+ const installable = ['ffmpeg', 'sox', 'pipx', 'flock', 'curl', 'bc'];
84
+ const toInstall = installable.filter(dep => missing[dep]);
85
+ if (toInstall.length === 0) return;
86
+
87
+ if (platform === 'darwin') {
88
+ const brewMap = { ffmpeg: 'ffmpeg', sox: 'sox', pipx: 'pipx', flock: 'util-linux', curl: 'curl', bc: 'bc' };
89
+ const packages = toInstall.map(d => brewMap[d]).filter(Boolean);
90
+ if (packages.length === 0) return;
91
+ console.log(chalk.cyan(`\nšŸ“¦ Homebrew: brew install ${packages.join(' ')}\n`));
92
+ try {
93
+ execFileSync('brew', ['install', ...packages], { stdio: 'inherit', env: process.env }); // NOSONAR
94
+ augmentPathForBrew();
95
+ } catch {
96
+ console.log(chalk.yellow('āš ļø Some Homebrew packages may have failed — continuing\n'));
97
+ }
98
+ } else if (platform === 'linux') {
99
+ const aptMap = { ffmpeg: ['ffmpeg'], sox: ['sox', 'libsox-fmt-mp3'], pipx: ['pipx'], flock: ['util-linux'], curl: ['curl'], bc: ['bc'] };
100
+ const packages = toInstall.flatMap(d => aptMap[d] || []);
101
+ if (packages.length === 0) return;
102
+ console.log(chalk.cyan(`\nšŸ“¦ apt-get: sudo apt-get install -y ${packages.join(' ')}\n`));
103
+ try {
104
+ execFileSync('sudo', ['apt-get', 'update', '-qq'], { stdio: 'pipe', env: process.env }); // NOSONAR
105
+ execFileSync('sudo', ['apt-get', 'install', '-y', ...packages], { stdio: 'inherit', env: process.env }); // NOSONAR
106
+ } catch {
107
+ console.log(chalk.yellow('āš ļø Some apt packages may have failed — continuing\n'));
108
+ }
109
+ }
110
+ }
111
+
19
112
  /**
20
113
  * Check if WSL is installed on Windows
21
114
  */
@@ -163,26 +256,37 @@ function updateClaudeConfig(agentVibesPath, provider, apiKey = null) {
163
256
  * Install Piper TTS
164
257
  */
165
258
  async function installPiper(useWSL = false) {
166
- const spinner = ora('Installing Piper TTS...').start();
259
+ const platform = os.platform();
167
260
 
261
+ // Ensure pipx is present before attempting piper-tts install
262
+ if (!useWSL && !commandExists('pipx')) {
263
+ console.log(chalk.yellow('\nāš ļø pipx not found — installing it first...\n'));
264
+ const ok = await ensurePipx(platform);
265
+ if (!ok) {
266
+ console.log(chalk.red('āŒ Could not install pipx automatically.'));
267
+ if (platform === 'darwin') {
268
+ console.log(chalk.cyan(' brew install pipx'));
269
+ } else {
270
+ console.log(chalk.cyan(' sudo apt install pipx'));
271
+ }
272
+ console.log(chalk.gray(' Then re-run this installer.\n'));
273
+ return false;
274
+ }
275
+ console.log(chalk.green('āœ“ pipx ready\n'));
276
+ }
277
+
278
+ console.log(chalk.cyan('\nšŸ“¦ Installing Piper TTS via pipx...\n'));
168
279
  try {
169
- // Security: Use execFileSync with array args to prevent command injection
280
+ // Security: execFileSync with array args prevents command injection
170
281
  if (useWSL) {
171
282
  execFileSync('wsl', ['pipx', 'install', 'piper-tts'], { stdio: 'inherit' });
172
283
  } else {
173
- execFileSync('pipx', ['install', 'piper-tts'], { stdio: 'inherit' });
284
+ execFileSync('pipx', ['install', 'piper-tts'], { stdio: 'inherit', env: process.env }); // NOSONAR
174
285
  }
175
- spinner.succeed('Piper TTS installed successfully!');
286
+ console.log(chalk.green('\nāœ… Piper TTS installed!\n'));
176
287
  return true;
177
288
  } catch (error) {
178
- spinner.fail('Failed to install Piper TTS');
179
- console.error(chalk.yellow('\nāš ļø You may need to install pipx first:'));
180
- if (useWSL) {
181
- console.log(chalk.cyan(' wsl sudo apt install pipx'));
182
- } else {
183
- console.log(chalk.cyan(' brew install pipx (macOS)'));
184
- console.log(chalk.cyan(' sudo apt install pipx (Linux)'));
185
- }
289
+ console.log(chalk.red(`\nāŒ Failed to install Piper TTS: ${error.message}\n`));
186
290
  return false;
187
291
  }
188
292
  }
@@ -227,6 +331,7 @@ async function checkSystemDependencies() {
227
331
  return;
228
332
  }
229
333
 
334
+ const platform = os.platform();
230
335
  const hasCoreMissing = depResults.missing.node || depResults.missing.python || depResults.missing.bash;
231
336
 
232
337
  if (hasCoreMissing) {
@@ -234,7 +339,35 @@ async function checkSystemDependencies() {
234
339
  process.exit(1);
235
340
  }
236
341
 
237
- // Only optional dependencies missing
342
+ // Optional deps missing — offer auto-install on Mac/Linux
343
+ const canAutoInstall = (platform === 'darwin' && brewExists()) || platform === 'linux';
344
+
345
+ if (canAutoInstall) {
346
+ const mgr = platform === 'darwin' ? 'Homebrew' : 'apt-get';
347
+ const { doInstall } = await inquirer.prompt([{
348
+ type: 'confirm',
349
+ name: 'doInstall',
350
+ message: `Install missing optional dependencies now using ${mgr}?`,
351
+ default: true
352
+ }]);
353
+
354
+ if (doInstall) {
355
+ await autoInstallOptionalDeps(depResults.missing, platform);
356
+ const recheckResults = checkDependencies();
357
+ const stillMissing = Object.values(recheckResults.missing).some(Boolean);
358
+ if (stillMissing) {
359
+ console.log(chalk.yellow('āš ļø Some dependencies could not be installed automatically.\n'));
360
+ displayMissingDependencies(recheckResults);
361
+ } else {
362
+ console.log(chalk.green('āœ“ All optional dependencies installed\n'));
363
+ }
364
+ return;
365
+ }
366
+ } else if (platform === 'darwin' && !brewExists()) {
367
+ console.log(chalk.yellow('ā„¹ļø Homebrew not found — install it from https://brew.sh to get ffmpeg and other tools automatically.\n'));
368
+ }
369
+
370
+ // Fall through: ask to continue without the missing deps
238
371
  const { continueAnyway } = await inquirer.prompt([{
239
372
  type: 'confirm',
240
373
  name: 'continueAnyway',
@@ -243,7 +376,7 @@ async function checkSystemDependencies() {
243
376
  }]);
244
377
 
245
378
  if (!continueAnyway) {
246
- console.log(chalk.yellow('\nInstallation cancelled. Please install the dependencies and try again.\n'));
379
+ console.log(chalk.yellow('\nInstallation cancelled. Install the missing dependencies and try again.\n'));
247
380
  process.exit(0);
248
381
  }
249
382
  }
@@ -389,6 +522,121 @@ async function setupPythonDependencies(isWindows) {
389
522
  }
390
523
  }
391
524
 
525
+ /**
526
+ * Interactive voice download step shown after Piper installs.
527
+ * Offers starter, libritts-900, full pack, or skip.
528
+ * @param {string} agentVibesDir - Path to the AgentVibes installation directory
529
+ * @returns {Promise<void>}
530
+ */
531
+ async function downloadPiperVoices(agentVibesDir) {
532
+ console.log(chalk.bold('\nšŸŽ™ļø Step 4b: Download Voice Models\n'));
533
+
534
+ // Skip if voices are already present — respect PIPER_VOICES_DIR override so we
535
+ // check the same directory the runtime will use.
536
+ const homeDir = os.homedir();
537
+ const voiceDir = process.env.PIPER_VOICES_DIR || path.join(homeDir, '.claude', 'piper-voices');
538
+ try {
539
+ if (fs.existsSync(voiceDir)) {
540
+ const onnxFiles = fs.readdirSync(voiceDir).filter(f => f.endsWith('.onnx'));
541
+ // Only count voices where both .onnx and .onnx.json exist and are non-empty
542
+ const completeVoices = onnxFiles.filter(f => {
543
+ try {
544
+ const onnxStat = fs.statSync(path.join(voiceDir, f));
545
+ const jsonStat = fs.statSync(path.join(voiceDir, f + '.json'));
546
+ return onnxStat.size > 0 && jsonStat.size > 0;
547
+ } catch { return false; }
548
+ });
549
+ if (completeVoices.length > 0) {
550
+ console.log(chalk.green(`āœ“ ${completeVoices.length} voice model(s) already installed — skipping download\n`));
551
+ return;
552
+ }
553
+ }
554
+ } catch { /* ignore, fall through to download prompt */ }
555
+
556
+ console.log(chalk.gray('Piper needs at least one voice model to speak.\n'));
557
+
558
+ const { voiceChoice } = await inquirer.prompt([{
559
+ type: 'list',
560
+ name: 'voiceChoice',
561
+ message: 'Which voices would you like to download?',
562
+ choices: [
563
+ {
564
+ name: chalk.cyan('Starter voice') + chalk.gray(' — en_US-lessac-medium (~13 MB, download in seconds)'),
565
+ value: 'starter',
566
+ short: 'Starter'
567
+ },
568
+ {
569
+ name: chalk.cyan('LibriTTS 900-speaker pack') + chalk.gray(' — en_US-libritts-high (~57 MB, 900 unique named speakers)'),
570
+ value: 'libritts',
571
+ short: 'LibriTTS 900'
572
+ },
573
+ {
574
+ name: chalk.cyan('Full pack') + chalk.gray(' — 11 voices including LibriTTS (~250 MB, all BMAD agent voices)'),
575
+ value: 'full',
576
+ short: 'Full pack'
577
+ },
578
+ {
579
+ name: chalk.gray('Skip — download voices later with /agent-vibes:add'),
580
+ value: 'skip',
581
+ short: 'Skip'
582
+ }
583
+ ]
584
+ }]);
585
+
586
+ if (voiceChoice === 'skip') {
587
+ console.log(chalk.yellow('\nāš ļø No voices downloaded.'));
588
+ console.log(chalk.gray(' Download voices anytime with: ') + chalk.cyan('/agent-vibes:add\n'));
589
+ return;
590
+ }
591
+
592
+ const voiceManagerScript = path.join(agentVibesDir, '.claude', 'hooks', 'piper-voice-manager.sh');
593
+ const fullPackScript = path.join(agentVibesDir, '.claude', 'hooks', 'piper-download-voices.sh');
594
+
595
+ if (voiceChoice === 'starter') {
596
+ console.log(chalk.cyan('\nšŸ“„ Downloading en_US-lessac-medium (~13 MB)...\n'));
597
+ try {
598
+ // Use positional params ($1/$2) so the script path is never string-interpolated
599
+ execFileSync('bash', ['-c', 'source "$1" && download_voice "$2"', '--', voiceManagerScript, 'en_US-lessac-medium'], { // NOSONAR
600
+ stdio: 'inherit',
601
+ env: process.env
602
+ });
603
+ console.log(chalk.green('\nāœ… Starter voice ready!\n'));
604
+ } catch {
605
+ console.log(chalk.yellow('\nāš ļø Download failed — try later with: /agent-vibes:add\n'));
606
+ }
607
+ } else if (voiceChoice === 'libritts') {
608
+ console.log(chalk.cyan('\nšŸ“„ Downloading LibriTTS 900-speaker pack (~57 MB)...\n'));
609
+ console.log(chalk.gray(' This gives you 900 individually named speakers to choose from.\n'));
610
+ try {
611
+ execFileSync('bash', ['-c', 'source "$1" && download_voice "$2"', '--', voiceManagerScript, 'en_US-libritts-high'], { // NOSONAR
612
+ stdio: 'inherit',
613
+ env: process.env
614
+ });
615
+ console.log(chalk.green('\nāœ… LibriTTS 900-speaker pack ready!'));
616
+ console.log(chalk.gray(' Browse speakers with: ') + chalk.cyan('/agent-vibes:list\n'));
617
+ } catch {
618
+ console.log(chalk.yellow('\nāš ļø Download failed — try later with: /agent-vibes:add\n'));
619
+ }
620
+ } else if (voiceChoice === 'full') {
621
+ console.log(chalk.cyan('\nšŸ“„ Downloading full voice pack (~250 MB)...\n'));
622
+ console.log(chalk.gray(' This includes all BMAD agent voices plus LibriTTS 900 speakers.\n'));
623
+ try {
624
+ if (fs.existsSync(fullPackScript)) {
625
+ execFileSync('bash', [fullPackScript, '--yes'], { // NOSONAR
626
+ stdio: 'inherit',
627
+ env: process.env
628
+ });
629
+ console.log(chalk.green('\nāœ… Full voice pack downloaded!\n'));
630
+ } else {
631
+ console.log(chalk.yellow(' Download script not found at: ' + fullPackScript));
632
+ console.log(chalk.yellow(' Try: /agent-vibes:add\n'));
633
+ }
634
+ } catch {
635
+ console.log(chalk.yellow('\nāš ļø Download failed — try later with: /agent-vibes:add\n'));
636
+ }
637
+ }
638
+ }
639
+
392
640
  /**
393
641
  * Display welcome banner
394
642
  * @param {string} platform - Platform name
@@ -433,7 +681,8 @@ function showSuccessMessage(configPath, provider) {
433
681
  console.log(chalk.gray(` ${configPath}\n`));
434
682
 
435
683
  if (provider === 'piper') {
436
- console.log(chalk.gray('Voice models will download automatically on first use.\n'));
684
+ console.log(chalk.gray('Browse voices: ') + chalk.cyan('/agent-vibes:list') + '\n');
685
+ console.log(chalk.gray('Add more voices: ') + chalk.cyan('/agent-vibes:add') + '\n');
437
686
  }
438
687
  }
439
688
 
@@ -457,6 +706,11 @@ export async function installMCP() {
457
706
  // Step 4: Choose TTS provider
458
707
  const provider = await setupTTSProvider(isWindows);
459
708
 
709
+ // Step 4b: Download voices if Piper was selected
710
+ if (provider === 'piper') {
711
+ await downloadPiperVoices(agentVibesDir);
712
+ }
713
+
460
714
  // Step 5: Install Python dependencies
461
715
  await setupPythonDependencies(isWindows);
462
716
 
@@ -468,7 +722,7 @@ export async function installMCP() {
468
722
 
469
723
  // Step 7: Update Claude Desktop config
470
724
  console.log(chalk.bold('šŸ“ Step 7: Updating Claude Desktop configuration...\n'));
471
- const configPath = updateClaudeConfig(agentVibesDir, provider, apiKey);
725
+ const configPath = updateClaudeConfig(agentVibesDir, provider);
472
726
  console.log(chalk.green(`āœ“ Updated: ${configPath}\n`));
473
727
 
474
728
  // Success!
@@ -238,9 +238,9 @@ export class AgentVibesConsole {
238
238
  // Right-aligned: git remote + branch when available, else AgentVibes repo link
239
239
  let topRightContent = `{${BRAND_PINK}-fg}github.com/preibisch/agentvibes{/${BRAND_PINK}-fg}`;
240
240
  try {
241
- const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'],
241
+ const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], // NOSONAR
242
242
  { encoding: 'utf8', timeout: 2000, cwd });
243
- const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'],
243
+ const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'], // NOSONAR
244
244
  { encoding: 'utf8', timeout: 2000, cwd });
245
245
  if (branchResult.status === 0 && remoteResult.status === 0) {
246
246
  const branch = branchResult.stdout.trim();
@@ -627,7 +627,7 @@ export class AgentVibesConsole {
627
627
  _createFooter() {
628
628
  // Detect installed providers inline (same logic as ProviderService)
629
629
  const _has = (bin) => {
630
- try { execFileSync('which', [bin], { stdio: 'ignore', timeout: 2000 }); return true; }
630
+ try { execFileSync('which', [bin], { stdio: 'ignore', timeout: 2000 }); return true; } // NOSONAR
631
631
  catch { return false; }
632
632
  };
633
633
  const detected = {
@@ -121,7 +121,10 @@ function _detect(players, env) {
121
121
  * @returns {string|null}
122
122
  */
123
123
  export function detectRemoteLlm() {
124
- const cfgPath = path.join(os.homedir(), '.agentvibes', 'transport-config.json');
124
+ // process.env.HOME takes priority over os.homedir() so tests can inject a fake home on all platforms.
125
+ // On Windows production use, HOME is typically unset, so os.homedir() (reads USERPROFILE) is the fallback.
126
+ const homeDir = process.env.HOME ?? os.homedir();
127
+ const cfgPath = path.join(homeDir, '.agentvibes', 'transport-config.json');
125
128
  if (!fs.existsSync(cfgPath)) return null;
126
129
  try {
127
130
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));