agentvibes 4.2.0 → 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 +2 -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 -433
  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 -269
  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 -105
  79. package/.claude/hooks/play-tts-macos.sh +368 -368
  80. package/.claude/hooks/play-tts-piper.sh +679 -679
  81. package/.claude/hooks/play-tts-soprano.sh +356 -356
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -301
  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 -81
  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 -84
  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 -145
  99. package/.claude/hooks/tts-queue.sh +165 -165
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -548
  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 -170
  149. package/README.md +2029 -2007
  150. package/RELEASE_NOTES.md +1310 -1203
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1840
  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 -1453
  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 -110
  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 -824
  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 -44
  182. package/src/console/footer-config.js +50 -50
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -62
  185. package/src/console/tabs/agents-tab.js +1684 -1516
  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 -53
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -1212
  192. package/src/console/tabs/settings-tab.js +152 -79
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -25
  195. package/src/console/widgets/format-utils.js +89 -89
  196. package/src/console/widgets/notice.js +55 -55
  197. package/src/console/widgets/personality-picker.js +185 -185
  198. package/src/console/widgets/reverb-picker.js +94 -94
  199. package/src/console/widgets/track-picker.js +285 -285
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5829
  202. package/src/services/agent-voice-store.js +423 -423
  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 -285
  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 -482
  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,136 +1,136 @@
