agentvibes 5.6.0 → 5.6.2

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 (101) hide show
  1. package/.agentvibes/config.json +3 -38
  2. package/.claude/config/audio-effects.cfg +1 -1
  3. package/.claude/config/background-music-enabled.txt +1 -1
  4. package/.claude/config/background-music-position.txt +6 -6
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/play-tts-ssh-remote.sh +119 -42
  7. package/.claude/hooks/play-tts-windows-receiver.sh +31 -0
  8. package/.claude/hooks/stop.sh +2 -27
  9. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  10. package/.claude/hooks-windows/play-tts.ps1 +58 -8
  11. package/.claude/piper-voices-dir.txt +1 -1
  12. package/.clawdbot/skill/README.md +326 -0
  13. package/.mcp.json +17 -27
  14. package/README.md +15 -2
  15. package/RELEASE_NOTES.md +64 -0
  16. package/bin/agent-vibes +39 -39
  17. package/package.json +1 -1
  18. package/src/bmad-detector.js +71 -71
  19. package/src/cli/list-personalities.js +110 -110
  20. package/src/cli/list-voices.js +114 -114
  21. package/src/commands/bmad-voices.js +394 -394
  22. package/src/commands/install-mcp.js +476 -476
  23. package/src/console/brand-colors.js +13 -13
  24. package/src/console/constants/personalities.js +44 -44
  25. package/src/console/modals/modal-overlay.js +247 -247
  26. package/src/console/navigation.js +5 -1
  27. package/src/console/tabs/agents-tab.js +5 -5
  28. package/src/console/tabs/help-tab.js +314 -314
  29. package/src/console/tabs/readme-tab.js +272 -272
  30. package/src/console/tabs/setup-tab.js +32 -17
  31. package/src/console/tabs/voices-tab.js +2 -2
  32. package/src/console/widgets/destroy-list.js +25 -25
  33. package/src/console/widgets/notice.js +55 -55
  34. package/src/console/widgets/personality-picker.js +213 -213
  35. package/src/console/widgets/reverb-picker.js +97 -97
  36. package/src/console/widgets/track-picker.js +1 -1
  37. package/src/i18n/de.js +202 -202
  38. package/src/i18n/es.js +202 -202
  39. package/src/i18n/fr.js +202 -202
  40. package/src/i18n/hi.js +202 -202
  41. package/src/i18n/ja.js +202 -202
  42. package/src/i18n/ko.js +202 -202
  43. package/src/i18n/pt.js +202 -202
  44. package/src/i18n/strings.js +54 -54
  45. package/src/i18n/zh-CN.js +202 -202
  46. package/src/installer/language-screen.js +31 -31
  47. package/src/installer/music-file-input.js +304 -304
  48. package/src/services/agent-voice-store.js +420 -423
  49. package/src/services/config-service.js +264 -264
  50. package/src/services/language-service.js +47 -47
  51. package/src/services/llm-provider-service.js +11 -4
  52. package/src/services/navigation-service.js +34 -10
  53. package/src/services/provider-service.js +143 -143
  54. package/src/utils/audio-duration-validator.js +298 -298
  55. package/src/utils/audio-format-validator.js +277 -277
  56. package/src/utils/dependency-checker.js +469 -469
  57. package/src/utils/file-ownership-verifier.js +358 -358
  58. package/src/utils/list-formatter.js +194 -194
  59. package/src/utils/music-file-validator.js +285 -285
  60. package/src/utils/preview-list-prompt.js +136 -136
  61. package/src/utils/secure-music-storage.js +412 -412
  62. package/.agentvibes/LITE-MODE.md +0 -236
  63. package/.agentvibes/README.md +0 -136
  64. package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +0 -141
  65. package/.agentvibes/backups/agents/analyst_20260204_144958.md +0 -78
  66. package/.agentvibes/backups/agents/architect_20260204_144958.md +0 -72
  67. package/.agentvibes/backups/agents/dev_20260204_144958.md +0 -74
  68. package/.agentvibes/backups/agents/pm_20260204_144958.md +0 -72
  69. package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +0 -64
  70. package/.agentvibes/backups/agents/sm_20260204_144958.md +0 -87
  71. package/.agentvibes/backups/agents/tea_20260204_144958.md +0 -79
  72. package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +0 -82
  73. package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +0 -80
  74. package/.agentvibes/config/README-personality-defaults.md +0 -162
  75. package/.agentvibes/config/agentvibes.json +0 -1
  76. package/.agentvibes/config/mode.txt +0 -1
  77. package/.agentvibes/config/personality-voice-defaults.default.json +0 -21
  78. package/.agentvibes/config/save-audio.txt +0 -1
  79. package/.agentvibes/config/voice-metadata.json +0 -160
  80. package/.agentvibes/hooks/help.sh +0 -191
  81. package/.agentvibes/hooks/post-tool-use-lite.sh +0 -111
  82. package/.agentvibes/hooks/save-audio-manager.sh +0 -162
  83. package/.agentvibes/hooks/session-start-full-optimized.sh +0 -102
  84. package/.agentvibes/hooks/session-start-full.sh +0 -142
  85. package/.agentvibes/hooks/session-start-lite-v2.sh +0 -34
  86. package/.agentvibes/hooks/session-start-lite.sh +0 -29
  87. package/.agentvibes/hooks/stop-lite.sh +0 -115
  88. package/.agentvibes/hooks/switch-mode.sh +0 -215
  89. package/.agentvibes/output-styles/audio-summary.md +0 -30
  90. package/.claude/audio/voice-samples/piper/alan.wav +0 -0
  91. package/.claude/audio/voice-samples/piper/amy.wav +0 -0
  92. package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
  93. package/.claude/audio/voice-samples/piper/joe.wav +0 -0
  94. package/.claude/audio/voice-samples/piper/john.wav +0 -0
  95. package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
  96. package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
  97. package/.claude/audio/voice-samples/piper/linda.wav +0 -0
  98. package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
  99. package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
  100. package/.claude/hooks/post-response.sh +0 -41
  101. package/bin/ensure-soprano-running.sh +0 -43
@@ -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
+ }