agentvibes 5.9.0 → 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 (145) hide show
  1. package/.agentvibes/config.json +3 -12
  2. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  3. package/.claude/commands/agent-vibes-rdp.md +24 -24
  4. package/.claude/config/audio-effects.cfg +4 -5
  5. package/.claude/config/audio-effects.cfg.sample +52 -52
  6. package/.claude/config/background-music-enabled.txt +1 -1
  7. package/.claude/docs/TERMUX_SETUP.md +408 -408
  8. package/.claude/github-star-reminder.txt +1 -1
  9. package/.claude/hooks/audio-cache-utils.sh +0 -0
  10. package/.claude/hooks/audio-processor.sh +0 -0
  11. package/.claude/hooks/background-music-manager.sh +0 -0
  12. package/.claude/hooks/bmad-party-speak.sh +0 -0
  13. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  14. package/.claude/hooks/bmad-speak.sh +0 -0
  15. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  16. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  17. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  18. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  19. package/.claude/hooks/clean-audio-cache.sh +0 -0
  20. package/.claude/hooks/cleanup-cache.sh +0 -0
  21. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  22. package/.claude/hooks/download-extra-voices.sh +0 -0
  23. package/.claude/hooks/effects-manager.sh +0 -0
  24. package/.claude/hooks/github-star-reminder.sh +0 -0
  25. package/.claude/hooks/language-manager.sh +0 -0
  26. package/.claude/hooks/learn-manager.sh +0 -0
  27. package/.claude/hooks/macos-voice-manager.sh +0 -0
  28. package/.claude/hooks/migrate-background-music.sh +0 -0
  29. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  30. package/.claude/hooks/optimize-background-music.sh +0 -0
  31. package/.claude/hooks/path-resolver.sh +0 -0
  32. package/.claude/hooks/personality-manager.sh +0 -0
  33. package/.claude/hooks/piper-download-voices.sh +0 -0
  34. package/.claude/hooks/piper-installer.sh +0 -0
  35. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  36. package/.claude/hooks/piper-voice-manager.sh +0 -0
  37. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
  38. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  39. package/.claude/hooks/play-tts-macos.sh +0 -0
  40. package/.claude/hooks/play-tts-piper.sh +20 -13
  41. package/.claude/hooks/play-tts-soprano.sh +0 -0
  42. package/.claude/hooks/play-tts-ssh-remote.sh +0 -0
  43. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  44. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  45. package/.claude/hooks/play-tts.sh +0 -0
  46. package/.claude/hooks/prepare-release.sh +0 -0
  47. package/.claude/hooks/provider-commands.sh +0 -0
  48. package/.claude/hooks/provider-manager.sh +0 -0
  49. package/.claude/hooks/replay-target-audio.sh +0 -0
  50. package/.claude/hooks/requirements.txt +6 -6
  51. package/.claude/hooks/sentiment-manager.sh +0 -0
  52. package/.claude/hooks/session-start-tts.sh +0 -0
  53. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  54. package/.claude/hooks/speed-manager.sh +0 -0
  55. package/.claude/hooks/stop-tts.sh +0 -0
  56. package/.claude/hooks/termux-installer.sh +0 -0
  57. package/.claude/hooks/translate-manager.sh +0 -0
  58. package/.claude/hooks/translator.py +237 -237
  59. package/.claude/hooks/tts-queue-worker.sh +0 -0
  60. package/.claude/hooks/tts-queue.sh +0 -0
  61. package/.claude/hooks/verbosity-manager.sh +0 -0
  62. package/.claude/hooks/voice-manager.sh +6 -0
  63. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
  64. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  65. package/.claude/verbosity.txt +1 -1
  66. package/.clawdbot/README.md +105 -105
  67. package/.mcp.json +19 -6
  68. package/README.md +1 -1
  69. package/WINDOWS-SETUP.md +208 -208
  70. package/bin/agent-vibes +39 -39
  71. package/bin/agentvibes-voice-browser.js +0 -0
  72. package/bin/agentvibes.js +0 -0
  73. package/bin/mcp-server.js +121 -121
  74. package/bin/mcp-server.sh +0 -0
  75. package/bin/test-bmad-pr +78 -78
  76. package/mcp-server/QUICK_START.md +203 -203
  77. package/mcp-server/README.md +345 -345
  78. package/mcp-server/WINDOWS_SETUP.md +0 -0
  79. package/mcp-server/examples/claude_desktop_config.json +11 -11
  80. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  81. package/mcp-server/examples/custom_instructions.md +169 -169
  82. package/mcp-server/install-deps.js +0 -0
  83. package/mcp-server/server.py +1807 -1797
  84. package/mcp-server/test_server.py +0 -0
  85. package/package.json +2 -2
  86. package/src/cli/list-personalities.js +110 -110
  87. package/src/cli/list-voices.js +114 -114
  88. package/src/commands/bmad-voices.js +394 -394
  89. package/src/commands/install-mcp.js +730 -476
  90. package/src/console/app.js +3 -3
  91. package/src/console/brand-colors.js +13 -13
  92. package/src/console/constants/personalities.js +44 -44
  93. package/src/console/tabs/agents-tab.js +6 -6
  94. package/src/console/tabs/help-tab.js +314 -314
  95. package/src/console/tabs/music-tab.js +1 -1
  96. package/src/console/tabs/readme-tab.js +272 -272
  97. package/src/console/tabs/receiver-tab.js +13 -13
  98. package/src/console/tabs/settings-tab.js +2 -2
  99. package/src/console/tabs/setup-tab.js +10 -10
  100. package/src/console/tabs/voices-tab.js +4 -4
  101. package/src/console/widgets/destroy-list.js +25 -25
  102. package/src/console/widgets/notice.js +55 -55
  103. package/src/console/widgets/personality-picker.js +2 -2
  104. package/src/console/widgets/reverb-picker.js +1 -1
  105. package/src/i18n/de.js +202 -202
  106. package/src/i18n/es.js +202 -202
  107. package/src/i18n/fr.js +202 -202
  108. package/src/i18n/hi.js +202 -202
  109. package/src/i18n/ja.js +202 -202
  110. package/src/i18n/ko.js +202 -202
  111. package/src/i18n/pt.js +202 -202
  112. package/src/i18n/strings.js +54 -54
  113. package/src/i18n/zh-CN.js +202 -202
  114. package/src/installer/language-screen.js +31 -31
  115. package/src/installer/music-file-input.js +304 -304
  116. package/src/installer.js +32 -27
  117. package/src/services/config-service.js +264 -264
  118. package/src/services/language-service.js +47 -47
  119. package/src/services/provider-service.js +143 -143
  120. package/src/services/tts-engine-service.js +2 -2
  121. package/src/utils/audio-duration-validator.js +298 -298
  122. package/src/utils/audio-format-validator.js +277 -277
  123. package/src/utils/dependency-checker.js +469 -469
  124. package/src/utils/file-ownership-verifier.js +358 -358
  125. package/src/utils/list-formatter.js +200 -194
  126. package/src/utils/music-file-validator.js +285 -285
  127. package/src/utils/platform-resolver.js +369 -0
  128. package/src/utils/preview-list-prompt.js +136 -136
  129. package/src/utils/provider-validator.js +9 -9
  130. package/src/utils/secure-music-storage.js +412 -412
  131. package/templates/agentvibes-receiver.sh +231 -231
  132. package/templates/audio/welcome-music.mp3 +0 -0
  133. package/.agentvibes/install-manifest.json +0 -330
  134. package/.claude/config/background-music-position.txt +0 -27
  135. package/.claude/config/background-music-volume.txt +0 -1
  136. package/.claude/config/background-music.cfg +0 -1
  137. package/.claude/config/background-music.txt +0 -1
  138. package/.claude/config/language.txt +0 -1
  139. package/.claude/config/reverb-level.txt +0 -1
  140. package/.claude/config/tts-speech-rate.txt +0 -1
  141. package/.claude/config/tts-verbosity.txt +0 -1
  142. package/.claude/hooks/play-tts-agentvibes-receiver.sh +0 -1
  143. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
  144. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
  145. package/.claude/piper-voices-dir.txt +0 -1
