agentvibes 4.0.1 → 4.4.0

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 (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +3 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -389
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -112
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -70
  79. package/.claude/hooks/play-tts-macos.sh +368 -345
  80. package/.claude/hooks/play-tts-piper.sh +679 -578
  81. package/.claude/hooks/play-tts-soprano.sh +356 -320
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -88
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -298
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -71
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -0
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -114
  99. package/.claude/hooks/tts-queue.sh +165 -136
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -544
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -181
  149. package/README.md +2029 -1909
  150. package/RELEASE_NOTES.md +1310 -66
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1826
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1417
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -112
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -806
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -0
  182. package/src/console/footer-config.js +50 -46
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -61
  185. package/src/console/tabs/agents-tab.js +1684 -369
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -46
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -0
  192. package/src/console/tabs/settings-tab.js +185 -402
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -0
  195. package/src/console/widgets/format-utils.js +89 -0
  196. package/src/console/widgets/notice.js +55 -0
  197. package/src/console/widgets/personality-picker.js +185 -0
  198. package/src/console/widgets/reverb-picker.js +94 -0
  199. package/src/console/widgets/track-picker.js +285 -0
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5777
  202. package/src/services/agent-voice-store.js +423 -163
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -275
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -162
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. package/.claude/config/background-music-position.txt +0 -1
@@ -1,394 +1,394 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * File: src/commands/bmad-voices.js
5
- *
6
- * AgentVibes - BMAD Voice Management Commands
7
- *
8
- * Provides CLI commands for managing BMAD agent voice assignments.
9
- * These commands make it easy to preview, list, and assign voices
10
- * without manually editing CSV files.
11
- *
12
- * Co-created by Paul Preibisch with Claude AI
13
- * Copyright (c) 2025 Paul Preibisch
14
- * Licensed under the Apache License, Version 2.0
15
- */
16
-
17
- import { execFileSync } from 'node:child_process';
18
- import path from 'node:path';
19
- import fs from 'node:fs/promises';
20
- import chalk from 'chalk';
21
-
22
- // Default BMAD agent voice assignments
23
- const DEFAULT_VOICE_ASSIGNMENTS = [
24
- { agent_id: 'pm', voice_name: 'en_US-ryan-high', description: 'Professional male' },
25
- { agent_id: 'architect', voice_name: 'en_US-danny-low', description: 'Deep male' },
26
- { agent_id: 'dev', voice_name: 'en_US-joe-medium', description: 'Casual male' },
27
- { agent_id: 'analyst', voice_name: 'en_US-amy-medium', description: 'Articulate female' },
28
- { agent_id: 'ux-designer', voice_name: 'en_US-kristin-medium', description: 'Warm female' },
29
- { agent_id: 'tea', voice_name: 'en_US-lessac-medium', description: 'Balanced neutral' },
30
- { agent_id: 'sm', voice_name: 'en_US-bryce-medium', description: 'Energetic male' },
31
- { agent_id: 'tech-writer', voice_name: 'en_US-kathleen-low', description: 'Clear female' },
32
- { agent_id: 'frame-expert', voice_name: 'en_US-kusal-medium', description: 'Precise male' },
33
- { agent_id: 'bmad-master', voice_name: 'en_US-libritts_r-high', description: 'Rich commanding' },
34
- ];
35
-
36
- // All available Piper voices with descriptions
37
- const PIPER_VOICES = {
38
- 'en_US-ryan-high': 'Professional, clear male voice',
39
- 'en_US-danny-low': 'Deep, authoritative male voice',
40
- 'en_US-joe-medium': 'Casual, friendly male voice',
41
- 'en_US-amy-medium': 'Articulate female voice',
42
- 'en_US-kristin-medium': 'Warm, professional female voice',
43
- 'en_US-lessac-medium': 'Balanced, neutral voice',
44
- 'en_US-bryce-medium': 'Energetic male voice',
45
- 'en_US-kathleen-low': 'Clear, measured female voice',
46
- 'en_US-kusal-medium': 'Precise male voice',
47
- 'en_US-libritts_r-high': 'Rich, commanding voice',
48
- 'en_US-hfc_female-medium': 'Clear female voice',
49
- 'en_US-hfc_male-medium': 'Clear male voice',
50
- 'en_US-arctic-medium': 'Crisp, neutral voice',
51
- };
52
-
53
- /**
54
- * Get the working directory (handles npx context)
55
- */
56
- function getWorkingDirectory() {
57
- return process.env.INIT_CWD || process.cwd();
58
- }
59
-
60
- /**
61
- * Check if BMAD is installed
62
- */
63
- async function isBmadInstalled() {
64
- const targetDir = getWorkingDirectory();
65
- const bmadPath = path.join(targetDir, '.bmad');
66
-
67
- try {
68
- await fs.access(bmadPath);
69
- return true;
70
- } catch {
71
- // Try legacy location
72
- const legacyPath = path.join(targetDir, 'bmad');
73
- try {
74
- await fs.access(legacyPath);
75
- return true;
76
- } catch {
77
- return false;
78
- }
79
- }
80
- }
81
-
82
- /**
83
- * Get BMAD folder path (.bmad or bmad)
84
- */
85
- async function getBmadPath() {
86
- const targetDir = getWorkingDirectory();
87
- const modernPath = path.join(targetDir, '.bmad');
88
- const legacyPath = path.join(targetDir, 'bmad');
89
-
90
- try {
91
- await fs.access(modernPath);
92
- return modernPath;
93
- } catch {
94
- try {
95
- await fs.access(legacyPath);
96
- return legacyPath;
97
- } catch {
98
- return null;
99
- }
100
- }
101
- }
102
-
103
- /**
104
- * Read BMAD agent voice assignments
105
- */
106
- async function readVoiceAssignments() {
107
- const bmadPath = await getBmadPath();
108
- if (!bmadPath) {
109
- return null;
110
- }
111
-
112
- const csvPath = path.join(bmadPath, '_cfg', 'agent-voice-map.csv');
113
-
114
- try {
115
- const content = await fs.readFile(csvPath, 'utf8');
116
- const lines = content.trim().split('\n');
117
- const assignments = [];
118
-
119
- // Skip header
120
- for (let i = 1; i < lines.length; i++) {
121
- const [agent_id, voice_name] = lines[i].split(',');
122
- if (agent_id && voice_name) {
123
- assignments.push({ agent_id, voice_name });
124
- }
125
- }
126
-
127
- return assignments;
128
- } catch {
129
- return null;
130
- }
131
- }
132
-
133
- /**
134
- * Write BMAD agent voice assignments
135
- */
136
- async function writeVoiceAssignments(assignments) {
137
- const bmadPath = await getBmadPath();
138
- if (!bmadPath) {
139
- throw new Error('BMAD not found');
140
- }
141
-
142
- const cfgDir = path.join(bmadPath, '_cfg');
143
- const csvPath = path.join(cfgDir, 'agent-voice-map.csv');
144
-
145
- // Ensure directory exists
146
- await fs.mkdir(cfgDir, { recursive: true });
147
-
148
- // Build CSV content
149
- const lines = ['agent_id,voice_name'];
150
- for (const { agent_id, voice_name } of assignments) {
151
- lines.push(`${agent_id},${voice_name}`);
152
- }
153
-
154
- await fs.writeFile(csvPath, lines.join('\n') + '\n', 'utf8');
155
- }
156
-
157
- /**
158
- * Find matching voice name using fuzzy matching
159
- * Supports partial matches like "ryan" → "en_US-ryan-high"
160
- */
161
- function findVoiceMatch(input) {
162
- const lowerInput = input.toLowerCase();
163
-
164
- // Exact match
165
- if (PIPER_VOICES[input]) {
166
- return input;
167
- }
168
-
169
- // Find voices containing the input string
170
- const matches = Object.keys(PIPER_VOICES).filter(voice =>
171
- voice.toLowerCase().includes(lowerInput)
172
- );
173
-
174
- if (matches.length === 1) {
175
- return matches[0];
176
- }
177
-
178
- if (matches.length > 1) {
179
- // Return the shortest match (most specific)
180
- // Initial value prevents TypeError if array were empty
181
- return matches.reduce((a, b) => (a.length <= b.length ? a : b), matches[0]);
182
- }
183
-
184
- return null;
185
- }
186
-
187
- /**
188
- * Command: preview-voice <voice-name>
189
- * Preview a voice with sample text
190
- */
191
- export async function previewVoice(voiceName, options = {}) {
192
- const text = options.text || 'Hello! This is a voice preview for AgentVibes.';
193
-
194
- // Try fuzzy matching
195
- const matchedVoice = findVoiceMatch(voiceName);
196
-
197
- if (!matchedVoice) {
198
- console.error(chalk.red(`\nāŒ Voice "${voiceName}" not found.`));
199
- console.error(chalk.gray(' View available voices: npx agentvibes list-available-voices\n'));
200
- process.exit(1);
201
- }
202
-
203
- if (matchedVoice !== voiceName) {
204
- console.log(chalk.yellow(`\nšŸ’” Matched "${voiceName}" to "${matchedVoice}"\n`));
205
- }
206
-
207
- console.log(chalk.cyan(`šŸ”Š Previewing voice: ${matchedVoice}\n`));
208
- console.log(chalk.gray(` Text: "${text}"\n`));
209
-
210
- const targetDir = getWorkingDirectory();
211
- const playTtsPath = path.join(targetDir, '.claude', 'hooks', 'play-tts.sh');
212
-
213
- try {
214
- await fs.access(playTtsPath);
215
- } catch {
216
- console.error(chalk.red('āŒ AgentVibes not installed in this directory.'));
217
- console.error(chalk.gray(' Run: npx agentvibes install\n'));
218
- process.exit(1);
219
- }
220
-
221
- try {
222
- // Security: Use execFileSync with array args to prevent command injection
223
- execFileSync('bash', [playTtsPath, text, matchedVoice], {
224
- stdio: 'inherit',
225
- cwd: targetDir,
226
- });
227
- } catch (error) {
228
- console.error(chalk.red('\nāŒ Failed to play voice preview'));
229
- console.error(chalk.gray(` Voice: ${matchedVoice}`));
230
- console.error(chalk.gray(` Error: ${error.message}\n`));
231
- process.exit(1);
232
- }
233
- }
234
-
235
- /**
236
- * Command: list-available-voices
237
- * Show all available voices grouped by provider
238
- */
239
- export async function listAvailableVoices() {
240
- console.log(chalk.cyan('\nšŸ“‹ Available Voices:\n'));
241
-
242
- console.log(chalk.white.bold('Piper TTS Voices (Free):'));
243
- console.log(chalk.gray('─'.repeat(60)));
244
-
245
- for (const [voice, description] of Object.entries(PIPER_VOICES)) {
246
- console.log(chalk.cyan(` ${voice.padEnd(30)}`) + chalk.gray(description));
247
- }
248
-
249
- console.log('');
250
- console.log(chalk.gray('šŸ’” Tip: Preview voices with:'));
251
- console.log(chalk.white(' npx agentvibes preview-voice <voice-name>'));
252
- console.log('');
253
- }
254
-
255
- /**
256
- * Command: list-bmad-assigned-voices
257
- * Show all BMAD agents with their current voice assignments
258
- */
259
- export async function listBmadAssignedVoices() {
260
- if (!(await isBmadInstalled())) {
261
- console.error(chalk.red('\nāŒ BMAD not found in this directory.'));
262
- console.error(chalk.gray(' Install BMAD first: npx bmad install\n'));
263
- process.exit(1);
264
- }
265
-
266
- const assignments = await readVoiceAssignments();
267
-
268
- if (!assignments || assignments.length === 0) {
269
- console.error(chalk.yellow('\nāš ļø No voice assignments found.'));
270
- console.error(chalk.gray(' Create assignments with: npx agentvibes reset-bmad-voices\n'));
271
- process.exit(1);
272
- }
273
-
274
- console.log(chalk.cyan('\nšŸŽ™ļø BMAD Agent Voice Assignments:\n'));
275
- console.log(chalk.gray('─'.repeat(70)));
276
-
277
- for (const { agent_id, voice_name } of assignments) {
278
- const description = PIPER_VOICES[voice_name] || 'Unknown voice';
279
- const paddedAgent = agent_id.padEnd(20);
280
- console.log(
281
- chalk.white(` ${paddedAgent}`) +
282
- chalk.cyan('→ ') +
283
- chalk.yellow(voice_name.padEnd(30)) +
284
- chalk.gray(`(${description})`)
285
- );
286
- }
287
-
288
- console.log('');
289
- console.log(chalk.gray('šŸ’” Tip: Change assignments with:'));
290
- console.log(chalk.white(' npx agentvibes assign-voice <agent-id> <voice-name>'));
291
- console.log('');
292
- }
293
-
294
- /**
295
- * Command: assign-voice <agent-id> <voice-name>
296
- * Assign a voice to a specific BMAD agent
297
- */
298
- export async function assignVoice(agentId, voiceName) {
299
- if (!(await isBmadInstalled())) {
300
- console.error(chalk.red('\nāŒ BMAD not found in this directory.'));
301
- console.error(chalk.gray(' Install BMAD first: npx bmad install\n'));
302
- process.exit(1);
303
- }
304
-
305
- // Validate voice exists
306
- if (!PIPER_VOICES[voiceName]) {
307
- console.error(chalk.red(`\nāŒ Voice "${voiceName}" not found.`));
308
- console.error(chalk.gray(' View available voices: npx agentvibes list-available-voices\n'));
309
- process.exit(1);
310
- }
311
-
312
- let assignments = await readVoiceAssignments();
313
-
314
- if (!assignments) {
315
- console.log(chalk.yellow('āš ļø No voice assignments found. Creating default assignments...\n'));
316
- assignments = DEFAULT_VOICE_ASSIGNMENTS.map(({ agent_id, voice_name }) => ({
317
- agent_id,
318
- voice_name,
319
- }));
320
- }
321
-
322
- // Find and update assignment
323
- const existing = assignments.find(a => a.agent_id === agentId);
324
-
325
- if (existing) {
326
- const oldVoice = existing.voice_name;
327
- existing.voice_name = voiceName;
328
- console.log(chalk.green(`\nāœ“ Updated ${agentId} voice assignment:`));
329
- console.log(chalk.gray(` ${oldVoice} → ${voiceName}\n`));
330
- } else {
331
- assignments.push({ agent_id: agentId, voice_name: voiceName });
332
- console.log(chalk.green(`\nāœ“ Created new voice assignment for ${agentId}:`));
333
- console.log(chalk.gray(` Voice: ${voiceName}\n`));
334
- }
335
-
336
- await writeVoiceAssignments(assignments);
337
-
338
- console.log(chalk.gray('šŸ’” Test the voice with:'));
339
- console.log(chalk.white(` npx agentvibes preview-voice ${voiceName}\n`));
340
- }
341
-
342
- /**
343
- * Command: reset-bmad-voices
344
- * Reset all BMAD agents to default voice assignments
345
- */
346
- export async function resetBmadVoices(options = {}) {
347
- if (!(await isBmadInstalled())) {
348
- console.error(chalk.red('\nāŒ BMAD not found in this directory.'));
349
- console.error(chalk.gray(' Install BMAD first: npx bmad install\n'));
350
- process.exit(1);
351
- }
352
-
353
- console.log(chalk.yellow('\nāš ļø Reset BMAD Voice Assignments\n'));
354
- console.log(chalk.gray('This will reset all agent voices to defaults:\n'));
355
-
356
- for (const { agent_id, voice_name, description } of DEFAULT_VOICE_ASSIGNMENTS) {
357
- console.log(chalk.gray(` ${agent_id.padEnd(15)} → ${voice_name.padEnd(25)} (${description})`));
358
- }
359
-
360
- console.log('');
361
-
362
- if (!options.yes) {
363
- const readline = await import('node:readline');
364
- const rl = readline.createInterface({
365
- input: process.stdin,
366
- output: process.stdout,
367
- });
368
-
369
- const answer = await new Promise(resolve => {
370
- rl.question(chalk.cyan('Continue with reset? (y/N): '), answer => {
371
- rl.close();
372
- resolve(answer);
373
- });
374
- });
375
-
376
- if (answer.toLowerCase() !== 'y') {
377
- console.log(chalk.red('\nāŒ Reset cancelled.\n'));
378
- process.exit(0);
379
- }
380
- } else {
381
- console.log(chalk.green('āœ“ Auto-confirmed (--yes flag)\n'));
382
- }
383
-
384
- const assignments = DEFAULT_VOICE_ASSIGNMENTS.map(({ agent_id, voice_name }) => ({
385
- agent_id,
386
- voice_name,
387
- }));
388
-
389
- await writeVoiceAssignments(assignments);
390
-
391
- console.log(chalk.green('\nāœ“ Voice assignments reset to defaults!\n'));
392
- console.log(chalk.gray('šŸ’” View assignments with:'));
393
- console.log(chalk.white(' npx agentvibes list-bmad-assigned-voices\n'));
394
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * File: src/commands/bmad-voices.js
5
+ *
6
+ * AgentVibes - BMAD Voice Management Commands
7
+ *
8
+ * Provides CLI commands for managing BMAD agent voice assignments.
9
+ * These commands make it easy to preview, list, and assign voices
10
+ * without manually editing CSV files.
11
+ *
12
+ * Co-created by Paul Preibisch with Claude AI
13
+ * Copyright (c) 2025 Paul Preibisch
14
+ * Licensed under the Apache License, Version 2.0
15
+ */
16
+
17
+ import { execFileSync } from 'node:child_process';
18
+ import path from 'node:path';
19
+ import fs from 'node:fs/promises';
20
+ import chalk from 'chalk';
21
+
22
+ // Default BMAD agent voice assignments
23
+ const DEFAULT_VOICE_ASSIGNMENTS = [
24
+ { agent_id: 'pm', voice_name: 'en_US-ryan-high', description: 'Professional male' },
25
+ { agent_id: 'architect', voice_name: 'en_US-danny-low', description: 'Deep male' },
26
+ { agent_id: 'dev', voice_name: 'en_US-joe-medium', description: 'Casual male' },
27
+ { agent_id: 'analyst', voice_name: 'en_US-amy-medium', description: 'Articulate female' },
28
+ { agent_id: 'ux-designer', voice_name: 'en_US-kristin-medium', description: 'Warm female' },
29
+ { agent_id: 'tea', voice_name: 'en_US-lessac-medium', description: 'Balanced neutral' },
30
+ { agent_id: 'sm', voice_name: 'en_US-bryce-medium', description: 'Energetic male' },
31
+ { agent_id: 'tech-writer', voice_name: 'en_US-kathleen-low', description: 'Clear female' },
32
+ { agent_id: 'frame-expert', voice_name: 'en_US-kusal-medium', description: 'Precise male' },
33
+ { agent_id: 'bmad-master', voice_name: 'en_US-libritts_r-high', description: 'Rich commanding' },
34
+ ];
35
+
36
+ // All available Piper voices with descriptions
37
+ const PIPER_VOICES = {
38
+ 'en_US-ryan-high': 'Professional, clear male voice',
39
+ 'en_US-danny-low': 'Deep, authoritative male voice',
40
+ 'en_US-joe-medium': 'Casual, friendly male voice',
41
+ 'en_US-amy-medium': 'Articulate female voice',
42
+ 'en_US-kristin-medium': 'Warm, professional female voice',
43
+ 'en_US-lessac-medium': 'Balanced, neutral voice',
44
+ 'en_US-bryce-medium': 'Energetic male voice',
45
+ 'en_US-kathleen-low': 'Clear, measured female voice',
46
+ 'en_US-kusal-medium': 'Precise male voice',
47
+ 'en_US-libritts_r-high': 'Rich, commanding voice',
48
+ 'en_US-hfc_female-medium': 'Clear female voice',
49
+ 'en_US-hfc_male-medium': 'Clear male voice',
50
+ 'en_US-arctic-medium': 'Crisp, neutral voice',
51
+ };
52
+
53
+ /**
54
+ * Get the working directory (handles npx context)
55
+ */
56
+ function getWorkingDirectory() {
57
+ return process.env.INIT_CWD || process.cwd();
58
+ }
59
+
60
+ /**
61
+ * Check if BMAD is installed
62
+ */
63
+ async function isBmadInstalled() {
64
+ const targetDir = getWorkingDirectory();
65
+ const bmadPath = path.join(targetDir, '.bmad');
66
+
67
+ try {
68
+ await fs.access(bmadPath);
69
+ return true;
70
+ } catch {
71
+ // Try legacy location
72
+ const legacyPath = path.join(targetDir, 'bmad');
73
+ try {
74
+ await fs.access(legacyPath);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get BMAD folder path (.bmad or bmad)
84
+ */
85
+ async function getBmadPath() {
86
+ const targetDir = getWorkingDirectory();
87
+ const modernPath = path.join(targetDir, '.bmad');
88
+ const legacyPath = path.join(targetDir, 'bmad');
89
+
90
+ try {
91
+ await fs.access(modernPath);
92
+ return modernPath;
93
+ } catch {
94
+ try {
95
+ await fs.access(legacyPath);
96
+ return legacyPath;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Read BMAD agent voice assignments
105
+ */
106
+ async function readVoiceAssignments() {
107
+ const bmadPath = await getBmadPath();
108
+ if (!bmadPath) {
109
+ return null;
110
+ }
111
+
112
+ const csvPath = path.join(bmadPath, '_cfg', 'agent-voice-map.csv');
113
+
114
+ try {
115
+ const content = await fs.readFile(csvPath, 'utf8');
116
+ const lines = content.trim().split('\n');
117
+ const assignments = [];
118
+
119
+ // Skip header
120
+ for (let i = 1; i < lines.length; i++) {
121
+ const [agent_id, voice_name] = lines[i].split(',');
122
+ if (agent_id && voice_name) {
123
+ assignments.push({ agent_id, voice_name });
124
+ }
125
+ }
126
+
127
+ return assignments;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Write BMAD agent voice assignments
135
+ */
136
+ async function writeVoiceAssignments(assignments) {
137
+ const bmadPath = await getBmadPath();
138
+ if (!bmadPath) {
139
+ throw new Error('BMAD not found');
140
+ }
141
+
142
+ const cfgDir = path.join(bmadPath, '_cfg');
143
+ const csvPath = path.join(cfgDir, 'agent-voice-map.csv');
144
+
145
+ // Ensure directory exists
146
+ await fs.mkdir(cfgDir, { recursive: true });
147
+
148
+ // Build CSV content
149
+ const lines = ['agent_id,voice_name'];
150
+ for (const { agent_id, voice_name } of assignments) {
151
+ lines.push(`${agent_id},${voice_name}`);
152
+ }
153
+
154
+ await fs.writeFile(csvPath, lines.join('\n') + '\n', 'utf8');
155
+ }
156
+
157
+ /**
158
+ * Find matching voice name using fuzzy matching
159
+ * Supports partial matches like "ryan" → "en_US-ryan-high"
160
+ */
161
+ function findVoiceMatch(input) {
162
+ const lowerInput = input.toLowerCase();
163
+
164
+ // Exact match
165
+ if (PIPER_VOICES[input]) {
166
+ return input;
167
+ }
168
+
169
+ // Find voices containing the input string
170
+ const matches = Object.keys(PIPER_VOICES).filter(voice =>
171
+ voice.toLowerCase().includes(lowerInput)
172
+ );
173
+
174
+ if (matches.length === 1) {
175
+ return matches[0];
176
+ }
177
+
178
+ if (matches.length > 1) {
179
+ // Return the shortest match (most specific)
180
+ // Initial value prevents TypeError if array were empty
181
+ return matches.reduce((a, b) => (a.length <= b.length ? a : b), matches[0]);
182
+ }
183
+
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Command: preview-voice <voice-name>
189
+ * Preview a voice with sample text
190
+ */
191
+ export async function previewVoice(voiceName, options = {}) {
192
+ const text = options.text || 'Hello! This is a voice preview for AgentVibes.';
193
+
194
+ // Try fuzzy matching
195
+ const matchedVoice = findVoiceMatch(voiceName);
196
+
197
+ if (!matchedVoice) {
198
+ console.error(chalk.red(`\nāŒ Voice "${voiceName}" not found.`));
199
+ console.error(chalk.gray(' View available voices: npx agentvibes list-available-voices\n'));
200
+ process.exit(1);
201
+ }
202
+
203
+ if (matchedVoice !== voiceName) {
204
+ console.log(chalk.yellow(`\nšŸ’” Matched "${voiceName}" to "${matchedVoice}"\n`));
205
+ }
206
+
207
+ console.log(chalk.cyan(`šŸ”Š Previewing voice: ${matchedVoice}\n`));
208
+ console.log(chalk.gray(` Text: "${text}"\n`));
209
+
210
+ const targetDir = getWorkingDirectory();
211
+ const playTtsPath = path.join(targetDir, '.claude', 'hooks', 'play-tts.sh');
212
+
213
+ try {
214
+ await fs.access(playTtsPath);
215
+ } catch {
216
+ console.error(chalk.red('āŒ AgentVibes not installed in this directory.'));
217
+ console.error(chalk.gray(' Run: npx agentvibes install\n'));
218
+ process.exit(1);
219
+ }
220
+
221
+ try {
222
+ // Security: Use execFileSync with array args to prevent command injection
223
+ execFileSync('bash', [playTtsPath, text, matchedVoice], {
224
+ stdio: 'inherit',
225
+ cwd: targetDir,
226
+ });
227
+ } catch (error) {
228
+ console.error(chalk.red('\nāŒ Failed to play voice preview'));
229
+ console.error(chalk.gray(` Voice: ${matchedVoice}`));
230
+ console.error(chalk.gray(` Error: ${error.message}\n`));
231
+ process.exit(1);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Command: list-available-voices
237
+ * Show all available voices grouped by provider
238
+ */
239
+ export async function listAvailableVoices() {
240
+ console.log(chalk.cyan('\nšŸ“‹ Available Voices:\n'));
241
+
242
+ console.log(chalk.white.bold('Piper TTS Voices (Free):'));
243
+ console.log(chalk.gray('─'.repeat(60)));
244
+
245
+ for (const [voice, description] of Object.entries(PIPER_VOICES)) {
246
+ console.log(chalk.cyan(` ${voice.padEnd(30)}`) + chalk.gray(description));
247
+ }
248
+
249
+ console.log('');
250
+ console.log(chalk.gray('šŸ’” Tip: Preview voices with:'));
251
+ console.log(chalk.white(' npx agentvibes preview-voice <voice-name>'));
252
+ console.log('');
253
+ }
254
+
255
+ /**
256
+ * Command: list-bmad-assigned-voices
257
+ * Show all BMAD agents with their current voice assignments
258
+ */
259
+ export async function listBmadAssignedVoices() {
260
+ if (!(await isBmadInstalled())) {
261
+ console.error(chalk.red('\nāŒ BMAD not found in this directory.'));
262
+ console.error(chalk.gray(' Install BMAD first: npx bmad install\n'));
263
+ process.exit(1);
264
+ }
265
+
266
+ const assignments = await readVoiceAssignments();
267
+
268
+ if (!assignments || assignments.length === 0) {
269
+ console.error(chalk.yellow('\nāš ļø No voice assignments found.'));
270
+ console.error(chalk.gray(' Create assignments with: npx agentvibes reset-bmad-voices\n'));
271
+ process.exit(1);
272
+ }
273
+
274
+ console.log(chalk.cyan('\nšŸŽ™ļø BMAD Agent Voice Assignments:\n'));
275
+ console.log(chalk.gray('─'.repeat(70)));
276
+
277
+ for (const { agent_id, voice_name } of assignments) {
278
+ const description = PIPER_VOICES[voice_name] || 'Unknown voice';
279
+ const paddedAgent = agent_id.padEnd(20);
280
+ console.log(
281
+ chalk.white(` ${paddedAgent}`) +
282
+ chalk.cyan('→ ') +
283
+ chalk.yellow(voice_name.padEnd(30)) +
284
+ chalk.gray(`(${description})`)
285
+ );
286
+ }
287
+
288
+ console.log('');
289
+ console.log(chalk.gray('šŸ’” Tip: Change assignments with:'));
290
+ console.log(chalk.white(' npx agentvibes assign-voice <agent-id> <voice-name>'));
291
+ console.log('');
292
+ }
293
+
294
+ /**
295
+ * Command: assign-voice <agent-id> <voice-name>
296
+ * Assign a voice to a specific BMAD agent
297
+ */
298
+ export async function assignVoice(agentId, voiceName) {
299
+ if (!(await isBmadInstalled())) {
300
+ console.error(chalk.red('\nāŒ BMAD not found in this directory.'));
301
+ console.error(chalk.gray(' Install BMAD first: npx bmad install\n'));
302
+ process.exit(1);
303
+ }
304
+
305
+ // Validate voice exists
306
+ if (!PIPER_VOICES[voiceName]) {
307
+ console.error(chalk.red(`\nāŒ Voice "${voiceName}" not found.`));
308
+ console.error(chalk.gray(' View available voices: npx agentvibes list-available-voices\n'));
309
+ process.exit(1);
310
+ }
311
+
312
+ let assignments = await readVoiceAssignments();
313
+
314
+ if (!assignments) {
315
+ console.log(chalk.yellow('āš ļø No voice assignments found. Creating default assignments...\n'));
316
+ assignments = DEFAULT_VOICE_ASSIGNMENTS.map(({ agent_id, voice_name }) => ({
317
+ agent_id,
318
+ voice_name,
319
+ }));
320
+ }
321
+
322
+ // Find and update assignment
323
+ const existing = assignments.find(a => a.agent_id === agentId);
324
+
325
+ if (existing) {
326
+ const oldVoice = existing.voice_name;
327
+ existing.voice_name = voiceName;
328
+ console.log(chalk.green(`\nāœ“ Updated ${agentId} voice assignment:`));
329
+ console.log(chalk.gray(` ${oldVoice} → ${voiceName}\n`));
330
+ } else {
331
+ assignments.push({ agent_id: agentId, voice_name: voiceName });
332
+ console.log(chalk.green(`\nāœ“ Created new voice assignment for ${agentId}:`));
333
+ console.log(chalk.gray(` Voice: ${voiceName}\n`));
334
+ }
335
+
336
+ await writeVoiceAssignments(assignments);
337
+
338
+ console.log(chalk.gray('šŸ’” Test the voice with:'));
339
+ console.log(chalk.white(` npx agentvibes preview-voice ${voiceName}\n`));
340
+ }
341
+
342
+ /**
343
+ * Command: reset-bmad-voices
344
+ * Reset all BMAD agents to default voice assignments
345
+ */
346
+ export async function resetBmadVoices(options = {}) {
347
+ if (!(await isBmadInstalled())) {
348
+ console.error(chalk.red('\nāŒ BMAD not found in this directory.'));
349
+ console.error(chalk.gray(' Install BMAD first: npx bmad install\n'));
350
+ process.exit(1);
351
+ }
352
+
353
+ console.log(chalk.yellow('\nāš ļø Reset BMAD Voice Assignments\n'));
354
+ console.log(chalk.gray('This will reset all agent voices to defaults:\n'));
355
+
356
+ for (const { agent_id, voice_name, description } of DEFAULT_VOICE_ASSIGNMENTS) {
357
+ console.log(chalk.gray(` ${agent_id.padEnd(15)} → ${voice_name.padEnd(25)} (${description})`));
358
+ }
359
+
360
+ console.log('');
361
+
362
+ if (!options.yes) {
363
+ const readline = await import('node:readline');
364
+ const rl = readline.createInterface({
365
+ input: process.stdin,
366
+ output: process.stdout,
367
+ });
368
+
369
+ const answer = await new Promise(resolve => {
370
+ rl.question(chalk.cyan('Continue with reset? (y/N): '), answer => {
371
+ rl.close();
372
+ resolve(answer);
373
+ });
374
+ });
375
+
376
+ if (answer.toLowerCase() !== 'y') {
377
+ console.log(chalk.red('\nāŒ Reset cancelled.\n'));
378
+ process.exit(0);
379
+ }
380
+ } else {
381
+ console.log(chalk.green('āœ“ Auto-confirmed (--yes flag)\n'));
382
+ }
383
+
384
+ const assignments = DEFAULT_VOICE_ASSIGNMENTS.map(({ agent_id, voice_name }) => ({
385
+ agent_id,
386
+ voice_name,
387
+ }));
388
+
389
+ await writeVoiceAssignments(assignments);
390
+
391
+ console.log(chalk.green('\nāœ“ Voice assignments reset to defaults!\n'));
392
+ console.log(chalk.gray('šŸ’” View assignments with:'));
393
+ console.log(chalk.white(' npx agentvibes list-bmad-assigned-voices\n'));
394
+ }