1
- /**
2
- * Custom Inquirer List Prompt with Spacebar Preview
3
- * Uses wrapper approach with readline keypress events
4
- */
5
-
6
- import readline from 'node:readline';
7
- import { execSync } from 'node:child_process';
8
-
9
- /**
10
- * Wrapper for inquirer list prompt that adds spacebar preview
11
- * @param {Object} inquirer - Inquirer instance
12
- * @param {Object} config - Prompt configuration
13
- * @param {Function} config.onPreview - Callback for preview (receives selected value)
14
- * @returns {Promise} Inquirer prompt promise
15
- */
16
- export async function createPreviewListPrompt(inquirer, config) {
17
- const { onPreview, ...promptConfig } = config;
18
-
19
- // Set up keypress listener
20
- let keypressListener = null;
21
-
22
- // Track playing state
23
- let currentlyPlaying = null;
24
- let audioProcess = null;
25
-
26
- // Initialize currentSelection to match the default value
27
- let currentSelection = 0;
28
- if (promptConfig.default) {
29
- const defaultIndex = promptConfig.choices.findIndex(c => c.value === promptConfig.default);
30
- if (defaultIndex !== -1) {
31
- currentSelection = defaultIndex;
32
- }
33
- }
34
-
35
- // Function to stop currently playing audio
36
- const stopAudio = () => {
37
- // Kill the specific process if we have it
38
- // SECURITY: Only kill our own process, never use pkill which affects all users
39
- if (audioProcess) {
40
- try {
41
- audioProcess.kill('SIGKILL');
42
- audioProcess = null;
43
- } catch (e) {
44
- // Process might have already finished or already killed
45
- }
46
- }
47
-
48
- currentlyPlaying = null;
49
- };
50
-
51
- if (onPreview && process.stdin.isTTY) {
52
- readline.emitKeypressEvents(process.stdin);
53
- // SECURITY: Wrap in try-catch to prevent terminal corruption on error
54
- try {
55
- if (process.stdin.setRawMode) {
56
- process.stdin.setRawMode(true);
57
- }
58
- } catch (e) {
59
- // Failed to set raw mode, continue without it
60
- }
61
-
62
- keypressListener = async (str, key) => {
63
- // Track current selection based on arrow keys
64
- if (key && key.name === 'down') {
65
- currentSelection = Math.min(currentSelection + 1, promptConfig.choices.length - 1);
66
- } else if (key && key.name === 'up') {
67
- currentSelection = Math.max(currentSelection - 1, 0);
68
- }
69
-
70
- if (key && key.name === 'space') {
71
- // Get the current item (don't filter - use actual index)
72
- const currentChoice = promptConfig.choices[currentSelection];
73
-
74
- // Only preview if it's a valid choice (not separator, not special item)
75
- if (currentChoice && currentChoice.value && !currentChoice.value.startsWith('__')) {
76
-
77
- // Toggle: if same voice pressed twice, stop it
78
- if (currentlyPlaying === currentChoice.value) {
79
- stopAudio();
80
- return;
81
- }
82
-
83
- // CRITICAL: Stop previous voice BEFORE starting new one
84
- if (currentlyPlaying) {
85
- stopAudio();
86
- // Small delay to ensure kill takes effect
87
- await new Promise(resolve => setTimeout(resolve, 100));
88
- }
89
-
90
- currentlyPlaying = currentChoice.value;
91
-
92
- // Call onPreview and store process handle immediately
93
- try {
94
- const result = await onPreview(currentChoice.value);
95
- // Store the process handle - onPreview should return the spawn() result
96
- audioProcess = result;
97
- } catch (err) {
98
- console.error(`[Preview] Error playing sample:`, err.message);
99
- currentlyPlaying = null;
100
- audioProcess = null;
101
- }
102
- }
103
- }
104
- };
105
-
106
- process.stdin.on('keypress', keypressListener);
107
- }
108
-
109
- try {
110
- // Run the standard list prompt
111
- const result = await inquirer.prompt([{
112
- ...promptConfig,
113
- type: 'list'
114
- }]);
115
-
116
- return result;
117
- } finally {
118
- // Stop any playing audio
119
- stopAudio();
120
-
121
- // Clean up keypress listener
122
- if (keypressListener) {
123
- process.stdin.removeListener('keypress', keypressListener);
124
- // SECURITY: Wrap in try-catch to ensure cleanup always completes
125
- try {
126
- if (process.stdin.setRawMode) {
127
- process.stdin.setRawMode(false);
128
- }
129
- } catch (e) {
130
- // Failed to restore raw mode, but we're exiting anyway
131
- }
132
- }
133
- }
134
- }
135
-
136
- export default createPreviewListPrompt;
1
+ /**
2
+ * Custom Inquirer List Prompt with Spacebar Preview
3
+ * Uses wrapper approach with readline keypress events
4
+ */
5
+
6
+ import readline from 'node:readline';
7
+ import { execSync } from 'node:child_process';
8
+
9
+ /**
10
+ * Wrapper for inquirer list prompt that adds spacebar preview
11
+ * @param {Object} inquirer - Inquirer instance
12
+ * @param {Object} config - Prompt configuration
13
+ * @param {Function} config.onPreview - Callback for preview (receives selected value)
14
+ * @returns {Promise} Inquirer prompt promise
15
+ */
16
+ export async function createPreviewListPrompt(inquirer, config) {
17
+ const { onPreview, ...promptConfig } = config;
18
+
19
+ // Set up keypress listener
20
+ let keypressListener = null;
21
+
22
+ // Track playing state
23
+ let currentlyPlaying = null;
24
+ let audioProcess = null;
25
+
26
+ // Initialize currentSelection to match the default value
27
+ let currentSelection = 0;
28
+ if (promptConfig.default) {
29
+ const defaultIndex = promptConfig.choices.findIndex(c => c.value === promptConfig.default);
30
+ if (defaultIndex !== -1) {
31
+ currentSelection = defaultIndex;
32
+ }
33
+ }
34
+
35
+ // Function to stop currently playing audio
36
+ const stopAudio = () => {
37
+ // Kill the specific process if we have it
38
+ // SECURITY: Only kill our own process, never use pkill which affects all users
39
+ if (audioProcess) {
40
+ try {
41
+ audioProcess.kill('SIGKILL');
42
+ audioProcess = null;
43
+ } catch (e) {
44
+ // Process might have already finished or already killed
45
+ }
46
+ }
47
+
48
+ currentlyPlaying = null;
49
+ };
50
+
51
+ if (onPreview && process.stdin.isTTY) {
52
+ readline.emitKeypressEvents(process.stdin);
53
+ // SECURITY: Wrap in try-catch to prevent terminal corruption on error
54
+ try {
55
+ if (process.stdin.setRawMode) {
56
+ process.stdin.setRawMode(true);
57
+ }
58
+ } catch (e) {
59
+ // Failed to set raw mode, continue without it
60
+ }
61
+
62
+ keypressListener = async (str, key) => {
63
+ // Track current selection based on arrow keys
64
+ if (key && key.name === 'down') {
65
+ currentSelection = Math.min(currentSelection + 1, promptConfig.choices.length - 1);
66
+ } else if (key && key.name === 'up') {
67
+ currentSelection = Math.max(currentSelection - 1, 0);
68
+ }
69
+
70
+ if (key && key.name === 'space') {
71
+ // Get the current item (don't filter - use actual index)
72
+ const currentChoice = promptConfig.choices[currentSelection];
73
+
74
+ // Only preview if it's a valid choice (not separator, not special item)
75
+ if (currentChoice && currentChoice.value && !currentChoice.value.startsWith('__')) {
76
+
77
+ // Toggle: if same voice pressed twice, stop it
78
+ if (currentlyPlaying === currentChoice.value) {
79
+ stopAudio();
80
+ return;
81
+ }
82
+
83
+ // CRITICAL: Stop previous voice BEFORE starting new one
84
+ if (currentlyPlaying) {
85
+ stopAudio();
86
+ // Small delay to ensure kill takes effect
87
+ await new Promise(resolve => setTimeout(resolve, 100));
88
+ }
89
+
90
+ currentlyPlaying = currentChoice.value;
91
+
92
+ // Call onPreview and store process handle immediately
93
+ try {
94
+ const result = await onPreview(currentChoice.value);
95
+ // Store the process handle - onPreview should return the spawn() result
96
+ audioProcess = result;
97
+ } catch (err) {
98
+ console.error(`[Preview] Error playing sample:`, err.message);
99
+ currentlyPlaying = null;
100
+ audioProcess = null;
101
+ }
102
+ }
103
+ }
104
+ };
105
+
106
+ process.stdin.on('keypress', keypressListener);
107
+ }
108
+
109
+ try {
110
+ // Run the standard list prompt
111
+ const result = await inquirer.prompt([{
112
+ ...promptConfig,
113
+ type: 'list'
114
+ }]);
115
+
116
+ return result;
117
+ } finally {
118
+ // Stop any playing audio
119
+ stopAudio();
120
+
121
+ // Clean up keypress listener
122
+ if (keypressListener) {
123
+ process.stdin.removeListener('keypress', keypressListener);
124
+ // SECURITY: Wrap in try-catch to ensure cleanup always completes
125
+ try {
126
+ if (process.stdin.setRawMode) {
127
+ process.stdin.setRawMode(false);
128
+ }
129
+ } catch (e) {
130
+ // Failed to restore raw mode, but we're exiting anyway
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ export default createPreviewListPrompt;
@@ -62,10 +62,11 @@ export async function validateProvider(providerName) {
62
62
  return await validatePiperInstallation();
63
63
  case 'macos':
64
64
  return await validateMacOSProvider();
65
+ case 'sapi':
65
66
  case 'windows-sapi':
66
67
  return await validateWindowsSAPI();
67
68
  case 'windows-piper':
68
- return await validatePiperInstallation();
69
+ return await validateWindowsPiperInstallation();
69
70
  case 'termux-ssh':
70
71
  case 'ssh-remote':
71
72
  case 'ssh-pulseaudio':
@@ -111,7 +112,10 @@ async function validatePipxProvider(providerName, packageName) {
111
112
  checkedLocations.push('pipx venv');
112
113
 
113
114
  // Check Python package installations (comprehensive version detection)
114
- const pythonCommands = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8'];
115
+ // On Windows, 'py' is the standard Python launcher and often the only one in PATH
116
+ const pythonCommands = process.platform === 'win32'
117
+ ? ['py', 'python', 'python3', 'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8']
118
+ : ['python3', 'python', 'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8'];
115
119
 
116
120
  for (const pythonCmd of pythonCommands) {
117
121
  try {
@@ -210,6 +214,43 @@ export async function validateWindowsSAPI() {
210
214
  return { installed: true, message: 'Windows SAPI available' };
211
215
  }
212
216
 
217
+ /**
218
+ * Validate Windows Piper TTS installation
219
+ * Checks for piper.exe at the standard Windows install location
220
+ * @returns {Promise<{installed: boolean, message: string, checkedLocations?: string[], error?: string}>}
221
+ */
222
+ export async function validateWindowsPiperInstallation() {
223
+ if (process.platform !== 'win32') {
224
+ return await validatePiperInstallation(); // Fall back to Unix checks
225
+ }
226
+
227
+ const checkedLocations = [];
228
+
229
+ // Check the standard Windows install path
230
+ const localAppData = process.env.LOCALAPPDATA ||
231
+ (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
232
+ if (localAppData) {
233
+ const piperExe = path.join(localAppData, 'Programs', 'Piper', 'piper.exe');
234
+ checkedLocations.push(piperExe);
235
+ if (fs.existsSync(piperExe)) {
236
+ return { installed: true, message: 'Piper TTS detected (Windows exe)', checkedLocations };
237
+ }
238
+ }
239
+
240
+ // Also check PATH in case user installed it differently
241
+ if (commandExistsInPath('piper')) {
242
+ return { installed: true, message: 'Piper TTS detected (in PATH)', checkedLocations: ['PATH'] };
243
+ }
244
+ checkedLocations.push('PATH');
245
+
246
+ return {
247
+ installed: false,
248
+ message: `Piper TTS is not installed on your system (checked: ${checkedLocations.join(', ')})`,
249
+ error: 'WINDOWS_PIPER_NOT_FOUND',
250
+ checkedLocations
251
+ };
252
+ }
253
+
213
254
  /**
214
255
  * Test if a provider works at runtime (more thorough check)
215
256
  * Attempts to actually use the provider for a brief moment
@@ -328,10 +369,11 @@ export function getProviderInstallCommand(providerName) {
328
369
  * @returns {object} {success: boolean, message: string, command?: string, verified?: boolean}
329
370
  */
330
371
  export async function attemptProviderInstallation(providerName) {
331
- // Whitelist approach - only allow known providers (MEDIUM #1 fix)
372
+ // Whitelist approach - only allow known providers
332
373
  const providers = {
333
374
  soprano: 'soprano-tts',
334
- piper: 'piper-tts'
375
+ piper: 'piper-tts',
376
+ 'windows-piper': 'piper-windows-exe'
335
377
  };
336
378
 
337
379
  const pkgName = providers[providerName];
@@ -339,9 +381,22 @@ export async function attemptProviderInstallation(providerName) {
339
381
  return { success: false, message: `Unknown provider: ${providerName}` };
340
382
  }
341
383
 
384
+ // Windows Piper: download exe instead of pip install
385
+ // The actual download is handled by checkAndInstallPiperWindows() in installer.js.
386
+ // Signal success here so the installer flow continues to that function.
387
+ if (providerName === 'windows-piper') {
388
+ return {
389
+ success: true,
390
+ message: 'Windows Piper will be downloaded during installation',
391
+ command: 'checkAndInstallPiperWindows()',
392
+ verified: false // Verification happens after the actual download
393
+ };
394
+ }
395
+
396
+ const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
397
+
342
398
  // Strategy 1: Try regular pip install (using spawnSync for correct API usage)
343
399
  try {
344
- // Show installation in progress
345
400
  console.log(` Attempting: pip install ${pkgName}...`);
346
401
  const result = spawnSync('pip', ['install', pkgName], {
347
402
  stdio: 'inherit',
@@ -351,7 +406,6 @@ export async function attemptProviderInstallation(providerName) {
351
406
  if (result.error || result.status !== 0) {
352
407
  // Strategy 1 failed - continue to Strategy 2
353
408
  } else {
354
- // Verify installation actually worked (proves it's installed)
355
409
  const validation = await validateProvider(providerName);
356
410
  if (validation.installed) {
357
411
  return {
@@ -377,9 +431,8 @@ export async function attemptProviderInstallation(providerName) {
377
431
  });
378
432
 
379
433
  if (result.error || result.status !== 0) {
380
- // Both strategies failed
434
+ // Strategy 2 failed
381
435
  } else {
382
- // Verify installation actually worked (proves it's installed)
383
436
  const validation = await validateProvider(providerName);
384
437
  if (validation.installed) {
385
438
  return {
@@ -393,11 +446,41 @@ export async function attemptProviderInstallation(providerName) {
393
446
  return { success: true, message: `Successfully installed via pipx`, command: `pipx install ${pkgName}` };
394
447
  }
395
448
  } catch (error) {
396
- // Both strategies failed
449
+ // Strategy 2 failed
450
+ }
451
+
452
+ // Strategy 3 (Windows only): Try 'py -m pip' which is the standard Windows Python launcher
453
+ if (isWindows) {
454
+ try {
455
+ console.log(` Attempting: py -m pip install ${pkgName}...`);
456
+ const result = spawnSync('py', ['-m', 'pip', 'install', pkgName], {
457
+ stdio: 'inherit',
458
+ timeout: 60000
459
+ });
460
+
461
+ if (!result.error && result.status === 0) {
462
+ const validation = await validateProvider(providerName);
463
+ if (validation.installed) {
464
+ return {
465
+ success: true,
466
+ message: `Successfully installed via py -m pip`,
467
+ command: `py -m pip install ${pkgName}`,
468
+ verified: true
469
+ };
470
+ }
471
+
472
+ return { success: true, message: `Successfully installed via py -m pip`, command: `py -m pip install ${pkgName}` };
473
+ }
474
+ } catch (error) {
475
+ // Strategy 3 failed
476
+ }
397
477
  }
398
478
 
399
- // Both strategies failed - consistent error message (MEDIUM #2 fix)
400
- return { success: false, message: `Installation failed. Please install manually:\n pip install ${pkgName}\n or\n pipx install ${pkgName}` };
479
+ // All strategies failed
480
+ const installHint = isWindows
481
+ ? `Installation failed. Please install manually:\n py -m pip install ${pkgName}\n or\n pip install ${pkgName}`
482
+ : `Installation failed. Please install manually:\n pip install ${pkgName}\n or\n pipx install ${pkgName}`;
483
+ return { success: false, message: installHint };
401
484
  }
402
485
 
403
486
  /**
@@ -460,8 +543,9 @@ export function getProviderDisplayName(providerName) {
460
543
  soprano: 'Soprano TTS',
461
544
  piper: 'Piper TTS',
462
545
  macos: 'macOS Say',
546
+ sapi: 'Windows SAPI',
463
547
  'windows-sapi': 'Windows SAPI',
464
- 'windows-piper': 'Windows Piper TTS'
548
+ 'windows-piper': 'Piper TTS'
465
549
  };
466
550
 
467
551
  return names[providerName] || providerName;