@@ -1,476 +1,730 @@
1
- #!/usr/bin/env node
2
- /**
3
- * AgentVibes MCP Server Installer
4
- *
5
- * Interactive installer for setting up AgentVibes MCP server with Claude Desktop
6
- * Handles platform-specific installation (Windows/Mac/Linux)
7
- */
8
-
9
- import inquirer from 'inquirer';
10
- import { execSync, execFileSync } from 'child_process';
11
- import fs from 'fs';
12
- import path from 'path';
13
- import os from 'os';
14
- import chalk from 'chalk';
15
- import ora from 'ora';
16
- import boxen from 'boxen';
17
- import { checkDependencies, displayMissingDependencies } from '../utils/dependency-checker.js';
18
-
19
- /**
20
- * Check if WSL is installed on Windows
21
- */
22
- function checkWSL() {
23
- try {
24
- // Security: Use execFileSync with array args to prevent command injection
25
- execFileSync('wsl', ['--version'], { stdio: 'pipe' });
26
- return true;
27
- } catch {
28
- return false;
29
- }
30
- }
31
-
32
- /**
33
- * Check if Python is available
34
- */
35
- function checkPython() {
36
- const commands = ['python3', 'python', 'py'];
37
-
38
- for (const cmd of commands) {
39
- try {
40
- // Security: Use execFileSync with array args to prevent command injection
41
- const version = execFileSync(cmd, ['--version'], { encoding: 'utf8', stdio: 'pipe' });
42
- return { available: true, command: cmd, version: version.trim() };
43
- } catch {
44
- continue;
45
- }
46
- }
47
-
48
- return { available: false };
49
- }
50
-
51
- /**
52
- * Check if Python MCP package is installed
53
- */
54
- function checkMCPPackage(pythonCmd) {
55
- try {
56
- // Security: Use execFileSync with array args to prevent command injection
57
- execFileSync(pythonCmd, ['-c', 'import mcp'], { stdio: 'pipe' });
58
- return true;
59
- } catch {
60
- return false;
61
- }
62
- }
63
-
64
- /**
65
- * Get Claude Desktop config path for current platform
66
- */
67
- function getClaudeConfigPath() {
68
- const platform = os.platform();
69
-
70
- switch (platform) {
71
- case 'darwin': // macOS
72
- return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
73
- case 'win32': // Windows
74
- return path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
75
- default: // Linux
76
- return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');
77
- }
78
- }
79
-
80
- /**
81
- * Get AgentVibes installation directory
82
- */
83
- function getAgentVibesDir() {
84
- // Try to find AgentVibes directory
85
- // 1. Current directory
86
- if (fs.existsSync('./.claude/hooks/play-tts.sh')) {
87
- return process.cwd();
88
- }
89
-
90
- // 2. Parent directory
91
- const parentDir = path.resolve(process.cwd(), '..');
92
- if (fs.existsSync(path.join(parentDir, '.claude/hooks/play-tts.sh'))) {
93
- return parentDir;
94
- }
95
-
96
- // 3. Ask user
97
- return null;
98
- }
99
-
100
- /**
101
- * Update Claude Desktop configuration
102
- */
103
- function updateClaudeConfig(agentVibesPath, provider, apiKey = null) {
104
- const configPath = getClaudeConfigPath();
105
- const platform = os.platform();
106
-
107
- // Create config directory if it doesn't exist
108
- const configDir = path.dirname(configPath);
109
- if (!fs.existsSync(configDir)) {
110
- fs.mkdirSync(configDir, { recursive: true });
111
- }
112
-
113
- // Read existing config or create new one
114
- let config = { mcpServers: {} };
115
- if (fs.existsSync(configPath)) {
116
- const content = fs.readFileSync(configPath, 'utf8');
117
- config = JSON.parse(content);
118
- if (!config.mcpServers) {
119
- config.mcpServers = {};
120
- }
121
- }
122
-
123
- // Prepare MCP server config
124
- let serverPath = path.join(agentVibesPath, 'mcp-server', 'server.py');
125
-
126
- if (platform === 'win32') {
127
- // Windows: Use WSL
128
- serverPath = serverPath.replace(/\\/g, '/').replace(/^([A-Z]):/, (match, drive) => {
129
- return `/mnt/${drive.toLowerCase()}`;
130
- });
131
-
132
- config.mcpServers.agentvibes = {
133
- command: 'wsl',
134
- args: ['python3', serverPath],
135
- env: {}
136
- };
137
- } else {
138
- // macOS/Linux: Use native Python
139
- config.mcpServers.agentvibes = {
140
- command: 'python3',
141
- args: [serverPath],
142
- env: {}
143
- };
144
- }
145
-
146
-
147
- // Write config atomically to prevent race conditions (TOCTOU)
148
- // Write to temp file first, then rename atomically
149
- const tempPath = `${configPath}.tmp.${process.pid}`;
150
- try {
151
- fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
152
- fs.renameSync(tempPath, configPath);
153
- } catch (error) {
154
- // Clean up temp file if rename fails
155
- try { fs.unlinkSync(tempPath); } catch { /* ignore cleanup errors */ }
156
- throw error;
157
- }
158
-
159
- return configPath;
160
- }
161
-
162
- /**
163
- * Install Piper TTS
164
- */
165
- async function installPiper(useWSL = false) {
166
- const spinner = ora('Installing Piper TTS...').start();
167
-
168
- try {
169
- // Security: Use execFileSync with array args to prevent command injection
170
- if (useWSL) {
171
- execFileSync('wsl', ['pipx', 'install', 'piper-tts'], { stdio: 'inherit' });
172
- } else {
173
- execFileSync('pipx', ['install', 'piper-tts'], { stdio: 'inherit' });
174
- }
175
- spinner.succeed('Piper TTS installed successfully!');
176
- return true;
177
- } 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
- }
186
- return false;
187
- }
188
- }
189
-
190
- /**
191
- * Install Python MCP package
192
- */
193
- async function installMCPPackage(pythonCmd, useWSL = false) {
194
- const spinner = ora('Installing Python MCP package...').start();
195
-
196
- try {
197
- // Security: Use execFileSync with array args to prevent command injection
198
- if (useWSL) {
199
- execFileSync('wsl', [pythonCmd, '-m', 'pip', 'install', '--break-system-packages', 'mcp'], { stdio: 'pipe' });
200
- } else {
201
- execFileSync(pythonCmd, ['-m', 'pip', 'install', '--user', 'mcp'], { stdio: 'pipe' });
202
- }
203
- spinner.succeed('Python MCP package installed successfully!');
204
- return true;
205
- } catch (error) {
206
- spinner.fail('Failed to install Python MCP package');
207
- console.error(chalk.red(`\n❌ Error: ${error.message}`));
208
- return false;
209
- }
210
- }
211
-
212
- /**
213
- * Main installer
214
- */
215
- /**
216
- * Check system dependencies and handle missing ones
217
- * @returns {Promise<void>}
218
- */
219
- async function checkSystemDependencies() {
220
- console.log(chalk.bold('🔍 Step 1: Checking system dependencies...\n'));
221
-
222
- const depResults = checkDependencies();
223
- const hasMissingDeps = displayMissingDependencies(depResults);
224
-
225
- if (!hasMissingDeps) {
226
- console.log(chalk.green('✓ All dependencies installed!\n'));
227
- return;
228
- }
229
-
230
- const hasCoreMissing = depResults.missing.node || depResults.missing.python || depResults.missing.bash;
231
-
232
- if (hasCoreMissing) {
233
- console.log(chalk.red('\n❌ Critical dependencies are missing. Please install them before continuing.\n'));
234
- process.exit(1);
235
- }
236
-
237
- // Only optional dependencies missing
238
- const { continueAnyway } = await inquirer.prompt([{
239
- type: 'confirm',
240
- name: 'continueAnyway',
241
- message: 'Some optional dependencies are missing. Continue anyway?',
242
- default: true
243
- }]);
244
-
245
- if (!continueAnyway) {
246
- console.log(chalk.yellow('\nInstallation cancelled. Please install the dependencies and try again.\n'));
247
- process.exit(0);
248
- }
249
- }
250
-
251
- /**
252
- * Locate AgentVibes installation directory
253
- * @param {boolean} isWindows - Whether running on Windows
254
- * @returns {Promise<string>} AgentVibes directory path
255
- */
256
- async function locateAgentVibesDir(isWindows) {
257
- console.log(chalk.bold('📁 Step 2: Locating AgentVibes installation...\n'));
258
-
259
- let agentVibesDir = getAgentVibesDir();
260
-
261
- if (!agentVibesDir) {
262
- const { customPath } = await inquirer.prompt([{
263
- type: 'input',
264
- name: 'customPath',
265
- message: 'Enter the path to your AgentVibes installation:',
266
- default: isWindows ? 'C:\\Users\\USERNAME\\AgentVibes' : '~/AgentVibes',
267
- validate: (input) => {
268
- const expanded = input.replace(/^~/, os.homedir());
269
- if (fs.existsSync(path.join(expanded, '.claude/hooks/play-tts.sh'))) {
270
- return true;
271
- }
272
- return 'AgentVibes not found at this path. Please check and try again.';
273
- }
274
- }]);
275
-
276
- agentVibesDir = customPath.replace(/^~/, os.homedir());
277
- }
278
-
279
- console.log(chalk.green(`✓ Found AgentVibes at: ${agentVibesDir}\n`));
280
- return agentVibesDir;
281
- }
282
-
283
- /**
284
- * Check and setup WSL on Windows
285
- * @returns {Promise<void>}
286
- */
287
- async function setupWindowsWSL() {
288
- console.log(chalk.bold('🪟 Step 3: Windows environment setup...\n'));
289
-
290
- const hasWSL = checkWSL();
291
-
292
- if (hasWSL) {
293
- console.log(chalk.green('✓ WSL is installed\n'));
294
- return;
295
- }
296
-
297
- console.log(chalk.yellow('⚠️ WSL (Windows Subsystem for Linux) is required but not installed.'));
298
- const { installWSL } = await inquirer.prompt([{
299
- type: 'confirm',
300
- name: 'installWSL',
301
- message: 'Install WSL now? (Requires restart)',
302
- default: true
303
- }]);
304
-
305
- if (!installWSL) {
306
- console.log(chalk.red('\n❌ WSL is required for AgentVibes MCP server on Windows'));
307
- process.exit(1);
308
- }
309
-
310
- console.log(chalk.cyan('\n📦 Installing WSL...'));
311
- try {
312
- // Security: Use execFileSync with array args to prevent command injection
313
- execFileSync('wsl', ['--install'], { stdio: 'inherit' });
314
- console.log(chalk.green('\n✅ WSL installed successfully!'));
315
- console.log(chalk.yellow('⚠️ Please restart your computer and run this installer again.'));
316
- process.exit(0);
317
- } catch (error) {
318
- console.error(chalk.red('\n❌ Failed to install WSL'));
319
- console.error(chalk.yellow('Please install WSL manually: https://aka.ms/wsl'));
320
- process.exit(1);
321
- }
322
- }
323
-
324
- /**
325
- * Select and install TTS provider
326
- * @param {boolean} isWindows - Whether running on Windows
327
- * @returns {Promise<string>} Selected provider name
328
- */
329
- async function setupTTSProvider(isWindows) {
330
- console.log(chalk.bold('🎤 Step 4: Choose TTS provider...\n'));
331
-
332
- const { provider } = await inquirer.prompt([{
333
- type: 'list',
334
- name: 'provider',
335
- message: 'Select your preferred TTS provider:',
336
- choices: [
337
- {
338
- name: 'Piper TTS (Free, Offline, Open Source) - Recommended',
339
- value: 'piper',
340
- short: 'Piper'
341
- },
342
- {
343
- name: 'macOS TTS (Native macOS text-to-speech)',
344
- value: 'macos',
345
- short: 'macOS'
346
- }
347
- ]
348
- }]);
349
-
350
- if (provider === 'piper') {
351
- console.log(chalk.cyan('\n📦 Installing Piper TTS...'));
352
- await installPiper(isWindows);
353
- } else if (provider === 'macos') {
354
- console.log(chalk.cyan('\n✅ macOS TTS uses native system voices - no installation needed'));
355
- }
356
-
357
- return provider;
358
- }
359
-
360
- /**
361
- * Setup Python dependencies
362
- * @param {boolean} isWindows - Whether running on Windows
363
- * @returns {Promise<void>}
364
- */
365
- async function setupPythonDependencies(isWindows) {
366
- console.log(chalk.bold('\n🐍 Step 5: Installing Python dependencies...\n'));
367
-
368
- const pythonCheck = isWindows
369
- ? { available: true, command: 'python3' } // WSL Python
370
- : checkPython();
371
-
372
- if (!pythonCheck.available) {
373
- console.error(chalk.red('❌ Python not found!'));
374
- console.log(chalk.yellow('Please install Python 3.10+ from https://python.org'));
375
- process.exit(1);
376
- }
377
-
378
- console.log(chalk.green(`✓ Python found: ${pythonCheck.version || 'python3'}\n`));
379
-
380
- // Check and install MCP package
381
- const hasMCP = isWindows
382
- ? false // Always install in WSL
383
- : checkMCPPackage(pythonCheck.command);
384
-
385
- if (!hasMCP) {
386
- await installMCPPackage(pythonCheck.command, isWindows);
387
- } else {
388
- console.log(chalk.green('✓ Python MCP package already installed\n'));
389
- }
390
- }
391
-
392
- /**
393
- * Display welcome banner
394
- * @param {string} platform - Platform name
395
- */
396
- function showWelcomeBanner(platform) {
397
- console.log(boxen(
398
- chalk.bold.cyan('AgentVibes MCP Server Installer') + '\n\n' +
399
- 'Give Claude Desktop a voice! 🎤',
400
- {
401
- padding: 1,
402
- margin: 1,
403
- borderStyle: 'round',
404
- borderColor: 'cyan'
405
- }
406
- ));
407
-
408
- const platformLabel = platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux';
409
- console.log(chalk.gray(`Platform: ${platformLabel}\n`));
410
- }
411
-
412
- /**
413
- * Display success message
414
- * @param {string} configPath - Config file path
415
- * @param {string} provider - Provider name
416
- */
417
- function showSuccessMessage(configPath, provider) {
418
- console.log(boxen(
419
- chalk.bold.green('✅ Installation Complete!') + '\n\n' +
420
- chalk.white('Next steps:\n') +
421
- chalk.cyan('1. Restart Claude Desktop\n') +
422
- chalk.cyan('2. Try: "Say hello using text to speech"\n') +
423
- chalk.cyan('3. Enjoy your talking Claude! 🎤'),
424
- {
425
- padding: 1,
426
- margin: 1,
427
- borderStyle: 'round',
428
- borderColor: 'green'
429
- }
430
- ));
431
-
432
- console.log(chalk.gray('\nConfiguration saved to:'));
433
- console.log(chalk.gray(` ${configPath}\n`));
434
-
435
- if (provider === 'piper') {
436
- console.log(chalk.gray('Voice models will download automatically on first use.\n'));
437
- }
438
- }
439
-
440
- export async function installMCP() {
441
- const platform = os.platform();
442
- const isWindows = platform === 'win32';
443
-
444
- showWelcomeBanner(platform);
445
-
446
- // Step 1: Check system dependencies
447
- await checkSystemDependencies();
448
-
449
- // Step 2: Find AgentVibes directory
450
- const agentVibesDir = await locateAgentVibesDir(isWindows);
451
-
452
- // Step 3: Windows-specific checks
453
- if (isWindows) {
454
- await setupWindowsWSL();
455
- }
456
-
457
- // Step 4: Choose TTS provider
458
- const provider = await setupTTSProvider(isWindows);
459
-
460
- // Step 5: Install Python dependencies
461
- await setupPythonDependencies(isWindows);
462
-
463
- // Step 6: Configure provider in AgentVibes
464
- console.log(chalk.bold('⚙️ Step 6: Configuring AgentVibes...\n'));
465
- const providerFile = path.join(agentVibesDir, '.claude', 'tts-provider.txt');
466
- fs.writeFileSync(providerFile, provider);
467
- console.log(chalk.green(`✓ Set provider to: ${provider}\n`));
468
-
469
- // Step 7: Update Claude Desktop config
470
- console.log(chalk.bold('📝 Step 7: Updating Claude Desktop configuration...\n'));
471
- const configPath = updateClaudeConfig(agentVibesDir, provider, apiKey);
472
- console.log(chalk.green(`✓ Updated: ${configPath}\n`));
473
-
474
- // Success!
475
- showSuccessMessage(configPath, provider);
476
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentVibes MCP Server Installer
4
+ *
5
+ * Interactive installer for setting up AgentVibes MCP server with Claude Desktop
6
+ * Handles platform-specific installation (Windows/Mac/Linux)
7
+ */
8
+
9
+ import inquirer from 'inquirer';
10
+ import { execSync, execFileSync } from 'child_process';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import os from 'os';
14
+ import chalk from 'chalk';
15
+ import ora from 'ora';
16
+ import boxen from 'boxen';
17
+ import { checkDependencies, displayMissingDependencies } from '../utils/dependency-checker.js';
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
+
112
+ /**
113
+ * Check if WSL is installed on Windows
114
+ */
115
+ function checkWSL() {
116
+ try {
117
+ // Security: Use execFileSync with array args to prevent command injection
118
+ execFileSync('wsl', ['--version'], { stdio: 'pipe' });
119
+ return true;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if Python is available
127
+ */
128
+ function checkPython() {
129
+ const commands = ['python3', 'python', 'py'];
130
+
131
+ for (const cmd of commands) {
132
+ try {
133
+ // Security: Use execFileSync with array args to prevent command injection
134
+ const version = execFileSync(cmd, ['--version'], { encoding: 'utf8', stdio: 'pipe' });
135
+ return { available: true, command: cmd, version: version.trim() };
136
+ } catch {
137
+ continue;
138
+ }
139
+ }
140
+
141
+ return { available: false };
142
+ }
143
+
144
+ /**
145
+ * Check if Python MCP package is installed
146
+ */
147
+ function checkMCPPackage(pythonCmd) {
148
+ try {
149
+ // Security: Use execFileSync with array args to prevent command injection
150
+ execFileSync(pythonCmd, ['-c', 'import mcp'], { stdio: 'pipe' });
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get Claude Desktop config path for current platform
159
+ */
160
+ function getClaudeConfigPath() {
161
+ const platform = os.platform();
162
+
163
+ switch (platform) {
164
+ case 'darwin': // macOS
165
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
166
+ case 'win32': // Windows
167
+ return path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
168
+ default: // Linux
169
+ return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Get AgentVibes installation directory
175
+ */
176
+ function getAgentVibesDir() {
177
+ // Try to find AgentVibes directory
178
+ // 1. Current directory
179
+ if (fs.existsSync('./.claude/hooks/play-tts.sh')) {
180
+ return process.cwd();
181
+ }
182
+
183
+ // 2. Parent directory
184
+ const parentDir = path.resolve(process.cwd(), '..');
185
+ if (fs.existsSync(path.join(parentDir, '.claude/hooks/play-tts.sh'))) {
186
+ return parentDir;
187
+ }
188
+
189
+ // 3. Ask user
190
+ return null;
191
+ }
192
+
193
+ /**
194
+ * Update Claude Desktop configuration
195
+ */
196
+ function updateClaudeConfig(agentVibesPath, provider, apiKey = null) {
197
+ const configPath = getClaudeConfigPath();
198
+ const platform = os.platform();
199
+
200
+ // Create config directory if it doesn't exist
201
+ const configDir = path.dirname(configPath);
202
+ if (!fs.existsSync(configDir)) {
203
+ fs.mkdirSync(configDir, { recursive: true });
204
+ }
205
+
206
+ // Read existing config or create new one
207
+ let config = { mcpServers: {} };
208
+ if (fs.existsSync(configPath)) {
209
+ const content = fs.readFileSync(configPath, 'utf8');
210
+ config = JSON.parse(content);
211
+ if (!config.mcpServers) {
212
+ config.mcpServers = {};
213
+ }
214
+ }
215
+
216
+ // Prepare MCP server config
217
+ let serverPath = path.join(agentVibesPath, 'mcp-server', 'server.py');
218
+
219
+ if (platform === 'win32') {
220
+ // Windows: Use WSL
221
+ serverPath = serverPath.replace(/\\/g, '/').replace(/^([A-Z]):/, (match, drive) => {
222
+ return `/mnt/${drive.toLowerCase()}`;
223
+ });
224
+
225
+ config.mcpServers.agentvibes = {
226
+ command: 'wsl',
227
+ args: ['python3', serverPath],
228
+ env: {}
229
+ };
230
+ } else {
231
+ // macOS/Linux: Use native Python
232
+ config.mcpServers.agentvibes = {
233
+ command: 'python3',
234
+ args: [serverPath],
235
+ env: {}
236
+ };
237
+ }
238
+
239
+
240
+ // Write config atomically to prevent race conditions (TOCTOU)
241
+ // Write to temp file first, then rename atomically
242
+ const tempPath = `${configPath}.tmp.${process.pid}`;
243
+ try {
244
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
245
+ fs.renameSync(tempPath, configPath);
246
+ } catch (error) {
247
+ // Clean up temp file if rename fails
248
+ try { fs.unlinkSync(tempPath); } catch { /* ignore cleanup errors */ }
249
+ throw error;
250
+ }
251
+
252
+ return configPath;
253
+ }
254
+
255
+ /**
256
+ * Install Piper TTS
257
+ */
258
+ async function installPiper(useWSL = false) {
259
+ const platform = os.platform();
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'));
279
+ try {
280
+ // Security: execFileSync with array args prevents command injection
281
+ if (useWSL) {
282
+ execFileSync('wsl', ['pipx', 'install', 'piper-tts'], { stdio: 'inherit' });
283
+ } else {
284
+ execFileSync('pipx', ['install', 'piper-tts'], { stdio: 'inherit', env: process.env }); // NOSONAR
285
+ }
286
+ console.log(chalk.green('\n✅ Piper TTS installed!\n'));
287
+ return true;
288
+ } catch (error) {
289
+ console.log(chalk.red(`\n❌ Failed to install Piper TTS: ${error.message}\n`));
290
+ return false;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Install Python MCP package
296
+ */
297
+ async function installMCPPackage(pythonCmd, useWSL = false) {
298
+ const spinner = ora('Installing Python MCP package...').start();
299
+
300
+ try {
301
+ // Security: Use execFileSync with array args to prevent command injection
302
+ if (useWSL) {
303
+ execFileSync('wsl', [pythonCmd, '-m', 'pip', 'install', '--break-system-packages', 'mcp'], { stdio: 'pipe' });
304
+ } else {
305
+ execFileSync(pythonCmd, ['-m', 'pip', 'install', '--user', 'mcp'], { stdio: 'pipe' });
306
+ }
307
+ spinner.succeed('Python MCP package installed successfully!');
308
+ return true;
309
+ } catch (error) {
310
+ spinner.fail('Failed to install Python MCP package');
311
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
312
+ return false;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Main installer
318
+ */
319
+ /**
320
+ * Check system dependencies and handle missing ones
321
+ * @returns {Promise<void>}
322
+ */
323
+ async function checkSystemDependencies() {
324
+ console.log(chalk.bold('🔍 Step 1: Checking system dependencies...\n'));
325
+
326
+ const depResults = checkDependencies();
327
+ const hasMissingDeps = displayMissingDependencies(depResults);
328
+
329
+ if (!hasMissingDeps) {
330
+ console.log(chalk.green(' All dependencies installed!\n'));
331
+ return;
332
+ }
333
+
334
+ const platform = os.platform();
335
+ const hasCoreMissing = depResults.missing.node || depResults.missing.python || depResults.missing.bash;
336
+
337
+ if (hasCoreMissing) {
338
+ console.log(chalk.red('\n❌ Critical dependencies are missing. Please install them before continuing.\n'));
339
+ process.exit(1);
340
+ }
341
+
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
371
+ const { continueAnyway } = await inquirer.prompt([{
372
+ type: 'confirm',
373
+ name: 'continueAnyway',
374
+ message: 'Some optional dependencies are missing. Continue anyway?',
375
+ default: true
376
+ }]);
377
+
378
+ if (!continueAnyway) {
379
+ console.log(chalk.yellow('\nInstallation cancelled. Install the missing dependencies and try again.\n'));
380
+ process.exit(0);
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Locate AgentVibes installation directory
386
+ * @param {boolean} isWindows - Whether running on Windows
387
+ * @returns {Promise<string>} AgentVibes directory path
388
+ */
389
+ async function locateAgentVibesDir(isWindows) {
390
+ console.log(chalk.bold('📁 Step 2: Locating AgentVibes installation...\n'));
391
+
392
+ let agentVibesDir = getAgentVibesDir();
393
+
394
+ if (!agentVibesDir) {
395
+ const { customPath } = await inquirer.prompt([{
396
+ type: 'input',
397
+ name: 'customPath',
398
+ message: 'Enter the path to your AgentVibes installation:',
399
+ default: isWindows ? 'C:\\Users\\USERNAME\\AgentVibes' : '~/AgentVibes',
400
+ validate: (input) => {
401
+ const expanded = input.replace(/^~/, os.homedir());
402
+ if (fs.existsSync(path.join(expanded, '.claude/hooks/play-tts.sh'))) {
403
+ return true;
404
+ }
405
+ return 'AgentVibes not found at this path. Please check and try again.';
406
+ }
407
+ }]);
408
+
409
+ agentVibesDir = customPath.replace(/^~/, os.homedir());
410
+ }
411
+
412
+ console.log(chalk.green(`✓ Found AgentVibes at: ${agentVibesDir}\n`));
413
+ return agentVibesDir;
414
+ }
415
+
416
+ /**
417
+ * Check and setup WSL on Windows
418
+ * @returns {Promise<void>}
419
+ */
420
+ async function setupWindowsWSL() {
421
+ console.log(chalk.bold('🪟 Step 3: Windows environment setup...\n'));
422
+
423
+ const hasWSL = checkWSL();
424
+
425
+ if (hasWSL) {
426
+ console.log(chalk.green('✓ WSL is installed\n'));
427
+ return;
428
+ }
429
+
430
+ console.log(chalk.yellow('⚠️ WSL (Windows Subsystem for Linux) is required but not installed.'));
431
+ const { installWSL } = await inquirer.prompt([{
432
+ type: 'confirm',
433
+ name: 'installWSL',
434
+ message: 'Install WSL now? (Requires restart)',
435
+ default: true
436
+ }]);
437
+
438
+ if (!installWSL) {
439
+ console.log(chalk.red('\n❌ WSL is required for AgentVibes MCP server on Windows'));
440
+ process.exit(1);
441
+ }
442
+
443
+ console.log(chalk.cyan('\n📦 Installing WSL...'));
444
+ try {
445
+ // Security: Use execFileSync with array args to prevent command injection
446
+ execFileSync('wsl', ['--install'], { stdio: 'inherit' });
447
+ console.log(chalk.green('\n✅ WSL installed successfully!'));
448
+ console.log(chalk.yellow('⚠️ Please restart your computer and run this installer again.'));
449
+ process.exit(0);
450
+ } catch (error) {
451
+ console.error(chalk.red('\n❌ Failed to install WSL'));
452
+ console.error(chalk.yellow('Please install WSL manually: https://aka.ms/wsl'));
453
+ process.exit(1);
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Select and install TTS provider
459
+ * @param {boolean} isWindows - Whether running on Windows
460
+ * @returns {Promise<string>} Selected provider name
461
+ */
462
+ async function setupTTSProvider(isWindows) {
463
+ console.log(chalk.bold('🎤 Step 4: Choose TTS provider...\n'));
464
+
465
+ const { provider } = await inquirer.prompt([{
466
+ type: 'list',
467
+ name: 'provider',
468
+ message: 'Select your preferred TTS provider:',
469
+ choices: [
470
+ {
471
+ name: 'Piper TTS (Free, Offline, Open Source) - Recommended',
472
+ value: 'piper',
473
+ short: 'Piper'
474
+ },
475
+ {
476
+ name: 'macOS TTS (Native macOS text-to-speech)',
477
+ value: 'macos',
478
+ short: 'macOS'
479
+ }
480
+ ]
481
+ }]);
482
+
483
+ if (provider === 'piper') {
484
+ console.log(chalk.cyan('\n📦 Installing Piper TTS...'));
485
+ await installPiper(isWindows);
486
+ } else if (provider === 'macos') {
487
+ console.log(chalk.cyan('\n✅ macOS TTS uses native system voices - no installation needed'));
488
+ }
489
+
490
+ return provider;
491
+ }
492
+
493
+ /**
494
+ * Setup Python dependencies
495
+ * @param {boolean} isWindows - Whether running on Windows
496
+ * @returns {Promise<void>}
497
+ */
498
+ async function setupPythonDependencies(isWindows) {
499
+ console.log(chalk.bold('\n🐍 Step 5: Installing Python dependencies...\n'));
500
+
501
+ const pythonCheck = isWindows
502
+ ? { available: true, command: 'python3' } // WSL Python
503
+ : checkPython();
504
+
505
+ if (!pythonCheck.available) {
506
+ console.error(chalk.red('❌ Python not found!'));
507
+ console.log(chalk.yellow('Please install Python 3.10+ from https://python.org'));
508
+ process.exit(1);
509
+ }
510
+
511
+ console.log(chalk.green(`✓ Python found: ${pythonCheck.version || 'python3'}\n`));
512
+
513
+ // Check and install MCP package
514
+ const hasMCP = isWindows
515
+ ? false // Always install in WSL
516
+ : checkMCPPackage(pythonCheck.command);
517
+
518
+ if (!hasMCP) {
519
+ await installMCPPackage(pythonCheck.command, isWindows);
520
+ } else {
521
+ console.log(chalk.green('✓ Python MCP package already installed\n'));
522
+ }
523
+ }
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
+
640
+ /**
641
+ * Display welcome banner
642
+ * @param {string} platform - Platform name
643
+ */
644
+ function showWelcomeBanner(platform) {
645
+ console.log(boxen(
646
+ chalk.bold.cyan('AgentVibes MCP Server Installer') + '\n\n' +
647
+ 'Give Claude Desktop a voice! 🎤',
648
+ {
649
+ padding: 1,
650
+ margin: 1,
651
+ borderStyle: 'round',
652
+ borderColor: 'cyan'
653
+ }
654
+ ));
655
+
656
+ const platformLabel = platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : 'Linux';
657
+ console.log(chalk.gray(`Platform: ${platformLabel}\n`));
658
+ }
659
+
660
+ /**
661
+ * Display success message
662
+ * @param {string} configPath - Config file path
663
+ * @param {string} provider - Provider name
664
+ */
665
+ function showSuccessMessage(configPath, provider) {
666
+ console.log(boxen(
667
+ chalk.bold.green('✅ Installation Complete!') + '\n\n' +
668
+ chalk.white('Next steps:\n') +
669
+ chalk.cyan('1. Restart Claude Desktop\n') +
670
+ chalk.cyan('2. Try: "Say hello using text to speech"\n') +
671
+ chalk.cyan('3. Enjoy your talking Claude! 🎤'),
672
+ {
673
+ padding: 1,
674
+ margin: 1,
675
+ borderStyle: 'round',
676
+ borderColor: 'green'
677
+ }
678
+ ));
679
+
680
+ console.log(chalk.gray('\nConfiguration saved to:'));
681
+ console.log(chalk.gray(` ${configPath}\n`));
682
+
683
+ if (provider === 'piper') {
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');
686
+ }
687
+ }
688
+
689
+ export async function installMCP() {
690
+ const platform = os.platform();
691
+ const isWindows = platform === 'win32';
692
+
693
+ showWelcomeBanner(platform);
694
+
695
+ // Step 1: Check system dependencies
696
+ await checkSystemDependencies();
697
+
698
+ // Step 2: Find AgentVibes directory
699
+ const agentVibesDir = await locateAgentVibesDir(isWindows);
700
+
701
+ // Step 3: Windows-specific checks
702
+ if (isWindows) {
703
+ await setupWindowsWSL();
704
+ }
705
+
706
+ // Step 4: Choose TTS provider
707
+ const provider = await setupTTSProvider(isWindows);
708
+
709
+ // Step 4b: Download voices if Piper was selected
710
+ if (provider === 'piper') {
711
+ await downloadPiperVoices(agentVibesDir);
712
+ }
713
+
714
+ // Step 5: Install Python dependencies
715
+ await setupPythonDependencies(isWindows);
716
+
717
+ // Step 6: Configure provider in AgentVibes
718
+ console.log(chalk.bold('⚙️ Step 6: Configuring AgentVibes...\n'));
719
+ const providerFile = path.join(agentVibesDir, '.claude', 'tts-provider.txt');
720
+ fs.writeFileSync(providerFile, provider);
721
+ console.log(chalk.green(`✓ Set provider to: ${provider}\n`));
722
+
723
+ // Step 7: Update Claude Desktop config
724
+ console.log(chalk.bold('📝 Step 7: Updating Claude Desktop configuration...\n'));
725
+ const configPath = updateClaudeConfig(agentVibesDir, provider);
726
+ console.log(chalk.green(`✓ Updated: ${configPath}\n`));
727
+
728
+ // Success!
729
+ showSuccessMessage(configPath, provider);
730
+ }