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
package/bin/agent-vibes CHANGED
@@ -1,40 +1,40 @@
1
1
  #!/usr/bin/env node
2
-
3
- /**
4
- * AgentVibes - Beautiful ElevenLabs TTS voice commands for Claude Code
5
- * This file ensures proper execution when run via npx
6
- */
7
-
8
- import { execFileSync } from 'node:child_process';
9
- import path from 'node:path';
10
- import fs from 'node:fs';
11
- import { fileURLToPath } from 'node:url';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
15
-
16
- // Check if we're running in an npx temporary directory
17
- const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm');
18
-
19
- // Get CLI arguments
20
- const arguments_ = process.argv.slice(2);
21
-
22
- // Route through the TUI console (agentvibes.js) which handles install/config/etc
23
- const installerPath = path.join(__dirname, 'agentvibes.js');
24
-
25
- if (!fs.existsSync(installerPath)) {
26
- console.error('Error: Could not find installer.js at', installerPath);
27
- console.error('Current directory:', __dirname);
28
- process.exit(1);
29
- }
30
-
31
- try {
32
- // Security: Use execFileSync with array args to prevent command injection
33
- // Arguments are passed as array elements, not string interpolation
34
- execFileSync('node', [installerPath, ...arguments_], {
35
- stdio: 'inherit',
36
- cwd: path.dirname(__dirname),
37
- });
38
- } catch (error) {
39
- process.exit(error.status || 1);
40
- }
2
+
3
+ /**
4
+ * AgentVibes - Beautiful ElevenLabs TTS voice commands for Claude Code
5
+ * This file ensures proper execution when run via npx
6
+ */
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+ import path from 'node:path';
10
+ import fs from 'node:fs';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ // Check if we're running in an npx temporary directory
17
+ const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm');
18
+
19
+ // Get CLI arguments
20
+ const arguments_ = process.argv.slice(2);
21
+
22
+ // Route through the TUI console (agentvibes.js) which handles install/config/etc
23
+ const installerPath = path.join(__dirname, 'agentvibes.js');
24
+
25
+ if (!fs.existsSync(installerPath)) {
26
+ console.error('Error: Could not find installer.js at', installerPath);
27
+ console.error('Current directory:', __dirname);
28
+ process.exit(1);
29
+ }
30
+
31
+ try {
32
+ // Security: Use execFileSync with array args to prevent command injection
33
+ // Arguments are passed as array elements, not string interpolation
34
+ execFileSync('node', [installerPath, ...arguments_], {
35
+ stdio: 'inherit',
36
+ cwd: path.dirname(__dirname),
37
+ });
38
+ } catch (error) {
39
+ process.exit(error.status || 1);
40
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "agentvibes",
4
- "version": "5.6.0",
4
+ "version": "5.6.2",
5
5
  "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
6
  "homepage": "https://agentvibes.org",
7
7
  "keywords": [
@@ -1,71 +1,71 @@
1
- import path from 'node:path';
2
- import fs from 'node:fs/promises';
3
- import yaml from 'js-yaml';
4
-
5
- /**
6
- * Detect BMAD installation and version
7
- * @param {string} targetDir - Directory to check
8
- * @returns {Promise<Object>} Detection result with version info
9
- */
10
- export async function detectBMAD(targetDir) {
11
- // Check v6 first (newer version) - support both .bmad and bmad paths
12
- // Try .bmad (standard path with dot prefix) first
13
- let v6Manifest = path.join(targetDir, '.bmad/_cfg/manifest.yaml');
14
- let bmadPath = '.bmad';
15
-
16
- try {
17
- await fs.access(v6Manifest);
18
- } catch {
19
- // Try bmad (alternative path without dot prefix)
20
- v6Manifest = path.join(targetDir, 'bmad/_cfg/manifest.yaml');
21
- bmadPath = 'bmad';
22
- try {
23
- await fs.access(v6Manifest);
24
- } catch {
25
- // Neither path found, continue to v4 check
26
- bmadPath = null;
27
- }
28
- }
29
-
30
- if (bmadPath) {
31
- try {
32
- const manifestContent = await fs.readFile(v6Manifest, 'utf8');
33
- const manifest = yaml.load(manifestContent);
34
-
35
- return {
36
- version: 6,
37
- detailedVersion: manifest.installation?.version || '6.0.0-alpha.x',
38
- manifestPath: v6Manifest,
39
- configPath: path.join(targetDir, bmadPath, 'core/config.yaml'),
40
- bmadPath: path.join(targetDir, bmadPath),
41
- installed: true
42
- };
43
- } catch {}
44
- }
45
-
46
- // Check v4 (legacy)
47
- const v4Manifest = path.join(targetDir, '.bmad-core/install-manifest.yaml');
48
- try {
49
- await fs.access(v4Manifest);
50
- return {
51
- version: 4,
52
- detailedVersion: '4.x',
53
- manifestPath: v4Manifest,
54
- configPath: path.join(targetDir, '.bmad-core/config.yaml'),
55
- bmadPath: path.join(targetDir, '.bmad-core'),
56
- installed: true
57
- };
58
- } catch {}
59
-
60
- // Not installed
61
- return { version: null, installed: false };
62
- }
63
-
64
- /**
65
- * Get BMAD configuration file path for detected version
66
- * @param {Object} detection - Result from detectBMAD()
67
- * @returns {string|null} Path to config.yaml or null
68
- */
69
- export function getBMADConfigPath(detection) {
70
- return detection.installed ? detection.configPath : null;
71
- }
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import yaml from 'js-yaml';
4
+
5
+ /**
6
+ * Detect BMAD installation and version
7
+ * @param {string} targetDir - Directory to check
8
+ * @returns {Promise<Object>} Detection result with version info
9
+ */
10
+ export async function detectBMAD(targetDir) {
11
+ // Check v6 first (newer version) - support both .bmad and bmad paths
12
+ // Try .bmad (standard path with dot prefix) first
13
+ let v6Manifest = path.join(targetDir, '.bmad/_cfg/manifest.yaml');
14
+ let bmadPath = '.bmad';
15
+
16
+ try {
17
+ await fs.access(v6Manifest);
18
+ } catch {
19
+ // Try bmad (alternative path without dot prefix)
20
+ v6Manifest = path.join(targetDir, 'bmad/_cfg/manifest.yaml');
21
+ bmadPath = 'bmad';
22
+ try {
23
+ await fs.access(v6Manifest);
24
+ } catch {
25
+ // Neither path found, continue to v4 check
26
+ bmadPath = null;
27
+ }
28
+ }
29
+
30
+ if (bmadPath) {
31
+ try {
32
+ const manifestContent = await fs.readFile(v6Manifest, 'utf8');
33
+ const manifest = yaml.load(manifestContent);
34
+
35
+ return {
36
+ version: 6,
37
+ detailedVersion: manifest.installation?.version || '6.0.0-alpha.x',
38
+ manifestPath: v6Manifest,
39
+ configPath: path.join(targetDir, bmadPath, 'core/config.yaml'),
40
+ bmadPath: path.join(targetDir, bmadPath),
41
+ installed: true
42
+ };
43
+ } catch {}
44
+ }
45
+
46
+ // Check v4 (legacy)
47
+ const v4Manifest = path.join(targetDir, '.bmad-core/install-manifest.yaml');
48
+ try {
49
+ await fs.access(v4Manifest);
50
+ return {
51
+ version: 4,
52
+ detailedVersion: '4.x',
53
+ manifestPath: v4Manifest,
54
+ configPath: path.join(targetDir, '.bmad-core/config.yaml'),
55
+ bmadPath: path.join(targetDir, '.bmad-core'),
56
+ installed: true
57
+ };
58
+ } catch {}
59
+
60
+ // Not installed
61
+ return { version: null, installed: false };
62
+ }
63
+
64
+ /**
65
+ * Get BMAD configuration file path for detected version
66
+ * @param {Object} detection - Result from detectBMAD()
67
+ * @returns {string|null} Path to config.yaml or null
68
+ */
69
+ export function getBMADConfigPath(detection) {
70
+ return detection.installed ? detection.configPath : null;
71
+ }
@@ -1,110 +1,110 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Personality List Display - Beautiful multi-column personality listing
4
- * Called by personality-manager.sh to display personalities with boxen formatting
5
- */
6
-
7
- import { formatPersonalitiesList } from '../utils/list-formatter.js';
8
- import fs from 'fs';
9
- import path from 'path';
10
- import os from 'os';
11
-
12
- /**
13
- * Get personality description from markdown file
14
- */
15
- function getPersonalityDescription(filePath) {
16
- try {
17
- const content = fs.readFileSync(filePath, 'utf8');
18
-
19
- // Try to extract description from frontmatter or first paragraph
20
- const descMatch = content.match(/description:\s*(.+)/i);
21
- if (descMatch) {
22
- return descMatch[1].trim();
23
- }
24
-
25
- // Try to get first line after frontmatter
26
- const lines = content.split('\n');
27
- let inFrontmatter = false;
28
- let frontmatterCount = 0;
29
-
30
- for (const line of lines) {
31
- if (line.trim() === '---') {
32
- frontmatterCount++;
33
- inFrontmatter = frontmatterCount === 1;
34
- continue;
35
- }
36
-
37
- if (!inFrontmatter && frontmatterCount >= 2 && line.trim()) {
38
- // First non-empty line after frontmatter
39
- return line.trim().replace(/^#+\s*/, '').substring(0, 50);
40
- }
41
- }
42
-
43
- return '';
44
- } catch (error) {
45
- return '';
46
- }
47
- }
48
-
49
- /**
50
- * Get all personalities from directory
51
- */
52
- function getPersonalities(personalitiesDir, currentPersonality) {
53
- const personalities = [];
54
-
55
- if (!fs.existsSync(personalitiesDir)) {
56
- return personalities;
57
- }
58
-
59
- const files = fs.readdirSync(personalitiesDir);
60
- for (const file of files) {
61
- if (file.endsWith('.md')) {
62
- const name = path.basename(file, '.md');
63
- const filePath = path.join(personalitiesDir, file);
64
- const description = getPersonalityDescription(filePath);
65
-
66
- personalities.push({
67
- name,
68
- description,
69
- current: name === currentPersonality
70
- });
71
- }
72
- }
73
-
74
- // Add special 'random' option
75
- personalities.push({
76
- name: 'random',
77
- description: 'Picks randomly each time',
78
- current: currentPersonality === 'random'
79
- });
80
-
81
- return personalities.sort((a, b) => {
82
- // Keep 'random' at the end
83
- if (a.name === 'random') return 1;
84
- if (b.name === 'random') return -1;
85
- return a.name.localeCompare(b.name);
86
- });
87
- }
88
-
89
- /**
90
- * Main function
91
- */
92
- function main() {
93
- const args = process.argv.slice(2);
94
-
95
- // Parse arguments
96
- const personalitiesDir = args[0] || path.join(os.homedir(), '.claude', 'personalities');
97
- const currentPersonality = args[1] || 'normal';
98
-
99
- const personalities = getPersonalities(personalitiesDir, currentPersonality);
100
-
101
- // Display with boxen
102
- const output = formatPersonalitiesList(personalities, {
103
- columns: 2,
104
- showUsage: true
105
- });
106
-
107
- console.log(output);
108
- }
109
-
110
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Personality List Display - Beautiful multi-column personality listing
4
+ * Called by personality-manager.sh to display personalities with boxen formatting
5
+ */
6
+
7
+ import { formatPersonalitiesList } from '../utils/list-formatter.js';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+
12
+ /**
13
+ * Get personality description from markdown file
14
+ */
15
+ function getPersonalityDescription(filePath) {
16
+ try {
17
+ const content = fs.readFileSync(filePath, 'utf8');
18
+
19
+ // Try to extract description from frontmatter or first paragraph
20
+ const descMatch = content.match(/description:\s*(.+)/i);
21
+ if (descMatch) {
22
+ return descMatch[1].trim();
23
+ }
24
+
25
+ // Try to get first line after frontmatter
26
+ const lines = content.split('\n');
27
+ let inFrontmatter = false;
28
+ let frontmatterCount = 0;
29
+
30
+ for (const line of lines) {
31
+ if (line.trim() === '---') {
32
+ frontmatterCount++;
33
+ inFrontmatter = frontmatterCount === 1;
34
+ continue;
35
+ }
36
+
37
+ if (!inFrontmatter && frontmatterCount >= 2 && line.trim()) {
38
+ // First non-empty line after frontmatter
39
+ return line.trim().replace(/^#+\s*/, '').substring(0, 50);
40
+ }
41
+ }
42
+
43
+ return '';
44
+ } catch (error) {
45
+ return '';
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get all personalities from directory
51
+ */
52
+ function getPersonalities(personalitiesDir, currentPersonality) {
53
+ const personalities = [];
54
+
55
+ if (!fs.existsSync(personalitiesDir)) {
56
+ return personalities;
57
+ }
58
+
59
+ const files = fs.readdirSync(personalitiesDir);
60
+ for (const file of files) {
61
+ if (file.endsWith('.md')) {
62
+ const name = path.basename(file, '.md');
63
+ const filePath = path.join(personalitiesDir, file);
64
+ const description = getPersonalityDescription(filePath);
65
+
66
+ personalities.push({
67
+ name,
68
+ description,
69
+ current: name === currentPersonality
70
+ });
71
+ }
72
+ }
73
+
74
+ // Add special 'random' option
75
+ personalities.push({
76
+ name: 'random',
77
+ description: 'Picks randomly each time',
78
+ current: currentPersonality === 'random'
79
+ });
80
+
81
+ return personalities.sort((a, b) => {
82
+ // Keep 'random' at the end
83
+ if (a.name === 'random') return 1;
84
+ if (b.name === 'random') return -1;
85
+ return a.name.localeCompare(b.name);
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Main function
91
+ */
92
+ function main() {
93
+ const args = process.argv.slice(2);
94
+
95
+ // Parse arguments
96
+ const personalitiesDir = args[0] || path.join(os.homedir(), '.claude', 'personalities');
97
+ const currentPersonality = args[1] || 'normal';
98
+
99
+ const personalities = getPersonalities(personalitiesDir, currentPersonality);
100
+
101
+ // Display with boxen
102
+ const output = formatPersonalitiesList(personalities, {
103
+ columns: 2,
104
+ showUsage: true
105
+ });
106
+
107
+ console.log(output);
108
+ }
109
+
110
+ main();
@@ -1,114 +1,114 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Voice List Display - Beautiful multi-column voice listing
4
- * Called by voice-manager.sh to display voices with boxen formatting
5
- */
6
-
7
- import { formatVoicesList } from '../utils/list-formatter.js';
8
- import fs from 'fs';
9
- import path from 'path';
10
- import { execFileSync } from 'child_process';
11
- import os from 'os';
12
-
13
- /**
14
- * Get Piper voices from voice directory
15
- */
16
- function getPiperVoices(voiceDir, currentVoice) {
17
- const voices = [];
18
-
19
- if (!fs.existsSync(voiceDir)) {
20
- return voices;
21
- }
22
-
23
- const files = fs.readdirSync(voiceDir);
24
- for (const file of files) {
25
- if (file.endsWith('.onnx')) {
26
- const voiceName = path.basename(file, '.onnx');
27
- voices.push({
28
- name: voiceName,
29
- lang: extractLanguage(voiceName),
30
- current: voiceName === currentVoice
31
- });
32
- }
33
- }
34
-
35
- return voices.sort((a, b) => a.name.localeCompare(b.name));
36
- }
37
-
38
- /**
39
- * Get macOS voices using say -v ?
40
- */
41
- function getMacOSVoices(currentVoice) {
42
- const voices = [];
43
-
44
- if (os.platform() !== 'darwin') {
45
- return voices;
46
- }
47
-
48
- try {
49
- const output = execFileSync('say', ['-v', '?'], { encoding: 'utf8' }); // NOSONAR - Safe: checking macOS say voices from system PATH
50
- const lines = output.split('\n');
51
-
52
- for (const line of lines) {
53
- if (!line.trim()) continue;
54
-
55
- const parts = line.trim().split(/\s+/);
56
- if (parts.length >= 2) {
57
- const voiceName = parts[0];
58
- const lang = parts[1];
59
-
60
- voices.push({
61
- name: voiceName,
62
- lang,
63
- current: voiceName === currentVoice
64
- });
65
- }
66
- }
67
- } catch (error) {
68
- // say command failed
69
- }
70
-
71
- return voices;
72
- }
73
-
74
- /**
75
- * Extract language code from voice name
76
- */
77
- function extractLanguage(voiceName) {
78
- const match = voiceName.match(/^([a-z]{2}_[A-Z]{2})/);
79
- return match ? match[1] : '';
80
- }
81
-
82
- /**
83
- * Main function
84
- */
85
- function main() {
86
- const args = process.argv.slice(2);
87
-
88
- // Parse arguments
89
- const provider = args[0] || 'piper';
90
- const currentVoice = args[1] || '';
91
- const voiceDir = args[2] || '';
92
-
93
- let voices = [];
94
- let providerName = 'Piper TTS';
95
-
96
- if (provider === 'piper') {
97
- voices = getPiperVoices(voiceDir, currentVoice);
98
- providerName = 'Piper TTS';
99
- } else if (provider === 'macos') {
100
- voices = getMacOSVoices(currentVoice);
101
- providerName = 'macOS TTS';
102
- }
103
-
104
- // Display with boxen
105
- const output = formatVoicesList(voices, {
106
- provider: providerName,
107
- columns: 2,
108
- showUsage: true
109
- });
110
-
111
- console.log(output);
112
- }
113
-
114
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Voice List Display - Beautiful multi-column voice listing
4
+ * Called by voice-manager.sh to display voices with boxen formatting
5
+ */
6
+
7
+ import { formatVoicesList } from '../utils/list-formatter.js';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { execFileSync } from 'child_process';
11
+ import os from 'os';
12
+
13
+ /**
14
+ * Get Piper voices from voice directory
15
+ */
16
+ function getPiperVoices(voiceDir, currentVoice) {
17
+ const voices = [];
18
+
19
+ if (!fs.existsSync(voiceDir)) {
20
+ return voices;
21
+ }
22
+
23
+ const files = fs.readdirSync(voiceDir);
24
+ for (const file of files) {
25
+ if (file.endsWith('.onnx')) {
26
+ const voiceName = path.basename(file, '.onnx');
27
+ voices.push({
28
+ name: voiceName,
29
+ lang: extractLanguage(voiceName),
30
+ current: voiceName === currentVoice
31
+ });
32
+ }
33
+ }
34
+
35
+ return voices.sort((a, b) => a.name.localeCompare(b.name));
36
+ }
37
+
38
+ /**
39
+ * Get macOS voices using say -v ?
40
+ */
41
+ function getMacOSVoices(currentVoice) {
42
+ const voices = [];
43
+
44
+ if (os.platform() !== 'darwin') {
45
+ return voices;
46
+ }
47
+
48
+ try {
49
+ const output = execFileSync('say', ['-v', '?'], { encoding: 'utf8' }); // NOSONAR - Safe: checking macOS say voices from system PATH
50
+ const lines = output.split('\n');
51
+
52
+ for (const line of lines) {
53
+ if (!line.trim()) continue;
54
+
55
+ const parts = line.trim().split(/\s+/);
56
+ if (parts.length >= 2) {
57
+ const voiceName = parts[0];
58
+ const lang = parts[1];
59
+
60
+ voices.push({
61
+ name: voiceName,
62
+ lang,
63
+ current: voiceName === currentVoice
64
+ });
65
+ }
66
+ }
67
+ } catch (error) {
68
+ // say command failed
69
+ }
70
+
71
+ return voices;
72
+ }
73
+
74
+ /**
75
+ * Extract language code from voice name
76
+ */
77
+ function extractLanguage(voiceName) {
78
+ const match = voiceName.match(/^([a-z]{2}_[A-Z]{2})/);
79
+ return match ? match[1] : '';
80
+ }
81
+
82
+ /**
83
+ * Main function
84
+ */
85
+ function main() {
86
+ const args = process.argv.slice(2);
87
+
88
+ // Parse arguments
89
+ const provider = args[0] || 'piper';
90
+ const currentVoice = args[1] || '';
91
+ const voiceDir = args[2] || '';
92
+
93
+ let voices = [];
94
+ let providerName = 'Piper TTS';
95
+
96
+ if (provider === 'piper') {
97
+ voices = getPiperVoices(voiceDir, currentVoice);
98
+ providerName = 'Piper TTS';
99
+ } else if (provider === 'macos') {
100
+ voices = getMacOSVoices(currentVoice);
101
+ providerName = 'macOS TTS';
102
+ }
103
+
104
+ // Display with boxen
105
+ const output = formatVoicesList(voices, {
106
+ provider: providerName,
107
+ columns: 2,
108
+ showUsage: true
109
+ });
110
+
111
+ console.log(output);
112
+ }
113
+
114
+ main();