agentvibes 4.2.0 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +208 -84
  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 +5895 -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 +143 -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,1453 +1,1465 @@
1
- #!/usr/bin/env python3
2
- """
3
- File: mcp-server/server.py
4
-
5
- AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants!
6
- Website: https://agentvibes.org
7
- Repository: https://github.com/paulpreibisch/AgentVibes
8
-
9
- Co-created by Paul Preibisch with Claude AI
10
- Copyright (c) 2025 Paul Preibisch
11
-
12
- Licensed under the Apache License, Version 2.0 (the "License");
13
- you may not use this file except in compliance with the License.
14
- You may obtain a copy of the License at
15
-
16
- http://www.apache.org/licenses/LICENSE-2.0
17
-
18
- Unless required by applicable law or agreed to in writing, software
19
- distributed under the License is distributed on an "AS IS" BASIS,
20
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
- See the License for the specific language governing permissions and
22
- limitations under the License.
23
-
24
- DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
- express or implied, including but not limited to the warranties of
26
- merchantability, fitness for a particular purpose and noninfringement.
27
- In no event shall the authors or copyright holders be liable for any claim,
28
- damages or other liability, whether in an action of contract, tort or
29
- otherwise, arising from, out of or in connection with the software or the
30
- use or other dealings in the software.
31
-
32
- ---
33
-
34
- @fileoverview MCP Server exposing AgentVibes TTS capabilities via Model Context Protocol
35
- @context Provides natural language control of TTS features for Claude Desktop, Warp, and other MCP clients
36
- @architecture MCP Server implementation wrapping bash scripts, async subprocess execution for non-blocking I/O
37
- @dependencies .claude/hooks/*.sh scripts, MCP SDK, Python asyncio, subprocess
38
- @entrypoints Called by Claude Desktop/Warp via MCP protocol (stdio transport)
39
- @patterns Tool registry pattern, async subprocess wrapping, provider abstraction, state file management
40
- @related GitHub repo, mcp-server/test_server.py, .claude/hooks/play-tts.sh, docs/ai-optimized-documentation-standards.md
41
- """
42
-
43
- import asyncio
44
- import json
45
- import os
46
- import platform
47
- import subprocess
48
- from pathlib import Path
49
- from typing import Optional
50
-
51
- from mcp.server import Server
52
- from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
53
- import mcp.server.stdio
54
-
55
-
56
- class AgentVibesServer:
57
- """MCP Server for AgentVibes TTS functionality"""
58
-
59
- # Script name constants (addresses SonarCloud S1192)
60
- VOICE_MANAGER_SCRIPT = "voice-manager.sh"
61
- PERSONALITY_MANAGER_SCRIPT = "personality-manager.sh"
62
- LANGUAGE_MANAGER_SCRIPT = "language-manager.sh"
63
- BACKGROUND_MUSIC_MANAGER_SCRIPT = "background-music-manager.sh"
64
- EFFECTS_MANAGER_SCRIPT = "effects-manager.sh"
65
-
66
- # Path constants (addresses SonarCloud S1192)
67
- CLAUDE_DIR_NAME = ".claude"
68
- MUTE_FILE_NAME = ".agentvibes-muted"
69
- SEPARATOR = "━" * 39
70
-
71
- def __init__(self):
72
- """Initialize the AgentVibes MCP server"""
73
- # Detect native Windows (not WSL)
74
- self.is_windows = platform.system() == "Windows" and not os.environ.get("WSL_DISTRO_NAME")
75
-
76
- # Script name constants — Windows uses .ps1, Unix uses .sh
77
- if self.is_windows:
78
- self.VOICE_MANAGER_SCRIPT = "voice-manager-windows.ps1"
79
- self.PERSONALITY_MANAGER_SCRIPT = "personality-manager.ps1"
80
- self.LANGUAGE_MANAGER_SCRIPT = "language-manager.ps1"
81
- self.BACKGROUND_MUSIC_MANAGER_SCRIPT = "background-music-manager.ps1"
82
- self.EFFECTS_MANAGER_SCRIPT = "effects-manager.ps1"
83
-
84
- # Find the .claude directory (project-local or global)
85
- self.claude_dir = self._find_claude_dir()
86
- self.hooks_dir = self.claude_dir / ("hooks-windows" if self.is_windows else "hooks")
87
- # Store AgentVibes root directory for environment variable
88
- self.agentvibes_root = self.claude_dir.parent
89
-
90
- def _find_claude_dir(self) -> Path:
91
- """Find the .claude directory relative to this script"""
92
- # Get the AgentVibes root directory (parent of mcp-server)
93
- script_dir = Path(__file__).resolve().parent # mcp-server/
94
- agentvibes_root = script_dir.parent # AgentVibes/
95
- claude_dir = agentvibes_root / self.CLAUDE_DIR_NAME
96
-
97
- # ALWAYS use package .claude for hooks (even in NPX cache)
98
- # The package ALWAYS has .claude/ with all the hooks
99
- if claude_dir.exists() and claude_dir.is_dir():
100
- return claude_dir
101
-
102
- # Fallback to global ~/.claude (should never happen in properly installed package)
103
- return Path.home() / self.CLAUDE_DIR_NAME
104
-
105
- def _resolve_friendly_name(self, voice_name: str) -> str:
106
- """
107
- Resolve friendly name to Piper voice ID using voice-metadata.json.
108
-
109
- Args:
110
- voice_name: Friendly name (e.g., "ryan") or Piper ID
111
-
112
- Returns:
113
- Resolved Piper voice ID, or original voice_name if not found
114
- """
115
- import re
116
-
117
- metadata_path = self.agentvibes_root / ".agentvibes" / "config" / "voice-metadata.json"
118
-
119
- # SECURITY: Verify file exists and is not a symlink
120
- if not metadata_path.exists() or metadata_path.is_symlink():
121
- return voice_name
122
-
123
- # SECURITY: Verify file ownership matches current user (Unix only)
124
- try:
125
- if hasattr(os, 'getuid'):
126
- stat_info = metadata_path.stat()
127
- if stat_info.st_uid != os.getuid():
128
- return voice_name
129
- except (OSError, AttributeError):
130
- pass
131
-
132
- try:
133
- with open(metadata_path, 'r') as f:
134
- metadata = json.load(f)
135
-
136
- voices = metadata.get('voices', {})
137
- voice_lower = voice_name.lower()
138
-
139
- resolved_id = None
140
-
141
- # Check if it's a friendly name key
142
- if voice_lower in voices:
143
- resolved_id = voices[voice_lower].get('id')
144
-
145
- # Check if it matches a displayName
146
- if not resolved_id:
147
- for friendly_name, voice_data in voices.items():
148
- if voice_data.get('displayName', '').lower() == voice_lower:
149
- resolved_id = voice_data.get('id')
150
- break
151
-
152
- # SECURITY: Validate resolved ID matches safe pattern
153
- if resolved_id and re.match(r'^[a-zA-Z0-9_-]+$', resolved_id):
154
- return resolved_id
155
-
156
- except (json.JSONDecodeError, KeyError, IOError, TypeError):
157
- pass
158
-
159
- return voice_name
160
-
161
- async def text_to_speech(
162
- self,
163
- text: str,
164
- voice: Optional[str] = None,
165
- personality: Optional[str] = None,
166
- language: Optional[str] = None,
167
- ) -> str:
168
- """
169
- Convert text to speech using AgentVibes.
170
-
171
- Args:
172
- text: The text to speak
173
- voice: Optional voice name (e.g., "Aria", "Northern Terry")
174
- personality: Optional personality style (e.g., "flirty", "sarcastic")
175
- language: Optional language (e.g., "spanish", "french")
176
-
177
- Returns:
178
- Success message with audio file path
179
- """
180
- # Store original settings to restore later
181
- original_personality = None
182
- original_language = None
183
-
184
- try:
185
- # Temporarily set personality if specified
186
- if personality:
187
- original_personality = await self._get_personality()
188
- await self._run_script(
189
- self.PERSONALITY_MANAGER_SCRIPT, ["set", personality]
190
- )
191
-
192
- # Temporarily set language if specified
193
- if language:
194
- original_language = await self._get_language()
195
- await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
196
-
197
- # Call the TTS script via appropriate shell
198
- tts_script = "play-tts.ps1" if self.is_windows else "play-tts.sh"
199
- play_tts = self.hooks_dir / tts_script
200
- if self.is_windows:
201
- args = ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(play_tts), text]
202
- if voice:
203
- args.extend(["-VoiceOverride", voice])
204
- else:
205
- args = ["bash", str(play_tts), text]
206
- if voice:
207
- args.append(voice)
208
-
209
- env = self._build_script_env()
210
-
211
- result = await asyncio.create_subprocess_exec(
212
- *args,
213
- stdout=asyncio.subprocess.PIPE,
214
- stderr=asyncio.subprocess.PIPE,
215
- env=env,
216
- )
217
- try:
218
- stdout, stderr = await result.communicate()
219
-
220
- if result.returncode == 0:
221
- output = stdout.decode().strip()
222
- # Extract file path from output
223
- for line in output.split("\n"):
224
- if "Saved to:" in line:
225
- file_path = line.split("Saved to:")[1].strip()
226
- truncated = (
227
- f"{text[:50]}..." if len(text) > 50 else text
228
- )
229
- return f"✅ Spoke: {truncated}\n📁 Audio saved: {file_path}"
230
-
231
- return f"✅ Spoke: {text[:50]}..." if len(text) > 50 else f"✅ Spoke: {text}"
232
- else:
233
- error = stderr.decode().strip()
234
- stdout_output = stdout.decode().strip()
235
- full_error = f"{error}\nStdout: {stdout_output}" if stdout_output else error
236
- return f"❌ TTS failed: {full_error}"
237
- finally:
238
- # Ensure process cleanup
239
- if result.returncode is None:
240
- result.kill()
241
- await result.wait()
242
-
243
- finally:
244
- # Restore original settings
245
- if original_personality:
246
- await self._run_script(
247
- self.PERSONALITY_MANAGER_SCRIPT, ["set", original_personality]
248
- )
249
- if original_language:
250
- await self._run_script(
251
- self.LANGUAGE_MANAGER_SCRIPT, ["set", original_language]
252
- )
253
-
254
- async def list_voices(self) -> str:
255
- """
256
- List all available TTS voices for the active provider.
257
-
258
- Returns:
259
- Formatted list of available voices
260
- """
261
- # Get active provider for display purposes
262
- provider = await self._get_provider()
263
- current_voice = await self._get_current_voice()
264
-
265
- # voice-manager.sh list-simple is now provider-aware
266
- result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["list-simple"])
267
- if result:
268
- voices = result.strip().split("\n")
269
- voices = [v for v in voices if v] # Filter empty strings
270
-
271
- if not voices:
272
- return (
273
- f"📦 No voices available\n"
274
- f"{self.SEPARATOR}\n"
275
- f"For Piper: Download voices using /agent-vibes:provider download <voice-name>\n"
276
- f"Example: en_US-lessac-medium, en_GB-alba-medium"
277
- )
278
-
279
- # Determine provider label and alternative provider
280
- if "Piper" in provider:
281
- provider_label = "Piper TTS"
282
- alternative_provider = "macOS"
283
- elif "macOS" in provider:
284
- provider_label = "macOS TTS"
285
- alternative_provider = "Piper"
286
- elif "Termux" in provider or "Android" in provider:
287
- provider_label = "Termux SSH (Android)"
288
- alternative_provider = "Piper"
289
- else:
290
- provider_label = "TTS"
291
- alternative_provider = None
292
-
293
- output = f"🎤 Available {provider_label} Voices:\n"
294
- output += f"{self.SEPARATOR}\n"
295
- for voice in voices:
296
- marker = " ✓ (current)" if voice == current_voice else ""
297
- output += f" • {voice}{marker}\n"
298
- output += f"{self.SEPARATOR}\n"
299
-
300
- # Add provider switch hint
301
- if alternative_provider:
302
- output += f"\n💡 Switch to {alternative_provider}? Use: set_provider(provider=\"{alternative_provider.lower()}\")\n"
303
-
304
- return output
305
- return "❌ Failed to list voices"
306
-
307
- async def set_voice(self, voice_name: str) -> str:
308
- """
309
- Switch to a different voice (supports friendly names like "ryan" or "katherine").
310
-
311
- Args:
312
- voice_name: Friendly name (e.g., "ryan") or Piper voice ID
313
-
314
- Returns:
315
- Success or error message
316
- """
317
- # Resolve friendly name to Piper ID
318
- original_name = voice_name
319
- resolved_name = self._resolve_friendly_name(voice_name)
320
-
321
- result = await self._run_script(
322
- self.VOICE_MANAGER_SCRIPT, ["switch", resolved_name, "--silent"]
323
- )
324
-
325
- if result and "✅" in result:
326
- if original_name.lower() != resolved_name.lower():
327
- return f"✅ Voice switched to: {original_name} ({resolved_name})"
328
- return f"✅ Voice switched to: {voice_name}"
329
- return f"❌ Failed to switch voice: {result}"
330
-
331
- async def list_personalities(self) -> str:
332
- """
333
- List all available personalities.
334
-
335
- Returns:
336
- Formatted list of personalities with descriptions
337
- """
338
- result = await self._run_script(self.PERSONALITY_MANAGER_SCRIPT, ["list"])
339
- return result if result else "❌ Failed to list personalities"
340
-
341
- async def set_personality(self, personality: str) -> str:
342
- """
343
- Set the personality style for TTS messages.
344
-
345
- Args:
346
- personality: Personality name (e.g., "flirty", "sarcastic", "pirate")
347
-
348
- Returns:
349
- Success or error message
350
- """
351
- result = await self._run_script(
352
- self.PERSONALITY_MANAGER_SCRIPT, ["set", personality]
353
- )
354
- if result and "🎭" in result:
355
- return result
356
- return f"❌ Failed to set personality: {result}"
357
-
358
- async def get_config(self) -> str:
359
- """
360
- Get current AgentVibes configuration.
361
-
362
- Returns:
363
- Current voice, personality, language, and provider settings
364
- """
365
- voice = await self._get_current_voice()
366
- personality = await self._get_personality()
367
- language = await self._get_language()
368
- provider = await self._get_provider()
369
-
370
- output = "🎤 Current AgentVibes Configuration\n"
371
- output += f"{self.SEPARATOR}\n"
372
- output += f"Provider: {provider}\n"
373
- output += f"Voice: {voice}\n"
374
- output += f"Personality: {personality}\n"
375
- output += f"Language: {language}\n"
376
- output += f"{self.SEPARATOR}\n"
377
- return output
378
-
379
- async def set_language(self, language: str) -> str:
380
- """
381
- Set the language for TTS speech.
382
-
383
- Args:
384
- language: Language name (e.g., "spanish", "french", "german")
385
-
386
- Returns:
387
- Success or error message
388
- """
389
- result = await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
390
- if result and "✓" in result:
391
- return result
392
- return f"❌ Failed to set language: {result}"
393
-
394
- async def replay_audio(self, n: int = 1) -> str:
395
- """
396
- Replay recently generated TTS audio.
397
-
398
- Args:
399
- n: Which audio to replay (1 = most recent, 2 = second most recent, etc.)
400
-
401
- Returns:
402
- Success or error message
403
- """
404
- result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["replay", str(n)])
405
- if result and "🔊" in result:
406
- return result
407
- return f"❌ Failed to replay audio: {result}"
408
-
409
- async def set_provider(self, provider: str) -> str:
410
- """
411
- Switch TTS provider between Piper, macOS, and Termux SSH.
412
-
413
- Args:
414
- provider: Provider name ("piper", "macos", or "termux-ssh")
415
-
416
- Returns:
417
- Success or error message
418
- """
419
- provider = provider.lower()
420
- if self.is_windows:
421
- valid_providers = ["windows-piper", "windows-sapi", "soprano"]
422
- else:
423
- valid_providers = ["piper", "macos", "termux-ssh", "soprano"]
424
- if provider not in valid_providers:
425
- return f"❌ Invalid provider: {provider}. Choose from: {', '.join(valid_providers)}"
426
-
427
- result = await self._run_script("provider-manager.sh", ["switch", provider])
428
- if result and ("✓" in result or "[OK]" in result):
429
- # Automatically speak confirmation in the new provider's voice
430
- provider_names = {
431
- "macos": "macOS",
432
- "termux-ssh": "Termux SSH",
433
- "piper": "Piper",
434
- "windows-piper": "Windows Piper",
435
- "windows-sapi": "Windows SAPI",
436
- "soprano": "Soprano",
437
- }
438
- provider_name = provider_names.get(provider, provider.title())
439
- confirmation_text = f"Successfully switched to {provider_name} provider"
440
-
441
- try:
442
- # Speak the confirmation with 5 second timeout to prevent hanging
443
- await asyncio.wait_for(
444
- self.text_to_speech(confirmation_text),
445
- timeout=5.0
446
- )
447
- # Return the provider switch result plus TTS confirmation
448
- return f"{result}\n🔊 Spoken confirmation: {confirmation_text}"
449
- except asyncio.TimeoutError:
450
- # Timeout - provider may need setup (e.g., Piper not installed)
451
- return f"{result}\n⚠️ Provider switched (TTS confirmation timed out - provider may need setup)"
452
- except Exception as e:
453
- # If TTS fails, still return success for the provider switch
454
- return f"{result}\n⚠️ Provider switched but TTS confirmation failed: {e}"
455
-
456
- return f"❌ Failed to switch provider: {result}"
457
-
458
- async def set_learn_mode(self, enabled: bool) -> str:
459
- """
460
- Enable or disable language learning mode.
461
-
462
- When enabled, TTS speaks in both your main language and target language.
463
-
464
- Args:
465
- enabled: True to enable, False to disable
466
-
467
- Returns:
468
- Success or error message
469
- """
470
- action = "enable" if enabled else "disable"
471
- result = await self._run_script("learn-manager.sh", [action])
472
- if result and "✓" in result:
473
- return result
474
- return f"❌ Failed to set learn mode: {result}"
475
-
476
- async def set_speed(self, speed: str, target: bool = False) -> str:
477
- """
478
- Set speech speed for main or target voice.
479
-
480
- Works with both Piper and macOS providers.
481
-
482
- Args:
483
- speed: Speed value (e.g., "0.5x", "1x", "2x", "normal", "fast", "slow")
484
- target: If True, sets target language speed; if False, sets main voice speed
485
-
486
- Returns:
487
- Success or error message
488
- """
489
- # Security: Using secrets.choice for cryptographically secure random selection
490
- # Even though this is just for UI variety, we use secrets to satisfy security scanners
491
- import secrets
492
-
493
- args = ["target", speed] if target else [speed]
494
- result = await self._run_script("speed-manager.sh", args)
495
- if result and "✓" in result:
496
- # Simple test messages to demonstrate the new speed
497
- test_messages = [
498
- "Testing speed change",
499
- "Speed test in progress",
500
- "Checking audio speed",
501
- "Speed configuration test",
502
- "Audio speed test",
503
- ]
504
-
505
- # Pick a random test message and speak it
506
- test_message = secrets.choice(test_messages)
507
-
508
- try:
509
- # Speak the test message to demonstrate the new speed
510
- await self.text_to_speech(test_message)
511
- return f"{result}\n🔊 Testing new speed: \"{test_message}\""
512
- except Exception as e:
513
- # If TTS fails, still return success for the speed change
514
- return f"{result}\n⚠️ Speed changed but demo failed: {e}"
515
-
516
- return f"❌ Failed to set speed: {result}"
517
-
518
- async def get_speed(self) -> str:
519
- """
520
- Get current speech speed settings.
521
-
522
- Returns:
523
- Current speed settings for main and target voices
524
- """
525
- result = await self._run_script("speed-manager.sh", ["get"])
526
- return result if result else "❌ Failed to get speed settings"
527
-
528
- async def download_extra_voices(self, auto_yes: bool = False) -> str:
529
- """
530
- Download extra high-quality Piper voices from HuggingFace.
531
-
532
- Downloads custom voices: Kristin, Jenny, and Tracy/16Speakers.
533
-
534
- Args:
535
- auto_yes: If True, skips confirmation prompt and downloads automatically
536
-
537
- Returns:
538
- Success message with download summary
539
- """
540
- args = ["--yes"] if auto_yes else []
541
- result = await self._run_script("download-extra-voices.sh", args)
542
- if result and ("✅" in result or "Successfully downloaded" in result or "already downloaded" in result):
543
- return result
544
- return f"❌ Failed to download extra voices: {result}"
545
-
546
- async def get_verbosity(self) -> str:
547
- """
548
- Get current verbosity level.
549
-
550
- Returns:
551
- Current verbosity level with description
552
- """
553
- result = await self._run_script("verbosity-manager.sh", ["get"])
554
- if result:
555
- level = result.strip()
556
- descriptions = {
557
- "low": "LOW - Acknowledgments + Completions only (minimal)",
558
- "medium": "MEDIUM - + Major decisions and findings (balanced)",
559
- "high": "HIGH - All reasoning (maximum transparency)"
560
- }
561
- desc = descriptions.get(level, level)
562
- return f"🎙️ Current Verbosity: {desc}\n\n💡 Change with: set_verbosity(level=\"low|medium|high\")"
563
- return "❌ Failed to get verbosity level"
564
-
565
- async def set_verbosity(self, level: str) -> str:
566
- """
567
- Set verbosity level to control how much Claude speaks.
568
-
569
- Args:
570
- level: Verbosity level (low, medium, or high)
571
-
572
- Returns:
573
- Success or error message
574
- """
575
- result = await self._run_script("verbosity-manager.sh", ["set", level])
576
- if result and "✅" in result:
577
- return f"{result}\n\n⚠️ Restart Claude Code for changes to take effect"
578
- return f"❌ Failed to set verbosity: {result}"
579
-
580
- def _get_mute_files(self) -> list:
581
- """Get all mute file paths for current platform"""
582
- files = [
583
- Path.home() / self.MUTE_FILE_NAME,
584
- Path.cwd() / self.CLAUDE_DIR_NAME / "agentvibes-muted",
585
- ]
586
- # Windows PowerShell scripts check tts-muted.txt in .claude dir
587
- if self.is_windows:
588
- files.append(Path.home() / self.CLAUDE_DIR_NAME / "tts-muted.txt")
589
- return files
590
-
591
- async def mute(self) -> str:
592
- """
593
- Mute all TTS output. Creates a persistent mute flag.
594
-
595
- Returns:
596
- Success message confirming mute is active
597
- """
598
- try:
599
- mute_file = Path.home() / self.MUTE_FILE_NAME
600
- mute_file.touch()
601
- # On Windows, also write tts-muted.txt for PowerShell script compatibility
602
- if self.is_windows:
603
- win_mute = Path.home() / self.CLAUDE_DIR_NAME / "tts-muted.txt"
604
- win_mute.parent.mkdir(parents=True, exist_ok=True)
605
- win_mute.write_text("true")
606
- return "🔇 AgentVibes TTS muted. All voice output is now silenced.\n\n💡 To unmute, use: unmute()"
607
- except Exception as e:
608
- return f"❌ Failed to mute: {e}"
609
-
610
- async def unmute(self) -> str:
611
- """
612
- Unmute TTS output. Removes the mute flag.
613
-
614
- Returns:
615
- Success message confirming TTS is restored
616
- """
617
- removed = []
618
- try:
619
- for mute_file in self._get_mute_files():
620
- if mute_file.exists():
621
- # tts-muted.txt uses content "true"/"false", others use file existence
622
- if mute_file.name == "tts-muted.txt":
623
- content = mute_file.read_text().strip()
624
- if content == "true":
625
- mute_file.write_text("false")
626
- removed.append(str(mute_file.name))
627
- else:
628
- mute_file.unlink()
629
- removed.append(str(mute_file.name))
630
-
631
- if removed:
632
- return f"🔊 AgentVibes TTS unmuted. Voice output is now restored.\n (Removed: {', '.join(removed)} mute flag)"
633
- else:
634
- return "🔊 AgentVibes TTS was not muted. Voice output is active."
635
- except Exception as e:
636
- return f"❌ Failed to unmute: {e}"
637
-
638
- async def is_muted(self) -> str:
639
- """
640
- Check if TTS is currently muted.
641
-
642
- Returns:
643
- Current mute status
644
- """
645
- for mute_file in self._get_mute_files():
646
- if mute_file.exists():
647
- # tts-muted.txt uses content "true"/"false"
648
- if mute_file.name == "tts-muted.txt":
649
- content = mute_file.read_text().strip()
650
- if content == "true":
651
- return "🔇 TTS is currently MUTED\n\n💡 To unmute, use: unmute()"
652
- else:
653
- return "🔇 TTS is currently MUTED\n\n💡 To unmute, use: unmute()"
654
- return "🔊 TTS is currently ACTIVE\n\n💡 To mute, use: mute()"
655
-
656
- async def list_background_music(self) -> str:
657
- """
658
- List all available background music tracks.
659
-
660
- Returns:
661
- Formatted list of all pre-packaged background music files
662
- """
663
- result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["list"])
664
- return result if result else "❌ Failed to list background music"
665
-
666
- async def set_background_music(self, track_name: str, agent_name: Optional[str] = None) -> str:
667
- """
668
- Set background music track for a specific agent, all agents, or as default.
669
-
670
- Args:
671
- track_name: Track filename or partial name for fuzzy matching
672
- agent_name: Agent name ('all' for all agents, None for default)
673
-
674
- Returns:
675
- Success or error message
676
- """
677
- import re
678
-
679
- # Get list of available tracks for fuzzy matching
680
- list_result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["list"])
681
- if not list_result or "❌" in list_result:
682
- return "❌ Failed to list background music tracks"
683
-
684
- # Parse track names
685
- tracks = []
686
- for line in list_result.split("\n"):
687
- match = re.match(r'\s*\d+\.\s+(.+)', line)
688
- if match:
689
- tracks.append(match.group(1))
690
-
691
- # Try to find a matching track (case-insensitive partial match)
692
- track_lower = track_name.lower()
693
- matched_track = None
694
-
695
- # First try exact match
696
- for track in tracks:
697
- if track.lower() == track_lower:
698
- matched_track = track
699
- break
700
-
701
- # If no exact match, try partial match
702
- if not matched_track:
703
- for track in tracks:
704
- if track_lower in track.lower():
705
- matched_track = track
706
- break
707
-
708
- if not matched_track:
709
- # Show available tracks to help user
710
- available = "\n".join([f" {t}" for t in tracks])
711
- return f"❌ No track matching '{track_name}' found.\n\nAvailable tracks:\n{available}\n\n💡 Try a partial match like 'celtic' or 'chillwave'"
712
-
713
- # Determine which command to use based on agent_name
714
- if agent_name and agent_name.lower() == "all":
715
- # Set for all agents
716
- result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["set-all", matched_track])
717
- elif agent_name:
718
- # Set for specific agent
719
- result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["set-agent", agent_name, matched_track])
720
- else:
721
- # Set as default
722
- result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["set-default", matched_track])
723
-
724
- if result and "✅" in result:
725
- if matched_track.lower() != track_name.lower():
726
- return f"{result}\n\n🔍 Matched '{track_name}' to '{matched_track}'"
727
- return result
728
- return f"❌ Failed to set background music: {result}"
729
-
730
- async def enable_background_music(self, enabled: bool) -> str:
731
- """
732
- Enable or disable background music globally.
733
-
734
- Args:
735
- enabled: True to enable, False to disable
736
-
737
- Returns:
738
- Success or error message
739
- """
740
- command = "on" if enabled else "off"
741
- result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, [command])
742
- return result if result else f"❌ Failed to {'enable' if enabled else 'disable'} background music"
743
-
744
- async def set_background_music_volume(self, volume: float) -> str:
745
- """
746
- Set background music volume.
747
-
748
- Args:
749
- volume: Volume level (0.0-1.0)
750
-
751
- Returns:
752
- Success or error message
753
- """
754
- result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["volume", str(volume)])
755
- return result if result else "❌ Failed to set background music volume"
756
-
757
- async def get_background_music_status(self) -> str:
758
- """
759
- Get current background music configuration.
760
-
761
- Returns:
762
- Status information
763
- """
764
- result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["status"])
765
- return result if result else "❌ Failed to get background music status"
766
-
767
- async def set_reverb(self, level: str, agent: str = "default", apply_all: bool = False) -> str:
768
- """
769
- Set reverb level for an agent or globally.
770
-
771
- Args:
772
- level: Reverb level (off, light, medium, heavy, cathedral)
773
- agent: Agent name (default: "default")
774
- apply_all: Apply to all agents (default: False)
775
-
776
- Returns:
777
- Success message
778
- """
779
- args = ["set-reverb", level, agent]
780
- if apply_all:
781
- args.append("--all")
782
- result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, args)
783
- return result if result else f"✅ Set reverb to {level}"
784
-
785
- async def get_reverb(self, agent: str = "default") -> str:
786
- """
787
- Get current reverb level for an agent.
788
-
789
- Args:
790
- agent: Agent name (default: "default")
791
-
792
- Returns:
793
- Current reverb level
794
- """
795
- result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, ["get-reverb", agent])
796
- if result:
797
- return f"Current reverb level for {agent}: {result.strip()}"
798
- return f"❌ Failed to get reverb for {agent}"
799
-
800
- async def list_audio_effects(self) -> str:
801
- """
802
- List all audio effects for all agents.
803
-
804
- Returns:
805
- Effects configuration
806
- """
807
- result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, ["list"])
808
- return result if result else " Failed to list audio effects"
809
-
810
- async def clean_audio_cache(self) -> str:
811
- """
812
- Clean all TTS audio cache files and report space freed.
813
-
814
- Non-interactive cleanup suitable for MCP tool usage. Deletes all
815
- TTS-generated audio files (wav, mp3, aiff) while preserving
816
- background music tracks.
817
-
818
- Returns:
819
- Cleanup results with file count and space freed
820
- """
821
- result = await self._run_script("clean-audio-cache.sh", [])
822
- return result if result else "❌ Failed to clean audio cache"
823
-
824
- async def set_banner(self, enabled: bool) -> str:
825
- """
826
- Enable or disable the TTS output banner (voice info, file path, cache size).
827
-
828
- Args:
829
- enabled: True to show banner, False to hide it
830
-
831
- Returns:
832
- Confirmation message
833
- """
834
- banner_file = Path.home() / ".agentvibes" / "banner-disabled"
835
- if enabled:
836
- # Remove the disable flag
837
- try:
838
- banner_file.unlink(missing_ok=True)
839
- except Exception:
840
- pass
841
- return "✅ TTS banner enabled — voice info will show after each speech"
842
- else:
843
- # Create the disable flag
844
- banner_file.parent.mkdir(parents=True, exist_ok=True)
845
- banner_file.touch()
846
- return "🔇 TTS banner disabled — speech will play without output info"
847
-
848
- async def get_banner(self) -> str:
849
- """
850
- Check if the TTS output banner is enabled or disabled.
851
-
852
- Returns:
853
- Current banner status
854
- """
855
- banner_file = Path.home() / ".agentvibes" / "banner-disabled"
856
- if banner_file.exists():
857
- return "🔇 TTS banner: **disabled**\n\nSay: \"Turn on banner\" to re-enable"
858
- return "✅ TTS banner: **enabled**\n\nSay: \"Turn off banner\" to disable"
859
-
860
- # Helper methods
861
- def _build_script_env(self) -> dict:
862
- """Build environment dict for script execution (shared by all script runners)"""
863
- env = os.environ.copy()
864
-
865
- # Determine where to save settings based on context:
866
- # 1. If cwd has .claude/ → Use cwd (real Claude Code project)
867
- # 2. Otherwise → Use global ~/.claude/ (Claude Desktop, Warp, etc.)
868
- # Note: Hooks are ALWAYS from package .claude/ (self.claude_dir)
869
- cwd = Path.cwd()
870
- if (cwd / ".claude").is_dir() and cwd != self.agentvibes_root:
871
- env["CLAUDE_PROJECT_DIR"] = str(cwd)
872
-
873
- # Add common locations for piper to PATH (Unix only)
874
- if not self.is_windows:
875
- home_dir = Path.home()
876
- local_bin = str(home_dir / ".local" / "bin")
877
- if "PATH" in env:
878
- if local_bin not in env["PATH"]:
879
- env["PATH"] = f"{local_bin}:{env['PATH']}"
880
- else:
881
- env["PATH"] = local_bin
882
-
883
- return env
884
-
885
- async def _run_script(self, script_name: str, args: list[str]) -> str:
886
- """Run a script and return output (bash on Unix, PowerShell on Windows)"""
887
- # Auto-resolve .sh → .ps1 on Windows (class constants handle special cases)
888
- if self.is_windows and script_name.endswith('.sh'):
889
- script_name = script_name[:-3] + '.ps1'
890
- script_path = self.hooks_dir / script_name
891
- if not script_path.exists():
892
- return f"Script not found: {script_path}"
893
-
894
- # Build command — PowerShell on Windows, bash on Unix
895
- if self.is_windows:
896
- cmd = [
897
- "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
898
- "-File", str(script_path)
899
- ] + args
900
- else:
901
- cmd = ["bash", str(script_path)] + args
902
-
903
- env = self._build_script_env()
904
-
905
- try:
906
- result = await asyncio.create_subprocess_exec(
907
- *cmd,
908
- stdout=asyncio.subprocess.PIPE,
909
- stderr=asyncio.subprocess.PIPE,
910
- env=env,
911
- )
912
- try:
913
- stdout, stderr = await result.communicate()
914
- if result.returncode == 0:
915
- return stdout.decode().strip()
916
- else:
917
- error_msg = stderr.decode().strip()
918
- if not error_msg: # If stderr is empty, include stdout for debugging
919
- error_msg = f"Return code {result.returncode}. Stdout: {stdout.decode().strip()}"
920
- return error_msg
921
- finally:
922
- # Ensure process cleanup
923
- if result.returncode is None:
924
- result.kill()
925
- await result.wait()
926
- except Exception as e:
927
- return f"Error running script: {e}"
928
-
929
- async def _get_current_voice(self) -> str:
930
- """Get the currently active voice"""
931
- result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["get"])
932
- return result.strip() if result else "Unknown"
933
-
934
- async def _get_personality(self) -> str:
935
- """Get the current personality setting"""
936
- personality_file = self.claude_dir / "tts-personality.txt"
937
- if not personality_file.exists():
938
- # Try global
939
- personality_file = Path.home() / self.CLAUDE_DIR_NAME / "tts-personality.txt"
940
-
941
- try:
942
- if personality_file.exists():
943
- return personality_file.read_text().strip()
944
- except (PermissionError, UnicodeDecodeError, OSError) as e:
945
- # Log error but don't crash - return default
946
- import sys
947
- print(f"Warning: Could not read personality file: {e}", file=sys.stderr)
948
- return "normal"
949
-
950
- async def _get_language(self) -> str:
951
- """Get the current language setting"""
952
- result = await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["code"])
953
- return result.strip() if result else "english"
954
-
955
- async def _get_provider(self) -> str:
956
- """Get the active TTS provider"""
957
- provider_file = self.claude_dir / "tts-provider.txt"
958
- if not provider_file.exists():
959
- provider_file = Path.home() / self.CLAUDE_DIR_NAME / "tts-provider.txt"
960
-
961
- provider_labels = {
962
- "macos": "macOS TTS",
963
- "piper": "Piper TTS (Free, Offline)",
964
- "termux-ssh": "Termux SSH (Android)",
965
- "windows-piper": "Windows Piper TTS (Free, Offline)",
966
- "windows-sapi": "Windows SAPI (Built-in)",
967
- "soprano": "Soprano TTS (Ultra-fast Neural)",
968
- }
969
- try:
970
- if provider_file.exists():
971
- provider = provider_file.read_text().strip()
972
- # Strip BOM from PowerShell-written files
973
- provider = provider.lstrip('\ufeff')
974
- return provider_labels.get(provider, provider)
975
- except (PermissionError, UnicodeDecodeError, OSError) as e:
976
- # Log error but don't crash - return default
977
- import sys
978
- print(f"Warning: Could not read provider file: {e}", file=sys.stderr)
979
- # Default based on platform
980
- if self.is_windows:
981
- return "Windows SAPI (Built-in)"
982
- return "Piper TTS (Free, Offline)"
983
-
984
-
985
- # Create the MCP server
986
- app = Server("agentvibes")
987
- agent_vibes = AgentVibesServer()
988
-
989
-
990
- @app.list_tools()
991
- async def list_tools() -> list[Tool]:
992
- """List all available AgentVibes tools"""
993
- return [
994
- Tool(
995
- name="text_to_speech",
996
- description="""Convert text to speech using AgentVibes TTS.
997
-
998
- Supports both macOS TTS and Piper (free, offline) providers.
999
- Can use different voices, personalities, and languages.
1000
-
1001
- Perfect for:
1002
- - Speaking acknowledgments and confirmations
1003
- - Adding voice to Claude responses
1004
- - Multi-language communication
1005
- - Personality-driven interactions
1006
-
1007
- Examples:
1008
- - text_to_speech(text="Hello, I'm ready to help!")
1009
- - text_to_speech(text="Task completed!", personality="flirty")
1010
- - text_to_speech(text="Hola, ¿cómo estás?", language="spanish")
1011
- """,
1012
- inputSchema={
1013
- "type": "object",
1014
- "properties": {
1015
- "text": {
1016
- "type": "string",
1017
- "description": "Text to convert to speech (max 500 characters)",
1018
- },
1019
- "voice": {
1020
- "type": "string",
1021
- "description": "Voice name (optional). Use list_voices to see options.",
1022
- },
1023
- "personality": {
1024
- "type": "string",
1025
- "description": "Personality style (optional). Examples: flirty, sarcastic, pirate, robot, zen",
1026
- },
1027
- "language": {
1028
- "type": "string",
1029
- "description": "Language to speak in (optional). Examples: spanish, french, german, italian",
1030
- },
1031
- },
1032
- "required": ["text"],
1033
- },
1034
- ),
1035
- Tool(
1036
- name="list_voices",
1037
- description="List all available TTS voices with current selection",
1038
- inputSchema={"type": "object", "properties": {}},
1039
- ),
1040
- Tool(
1041
- name="set_voice",
1042
- description="Switch to a different TTS voice",
1043
- inputSchema={
1044
- "type": "object",
1045
- "properties": {
1046
- "voice_name": {
1047
- "type": "string",
1048
- "description": "Name of the voice to switch to",
1049
- }
1050
- },
1051
- "required": ["voice_name"],
1052
- },
1053
- ),
1054
- Tool(
1055
- name="list_personalities",
1056
- description="List all available personality styles with descriptions",
1057
- inputSchema={"type": "object", "properties": {}},
1058
- ),
1059
- Tool(
1060
- name="set_personality",
1061
- description="Set the personality style for TTS messages",
1062
- inputSchema={
1063
- "type": "object",
1064
- "properties": {
1065
- "personality": {
1066
- "type": "string",
1067
- "description": "Personality name (e.g., flirty, sarcastic, pirate)",
1068
- }
1069
- },
1070
- "required": ["personality"],
1071
- },
1072
- ),
1073
- Tool(
1074
- name="set_language",
1075
- description="Set the language for TTS speech (supports 25+ languages)",
1076
- inputSchema={
1077
- "type": "object",
1078
- "properties": {
1079
- "language": {
1080
- "type": "string",
1081
- "description": "Language name (e.g., spanish, french, german)",
1082
- }
1083
- },
1084
- "required": ["language"],
1085
- },
1086
- ),
1087
- Tool(
1088
- name="get_config",
1089
- description="Get current voice, personality, language, and provider configuration",
1090
- inputSchema={"type": "object", "properties": {}},
1091
- ),
1092
- Tool(
1093
- name="replay_audio",
1094
- description="Replay recently generated TTS audio",
1095
- inputSchema={
1096
- "type": "object",
1097
- "properties": {
1098
- "n": {
1099
- "type": "integer",
1100
- "description": "Which audio to replay (1 = most recent, default: 1)",
1101
- "minimum": 1,
1102
- "maximum": 10,
1103
- }
1104
- },
1105
- },
1106
- ),
1107
- Tool(
1108
- name="set_provider",
1109
- description="Switch between TTS providers" + (
1110
- ": Windows Piper, Windows SAPI, or Soprano" if agent_vibes.is_windows
1111
- else ": macOS TTS, Piper (free, offline), Soprano, or Termux SSH (Android)"
1112
- ),
1113
- inputSchema={
1114
- "type": "object",
1115
- "properties": {
1116
- "provider": {
1117
- "type": "string",
1118
- "description": (
1119
- "Provider name: 'windows-piper', 'windows-sapi', or 'soprano'"
1120
- if agent_vibes.is_windows
1121
- else "Provider name: 'piper', 'macos', 'soprano', or 'termux-ssh'"
1122
- ),
1123
- "enum": (
1124
- ["windows-piper", "windows-sapi", "soprano"]
1125
- if agent_vibes.is_windows
1126
- else ["piper", "macos", "soprano", "termux-ssh"]
1127
- ),
1128
- }
1129
- },
1130
- "required": ["provider"],
1131
- },
1132
- ),
1133
- Tool(
1134
- name="set_learn_mode",
1135
- description="Enable or disable language learning mode. When ON, TTS speaks in both your main language and target language for bilingual learning.",
1136
- inputSchema={
1137
- "type": "object",
1138
- "properties": {
1139
- "enabled": {
1140
- "type": "boolean",
1141
- "description": "True to enable learning mode, False to disable"
1142
- }
1143
- },
1144
- "required": ["enabled"],
1145
- },
1146
- ),
1147
- Tool(
1148
- name="set_speed",
1149
- description="Set speech speed for main or target voice. Works with both Piper and macOS providers. Use this to make voices faster or slower.",
1150
- inputSchema={
1151
- "type": "object",
1152
- "properties": {
1153
- "speed": {
1154
- "type": "string",
1155
- "description": "Speed value: '0.5x' or 'slow/slower' (half speed, slower), '1x' or 'normal' (normal speed), '2x' or 'fast' (double speed, faster), '3x' or 'faster' (triple speed, very fast)"
1156
- },
1157
- "target": {
1158
- "type": "boolean",
1159
- "description": "If true, sets target language speed (for learning mode); if false or omitted, sets main voice speed",
1160
- "default": False
1161
- }
1162
- },
1163
- "required": ["speed"],
1164
- },
1165
- ),
1166
- Tool(
1167
- name="get_speed",
1168
- description="Get current speech speed settings for main and target voices",
1169
- inputSchema={"type": "object", "properties": {}},
1170
- ),
1171
- Tool(
1172
- name="download_extra_voices",
1173
- description="Download extra high-quality custom Piper voices from HuggingFace. Includes: Kristin (US female), Jenny (UK female with Irish accent), and Tracy/16Speakers (multi-speaker). Perfect for adding variety to your TTS voices.",
1174
- inputSchema={
1175
- "type": "object",
1176
- "properties": {
1177
- "auto_yes": {
1178
- "type": "boolean",
1179
- "description": "Skip confirmation prompt and download automatically (default: False)",
1180
- "default": False
1181
- }
1182
- },
1183
- },
1184
- ),
1185
- Tool(
1186
- name="get_verbosity",
1187
- description="Get current AgentVibes verbosity level (low/medium/high). Verbosity controls how much Claude speaks while working - from minimal (acknowledgments only) to maximum transparency (all reasoning spoken).",
1188
- inputSchema={"type": "object", "properties": {}},
1189
- ),
1190
- Tool(
1191
- name="set_verbosity",
1192
- description="""Set AgentVibes verbosity level to control how much Claude speaks while working.
1193
-
1194
- Verbosity Levels:
1195
- - LOW: Only acknowledgments (start) and completions (end). Minimal interruption.
1196
- - MEDIUM: + Major decisions and key findings. Balanced transparency.
1197
- - HIGH: All reasoning, decisions, and findings. Maximum transparency.
1198
-
1199
- Perfect for:
1200
- - LOW: Quiet work sessions, minimal distraction
1201
- - MEDIUM: Understanding major decisions without full narration
1202
- - HIGH: Full transparency, learning mode, debugging complex tasks
1203
-
1204
- Note: Changes take effect on next Claude Code session restart.""",
1205
- inputSchema={
1206
- "type": "object",
1207
- "properties": {
1208
- "level": {
1209
- "type": "string",
1210
- "description": "Verbosity level to set",
1211
- "enum": ["low", "medium", "high"]
1212
- }
1213
- },
1214
- "required": ["level"],
1215
- },
1216
- ),
1217
- Tool(
1218
- name="mute",
1219
- description="Mute all AgentVibes TTS output. Creates a persistent mute flag that silences all voice output until unmuted. Persists across sessions.",
1220
- inputSchema={"type": "object", "properties": {}},
1221
- ),
1222
- Tool(
1223
- name="unmute",
1224
- description="Unmute AgentVibes TTS output. Removes the mute flag and restores voice output.",
1225
- inputSchema={"type": "object", "properties": {}},
1226
- ),
1227
- Tool(
1228
- name="is_muted",
1229
- description="Check if TTS is currently muted.",
1230
- inputSchema={"type": "object", "properties": {}},
1231
- ),
1232
- Tool(
1233
- name="list_background_music",
1234
- description="List all available pre-packaged background music tracks. Shows all audio files that can be used as background music for TTS.",
1235
- inputSchema={"type": "object", "properties": {}},
1236
- ),
1237
- Tool(
1238
- name="set_background_music",
1239
- description="""Set background music track for a specific agent, all agents, or as default. Supports smart fuzzy matching.
1240
-
1241
- Perfect for:
1242
- - "change background music to flamenco" - Sets for all agents
1243
- - "set John's background music to celtic harp" - Agent-specific
1244
- - "use chillwave as default background" - Default for new agents
1245
-
1246
- Fuzzy matching examples:
1247
- - "flamenco" matches "agentvibes_soft_flamenco_loop.mp3"
1248
- - "celtic" matches "agent_vibes_celtic_harp_v1_loop.mp3"
1249
- - "bossa" matches "agent_vibes_bossa_nova_v2_loop.mp3"
1250
- """,
1251
- inputSchema={
1252
- "type": "object",
1253
- "properties": {
1254
- "track_name": {
1255
- "type": "string",
1256
- "description": "Track filename or partial name for fuzzy matching (e.g., 'celtic', 'flamenco', 'bossa nova')",
1257
- },
1258
- "agent_name": {
1259
- "type": "string",
1260
- "description": "Agent name to configure (optional). Use 'all' for all agents, omit for default",
1261
- },
1262
- },
1263
- "required": ["track_name"],
1264
- },
1265
- ),
1266
- Tool(
1267
- name="enable_background_music",
1268
- description="Enable or disable background music globally. When enabled, TTS audio will be mixed with background music at configured volume (default 30%).",
1269
- inputSchema={
1270
- "type": "object",
1271
- "properties": {
1272
- "enabled": {
1273
- "type": "boolean",
1274
- "description": "True to enable background music, False to disable",
1275
- }
1276
- },
1277
- "required": ["enabled"],
1278
- },
1279
- ),
1280
- Tool(
1281
- name="set_background_music_volume",
1282
- description="Set the volume level for background music (0.0-1.0). Recommended: 0.20-0.40 for subtle background ambiance.",
1283
- inputSchema={
1284
- "type": "object",
1285
- "properties": {
1286
- "volume": {
1287
- "type": "number",
1288
- "description": "Volume level (0.0 = silent, 0.30 = default, 1.0 = full volume)",
1289
- "minimum": 0.0,
1290
- "maximum": 1.0,
1291
- }
1292
- },
1293
- "required": ["volume"],
1294
- },
1295
- ),
1296
- Tool(
1297
- name="get_background_music_status",
1298
- description="Get current background music configuration including enabled status, volume, default track, and number of available tracks.",
1299
- inputSchema={"type": "object", "properties": {}},
1300
- ),
1301
- Tool(
1302
- name="set_reverb",
1303
- description="""Set reverb level for TTS audio. Can apply globally (default agent), to a specific agent, or to all agents.
1304
-
1305
- Reverb adds room/space ambiance to the voice, making it sound like it's in a small room, conference room, or large hall.
1306
-
1307
- Examples:
1308
- - set_reverb(level="medium") - Set reverb for default agent
1309
- - set_reverb(level="cathedral", agent="Winston") - Set cathedral reverb for Winston
1310
- - set_reverb(level="light", apply_all=True) - Set light reverb for all agents
1311
- - set_reverb(level="off") - Turn off reverb for default agent
1312
- """,
1313
- inputSchema={
1314
- "type": "object",
1315
- "properties": {
1316
- "level": {
1317
- "type": "string",
1318
- "description": "Reverb level",
1319
- "enum": ["off", "light", "medium", "heavy", "cathedral"]
1320
- },
1321
- "agent": {
1322
- "type": "string",
1323
- "description": "Agent name (optional, defaults to 'default'). Examples: Winston, John, Mary, Amelia",
1324
- },
1325
- "apply_all": {
1326
- "type": "boolean",
1327
- "description": "Apply to all agents (optional, default: false)",
1328
- }
1329
- },
1330
- "required": ["level"],
1331
- },
1332
- ),
1333
- Tool(
1334
- name="get_reverb",
1335
- description="Get current reverb level for a specific agent or default",
1336
- inputSchema={
1337
- "type": "object",
1338
- "properties": {
1339
- "agent": {
1340
- "type": "string",
1341
- "description": "Agent name (optional, defaults to 'default')",
1342
- }
1343
- },
1344
- },
1345
- ),
1346
- Tool(
1347
- name="list_audio_effects",
1348
- description="List current audio effects configuration for all agents, including reverb levels and other effects",
1349
- inputSchema={"type": "object", "properties": {}},
1350
- ),
1351
- Tool(
1352
- name="clean_audio_cache",
1353
- description="Clean all TTS audio cache files and report space freed. Non-interactive cleanup that removes all wav/mp3/aiff files while preserving background music tracks.",
1354
- inputSchema={"type": "object", "properties": {}},
1355
- ),
1356
- ]
1357
-
1358
-
1359
- @app.call_tool()
1360
- async def call_tool(name: str, arguments: dict) -> list[TextContent]:
1361
- """Handle tool calls"""
1362
- try:
1363
- if name == "text_to_speech":
1364
- result = await agent_vibes.text_to_speech(
1365
- text=arguments["text"],
1366
- voice=arguments.get("voice"),
1367
- personality=arguments.get("personality"),
1368
- language=arguments.get("language"),
1369
- )
1370
- elif name == "list_voices":
1371
- result = await agent_vibes.list_voices()
1372
- elif name == "set_voice":
1373
- result = await agent_vibes.set_voice(arguments["voice_name"])
1374
- elif name == "list_personalities":
1375
- result = await agent_vibes.list_personalities()
1376
- elif name == "set_personality":
1377
- result = await agent_vibes.set_personality(arguments["personality"])
1378
- elif name == "set_language":
1379
- result = await agent_vibes.set_language(arguments["language"])
1380
- elif name == "get_config":
1381
- result = await agent_vibes.get_config()
1382
- elif name == "replay_audio":
1383
- n = arguments.get("n", 1)
1384
- result = await agent_vibes.replay_audio(n)
1385
- elif name == "set_provider":
1386
- result = await agent_vibes.set_provider(arguments["provider"])
1387
- elif name == "set_learn_mode":
1388
- result = await agent_vibes.set_learn_mode(arguments["enabled"])
1389
- elif name == "set_speed":
1390
- target = arguments.get("target", False)
1391
- result = await agent_vibes.set_speed(arguments["speed"], target)
1392
- elif name == "get_speed":
1393
- result = await agent_vibes.get_speed()
1394
- elif name == "download_extra_voices":
1395
- auto_yes = arguments.get("auto_yes", False)
1396
- result = await agent_vibes.download_extra_voices(auto_yes)
1397
- elif name == "get_verbosity":
1398
- result = await agent_vibes.get_verbosity()
1399
- elif name == "set_verbosity":
1400
- result = await agent_vibes.set_verbosity(arguments["level"])
1401
- elif name == "mute":
1402
- result = await agent_vibes.mute()
1403
- elif name == "unmute":
1404
- result = await agent_vibes.unmute()
1405
- elif name == "is_muted":
1406
- result = await agent_vibes.is_muted()
1407
- elif name == "list_background_music":
1408
- result = await agent_vibes.list_background_music()
1409
- elif name == "set_background_music":
1410
- track_name = arguments.get("track_name")
1411
- agent_name = arguments.get("agent_name")
1412
- result = await agent_vibes.set_background_music(track_name, agent_name)
1413
- elif name == "enable_background_music":
1414
- enabled = arguments.get("enabled")
1415
- result = await agent_vibes.enable_background_music(enabled)
1416
- elif name == "set_background_music_volume":
1417
- volume = arguments.get("volume")
1418
- result = await agent_vibes.set_background_music_volume(volume)
1419
- elif name == "get_background_music_status":
1420
- result = await agent_vibes.get_background_music_status()
1421
- elif name == "set_reverb":
1422
- level = arguments["level"]
1423
- agent = arguments.get("agent", "default")
1424
- apply_all = arguments.get("apply_all", False)
1425
- result = await agent_vibes.set_reverb(level, agent, apply_all)
1426
- elif name == "get_reverb":
1427
- agent = arguments.get("agent", "default")
1428
- result = await agent_vibes.get_reverb(agent)
1429
- elif name == "list_audio_effects":
1430
- result = await agent_vibes.list_audio_effects()
1431
- elif name == "clean_audio_cache":
1432
- result = await agent_vibes.clean_audio_cache()
1433
- else:
1434
- result = f"Unknown tool: {name}"
1435
-
1436
- return [TextContent(type="text", text=result)]
1437
-
1438
- except Exception as e:
1439
- return [TextContent(type="text", text=f"Error: {str(e)}")]
1440
-
1441
-
1442
- async def main():
1443
- """Run the MCP server"""
1444
- async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
1445
- await app.run(
1446
- read_stream,
1447
- write_stream,
1448
- app.create_initialization_options(),
1449
- )
1450
-
1451
-
1452
- if __name__ == "__main__":
1453
- asyncio.run(main())
1
+ #!/usr/bin/env python3
2
+ """
3
+ File: mcp-server/server.py
4
+
5
+ AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants!
6
+ Website: https://agentvibes.org
7
+ Repository: https://github.com/paulpreibisch/AgentVibes
8
+
9
+ Co-created by Paul Preibisch with Claude AI
10
+ Copyright (c) 2025 Paul Preibisch
11
+
12
+ Licensed under the Apache License, Version 2.0 (the "License");
13
+ you may not use this file except in compliance with the License.
14
+ You may obtain a copy of the License at
15
+
16
+ http://www.apache.org/licenses/LICENSE-2.0
17
+
18
+ Unless required by applicable law or agreed to in writing, software
19
+ distributed under the License is distributed on an "AS IS" BASIS,
20
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ See the License for the specific language governing permissions and
22
+ limitations under the License.
23
+
24
+ DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
+ express or implied, including but not limited to the warranties of
26
+ merchantability, fitness for a particular purpose and noninfringement.
27
+ In no event shall the authors or copyright holders be liable for any claim,
28
+ damages or other liability, whether in an action of contract, tort or
29
+ otherwise, arising from, out of or in connection with the software or the
30
+ use or other dealings in the software.
31
+
32
+ ---
33
+
34
+ @fileoverview MCP Server exposing AgentVibes TTS capabilities via Model Context Protocol
35
+ @context Provides natural language control of TTS features for Claude Desktop, Warp, and other MCP clients
36
+ @architecture MCP Server implementation wrapping bash scripts, async subprocess execution for non-blocking I/O
37
+ @dependencies .claude/hooks/*.sh scripts, MCP SDK, Python asyncio, subprocess
38
+ @entrypoints Called by Claude Desktop/Warp via MCP protocol (stdio transport)
39
+ @patterns Tool registry pattern, async subprocess wrapping, provider abstraction, state file management
40
+ @related GitHub repo, mcp-server/test_server.py, .claude/hooks/play-tts.sh, docs/ai-optimized-documentation-standards.md
41
+ """
42
+
43
+ import asyncio
44
+ import json
45
+ import os
46
+ import platform
47
+ import subprocess
48
+ from pathlib import Path
49
+ from typing import Optional
50
+
51
+ from mcp.server import Server
52
+ from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
53
+ import mcp.server.stdio
54
+
55
+
56
+ class AgentVibesServer:
57
+ """MCP Server for AgentVibes TTS functionality"""
58
+
59
+ # Script name constants (addresses SonarCloud S1192)
60
+ VOICE_MANAGER_SCRIPT = "voice-manager.sh"
61
+ PERSONALITY_MANAGER_SCRIPT = "personality-manager.sh"
62
+ LANGUAGE_MANAGER_SCRIPT = "language-manager.sh"
63
+ BACKGROUND_MUSIC_MANAGER_SCRIPT = "background-music-manager.sh"
64
+ EFFECTS_MANAGER_SCRIPT = "effects-manager.sh"
65
+
66
+ # Path constants (addresses SonarCloud S1192)
67
+ CLAUDE_DIR_NAME = ".claude"
68
+ MUTE_FILE_NAME = ".agentvibes-muted"
69
+ SEPARATOR = "━" * 39
70
+
71
+ def __init__(self):
72
+ """Initialize the AgentVibes MCP server"""
73
+ # Detect native Windows (not WSL)
74
+ self.is_windows = platform.system() == "Windows" and not os.environ.get("WSL_DISTRO_NAME")
75
+
76
+ # Script name constants — Windows uses .ps1, Unix uses .sh
77
+ if self.is_windows:
78
+ self.VOICE_MANAGER_SCRIPT = "voice-manager-windows.ps1"
79
+ self.PERSONALITY_MANAGER_SCRIPT = "personality-manager.ps1"
80
+ self.LANGUAGE_MANAGER_SCRIPT = "language-manager.ps1"
81
+ self.BACKGROUND_MUSIC_MANAGER_SCRIPT = "background-music-manager.ps1"
82
+ self.EFFECTS_MANAGER_SCRIPT = "effects-manager.ps1"
83
+
84
+ # Find the .claude directory (project-local or global)
85
+ self.claude_dir = self._find_claude_dir()
86
+ self.hooks_dir = self.claude_dir / ("hooks-windows" if self.is_windows else "hooks")
87
+ # Store AgentVibes root directory for environment variable
88
+ self.agentvibes_root = self.claude_dir.parent
89
+
90
+ def _find_claude_dir(self) -> Path:
91
+ """Find the .claude directory relative to this script"""
92
+ # Get the AgentVibes root directory (parent of mcp-server)
93
+ script_dir = Path(__file__).resolve().parent # mcp-server/
94
+ agentvibes_root = script_dir.parent # AgentVibes/
95
+ claude_dir = agentvibes_root / self.CLAUDE_DIR_NAME
96
+
97
+ # ALWAYS use package .claude for hooks (even in NPX cache)
98
+ # The package ALWAYS has .claude/ with all the hooks
99
+ if claude_dir.exists() and claude_dir.is_dir():
100
+ return claude_dir
101
+
102
+ # Fallback to global ~/.claude (should never happen in properly installed package)
103
+ return Path.home() / self.CLAUDE_DIR_NAME
104
+
105
+ def _resolve_friendly_name(self, voice_name: str) -> str:
106
+ """
107
+ Resolve friendly name to Piper voice ID using voice-metadata.json.
108
+
109
+ Args:
110
+ voice_name: Friendly name (e.g., "ryan") or Piper ID
111
+
112
+ Returns:
113
+ Resolved Piper voice ID, or original voice_name if not found
114
+ """
115
+ import re
116
+
117
+ metadata_path = self.agentvibes_root / ".agentvibes" / "config" / "voice-metadata.json"
118
+
119
+ # SECURITY: Verify file exists and is not a symlink
120
+ if not metadata_path.exists() or metadata_path.is_symlink():
121
+ return voice_name
122
+
123
+ # SECURITY: Verify file ownership matches current user (Unix only)
124
+ try:
125
+ if hasattr(os, 'getuid'):
126
+ stat_info = metadata_path.stat()
127
+ if stat_info.st_uid != os.getuid():
128
+ return voice_name
129
+ except (OSError, AttributeError):
130
+ pass
131
+
132
+ try:
133
+ with open(metadata_path, 'r') as f:
134
+ metadata = json.load(f)
135
+
136
+ voices = metadata.get('voices', {})
137
+ voice_lower = voice_name.lower()
138
+
139
+ resolved_id = None
140
+
141
+ # Check if it's a friendly name key
142
+ if voice_lower in voices:
143
+ resolved_id = voices[voice_lower].get('id')
144
+
145
+ # Check if it matches a displayName
146
+ if not resolved_id:
147
+ for friendly_name, voice_data in voices.items():
148
+ if voice_data.get('displayName', '').lower() == voice_lower:
149
+ resolved_id = voice_data.get('id')
150
+ break
151
+
152
+ # SECURITY: Validate resolved ID matches safe pattern
153
+ if resolved_id and re.match(r'^[a-zA-Z0-9_-]+$', resolved_id):
154
+ return resolved_id
155
+
156
+ except (json.JSONDecodeError, KeyError, IOError, TypeError):
157
+ pass
158
+
159
+ return voice_name
160
+
161
+ async def text_to_speech(
162
+ self,
163
+ text: str,
164
+ voice: Optional[str] = None,
165
+ personality: Optional[str] = None,
166
+ language: Optional[str] = None,
167
+ ) -> str:
168
+ """
169
+ Convert text to speech using AgentVibes.
170
+
171
+ Args:
172
+ text: The text to speak
173
+ voice: Optional voice name (e.g., "Aria", "Northern Terry")
174
+ personality: Optional personality style (e.g., "flirty", "sarcastic")
175
+ language: Optional language (e.g., "spanish", "french")
176
+
177
+ Returns:
178
+ Success message with audio file path
179
+ """
180
+ # Store original settings to restore later
181
+ original_personality = None
182
+ original_language = None
183
+
184
+ try:
185
+ # Temporarily set personality if specified
186
+ if personality:
187
+ original_personality = await self._get_personality()
188
+ await self._run_script(
189
+ self.PERSONALITY_MANAGER_SCRIPT, ["set", personality]
190
+ )
191
+
192
+ # Temporarily set language if specified
193
+ if language:
194
+ original_language = await self._get_language()
195
+ await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
196
+
197
+ # Call the TTS script via appropriate shell
198
+ tts_script = "play-tts.ps1" if self.is_windows else "play-tts.sh"
199
+ play_tts = self.hooks_dir / tts_script
200
+ if self.is_windows:
201
+ args = ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(play_tts), text]
202
+ if voice:
203
+ args.extend(["-VoiceOverride", voice])
204
+ else:
205
+ args = ["bash", str(play_tts), text]
206
+ if voice:
207
+ args.append(voice)
208
+
209
+ env = self._build_script_env()
210
+
211
+ result = await asyncio.create_subprocess_exec(
212
+ *args,
213
+ stdout=asyncio.subprocess.PIPE,
214
+ stderr=asyncio.subprocess.PIPE,
215
+ env=env,
216
+ )
217
+ try:
218
+ stdout, stderr = await result.communicate()
219
+
220
+ if result.returncode == 0:
221
+ output = stdout.decode().strip()
222
+ # Extract file path from output
223
+ for line in output.split("\n"):
224
+ if "Saved to:" in line:
225
+ file_path = line.split("Saved to:")[1].strip()
226
+ truncated = (
227
+ f"{text[:50]}..." if len(text) > 50 else text
228
+ )
229
+ return f"✅ Spoke: {truncated}\n📁 Audio saved: {file_path}"
230
+
231
+ return f"✅ Spoke: {text[:50]}..." if len(text) > 50 else f"✅ Spoke: {text}"
232
+ else:
233
+ error = stderr.decode().strip()
234
+ stdout_output = stdout.decode().strip()
235
+ full_error = f"{error}\nStdout: {stdout_output}" if stdout_output else error
236
+ return f"❌ TTS failed: {full_error}"
237
+ finally:
238
+ # Ensure process cleanup
239
+ if result.returncode is None:
240
+ result.kill()
241
+ await result.wait()
242
+
243
+ finally:
244
+ # Restore original settings
245
+ if original_personality:
246
+ await self._run_script(
247
+ self.PERSONALITY_MANAGER_SCRIPT, ["set", original_personality]
248
+ )
249
+ if original_language:
250
+ await self._run_script(
251
+ self.LANGUAGE_MANAGER_SCRIPT, ["set", original_language]
252
+ )
253
+
254
+ async def list_voices(self) -> str:
255
+ """
256
+ List all available TTS voices for the active provider.
257
+
258
+ Returns:
259
+ Formatted list of available voices
260
+ """
261
+ # Get active provider for display purposes
262
+ provider = await self._get_provider()
263
+ current_voice = await self._get_current_voice()
264
+
265
+ # voice-manager.sh list-simple is now provider-aware
266
+ result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["list-simple"])
267
+ if result:
268
+ voices = result.strip().split("\n")
269
+ voices = [v for v in voices if v] # Filter empty strings
270
+
271
+ if not voices:
272
+ return (
273
+ f"📦 No voices available\n"
274
+ f"{self.SEPARATOR}\n"
275
+ f"For Piper: Download voices using /agent-vibes:provider download <voice-name>\n"
276
+ f"Example: en_US-lessac-medium, en_GB-alba-medium"
277
+ )
278
+
279
+ # Determine provider label and alternative provider
280
+ if "Piper" in provider:
281
+ provider_label = "Piper TTS"
282
+ alternative_provider = "macOS"
283
+ elif "macOS" in provider:
284
+ provider_label = "macOS TTS"
285
+ alternative_provider = "Piper"
286
+ elif "Termux" in provider or "Android" in provider:
287
+ provider_label = "Termux SSH (Android)"
288
+ alternative_provider = "Piper"
289
+ else:
290
+ provider_label = "TTS"
291
+ alternative_provider = None
292
+
293
+ output = f"🎤 Available {provider_label} Voices:\n"
294
+ output += f"{self.SEPARATOR}\n"
295
+ for voice in voices:
296
+ marker = " ✓ (current)" if voice == current_voice else ""
297
+ output += f" • {voice}{marker}\n"
298
+ output += f"{self.SEPARATOR}\n"
299
+
300
+ # Add provider switch hint
301
+ if alternative_provider:
302
+ output += f"\n💡 Switch to {alternative_provider}? Use: set_provider(provider=\"{alternative_provider.lower()}\")\n"
303
+
304
+ return output
305
+ return "❌ Failed to list voices"
306
+
307
+ async def set_voice(self, voice_name: str) -> str:
308
+ """
309
+ Switch to a different voice (supports friendly names like "ryan" or "katherine").
310
+
311
+ Args:
312
+ voice_name: Friendly name (e.g., "ryan") or Piper voice ID
313
+
314
+ Returns:
315
+ Success or error message
316
+ """
317
+ # Resolve friendly name to Piper ID
318
+ original_name = voice_name
319
+ resolved_name = self._resolve_friendly_name(voice_name)
320
+
321
+ result = await self._run_script(
322
+ self.VOICE_MANAGER_SCRIPT, ["switch", resolved_name, "--silent"]
323
+ )
324
+
325
+ if result and "✅" in result:
326
+ if original_name.lower() != resolved_name.lower():
327
+ return f"✅ Voice switched to: {original_name} ({resolved_name})"
328
+ return f"✅ Voice switched to: {voice_name}"
329
+ return f"❌ Failed to switch voice: {result}"
330
+
331
+ async def list_personalities(self) -> str:
332
+ """
333
+ List all available personalities.
334
+
335
+ Returns:
336
+ Formatted list of personalities with descriptions
337
+ """
338
+ result = await self._run_script(self.PERSONALITY_MANAGER_SCRIPT, ["list"])
339
+ return result if result else "❌ Failed to list personalities"
340
+
341
+ async def set_personality(self, personality: str) -> str:
342
+ """
343
+ Set the personality style for TTS messages.
344
+
345
+ Args:
346
+ personality: Personality name (e.g., "flirty", "sarcastic", "pirate")
347
+
348
+ Returns:
349
+ Success or error message
350
+ """
351
+ result = await self._run_script(
352
+ self.PERSONALITY_MANAGER_SCRIPT, ["set", personality]
353
+ )
354
+ if result and "🎭" in result:
355
+ return result
356
+ return f"❌ Failed to set personality: {result}"
357
+
358
+ async def get_config(self) -> str:
359
+ """
360
+ Get current AgentVibes configuration.
361
+
362
+ Returns:
363
+ Current voice, personality, language, and provider settings
364
+ """
365
+ voice = await self._get_current_voice()
366
+ personality = await self._get_personality()
367
+ language = await self._get_language()
368
+ provider = await self._get_provider()
369
+
370
+ output = "🎤 Current AgentVibes Configuration\n"
371
+ output += f"{self.SEPARATOR}\n"
372
+ output += f"Provider: {provider}\n"
373
+ output += f"Voice: {voice}\n"
374
+ output += f"Personality: {personality}\n"
375
+ output += f"Language: {language}\n"
376
+ output += f"{self.SEPARATOR}\n"
377
+ return output
378
+
379
+ async def set_language(self, language: str) -> str:
380
+ """
381
+ Set the language for TTS speech.
382
+
383
+ Args:
384
+ language: Language name (e.g., "spanish", "french", "german")
385
+
386
+ Returns:
387
+ Success or error message
388
+ """
389
+ result = await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
390
+ if result and "✓" in result:
391
+ return result
392
+ return f"❌ Failed to set language: {result}"
393
+
394
+ async def replay_audio(self, n: int = 1) -> str:
395
+ """
396
+ Replay recently generated TTS audio.
397
+
398
+ Args:
399
+ n: Which audio to replay (1 = most recent, 2 = second most recent, etc.)
400
+
401
+ Returns:
402
+ Success or error message
403
+ """
404
+ result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["replay", str(n)])
405
+ if result and "🔊" in result:
406
+ return result
407
+ return f"❌ Failed to replay audio: {result}"
408
+
409
+ async def set_provider(self, provider: str) -> str:
410
+ """
411
+ Switch TTS provider between Piper, macOS, and Termux SSH.
412
+
413
+ Args:
414
+ provider: Provider name ("piper", "macos", or "termux-ssh")
415
+
416
+ Returns:
417
+ Success or error message
418
+ """
419
+ provider = provider.lower()
420
+ if self.is_windows:
421
+ valid_providers = ["piper", "sapi", "soprano"]
422
+ else:
423
+ valid_providers = ["piper", "macos", "termux-ssh", "soprano"]
424
+ if provider not in valid_providers:
425
+ return f"❌ Invalid provider: {provider}. Choose from: {', '.join(valid_providers)}"
426
+
427
+ result = await self._run_script("provider-manager.sh", ["switch", provider])
428
+ if result and ("✓" in result or "[OK]" in result):
429
+ # Automatically speak confirmation in the new provider's voice
430
+ provider_names = {
431
+ "macos": "macOS",
432
+ "termux-ssh": "Termux SSH",
433
+ "piper": "Piper",
434
+ "sapi": "Windows SAPI",
435
+ "soprano": "Soprano",
436
+ }
437
+ provider_name = provider_names.get(provider, provider.title())
438
+ confirmation_text = f"Successfully switched to {provider_name} provider"
439
+
440
+ try:
441
+ # Speak the confirmation with 5 second timeout to prevent hanging
442
+ await asyncio.wait_for(
443
+ self.text_to_speech(confirmation_text),
444
+ timeout=5.0
445
+ )
446
+ # Return the provider switch result plus TTS confirmation
447
+ return f"{result}\n🔊 Spoken confirmation: {confirmation_text}"
448
+ except asyncio.TimeoutError:
449
+ # Timeout - provider may need setup (e.g., Piper not installed)
450
+ return f"{result}\n⚠️ Provider switched (TTS confirmation timed out - provider may need setup)"
451
+ except Exception as e:
452
+ # If TTS fails, still return success for the provider switch
453
+ return f"{result}\n⚠️ Provider switched but TTS confirmation failed: {e}"
454
+
455
+ return f"❌ Failed to switch provider: {result}"
456
+
457
+ async def set_learn_mode(self, enabled: bool) -> str:
458
+ """
459
+ Enable or disable language learning mode.
460
+
461
+ When enabled, TTS speaks in both your main language and target language.
462
+
463
+ Args:
464
+ enabled: True to enable, False to disable
465
+
466
+ Returns:
467
+ Success or error message
468
+ """
469
+ action = "enable" if enabled else "disable"
470
+ result = await self._run_script("learn-manager.sh", [action])
471
+ if result and "" in result:
472
+ return result
473
+ return f"❌ Failed to set learn mode: {result}"
474
+
475
+ async def set_speed(self, speed: str, target: bool = False) -> str:
476
+ """
477
+ Set speech speed for main or target voice.
478
+
479
+ Works with both Piper and macOS providers.
480
+
481
+ Args:
482
+ speed: Speed value (e.g., "0.5x", "1x", "2x", "normal", "fast", "slow")
483
+ target: If True, sets target language speed; if False, sets main voice speed
484
+
485
+ Returns:
486
+ Success or error message
487
+ """
488
+ # Security: Using secrets.choice for cryptographically secure random selection
489
+ # Even though this is just for UI variety, we use secrets to satisfy security scanners
490
+ import secrets
491
+
492
+ args = ["target", speed] if target else [speed]
493
+ result = await self._run_script("speed-manager.sh", args)
494
+ if result and "" in result:
495
+ # Simple test messages to demonstrate the new speed
496
+ test_messages = [
497
+ "Testing speed change",
498
+ "Speed test in progress",
499
+ "Checking audio speed",
500
+ "Speed configuration test",
501
+ "Audio speed test",
502
+ ]
503
+
504
+ # Pick a random test message and speak it
505
+ test_message = secrets.choice(test_messages)
506
+
507
+ try:
508
+ # Speak the test message to demonstrate the new speed
509
+ await self.text_to_speech(test_message)
510
+ return f"{result}\n🔊 Testing new speed: \"{test_message}\""
511
+ except Exception as e:
512
+ # If TTS fails, still return success for the speed change
513
+ return f"{result}\n⚠️ Speed changed but demo failed: {e}"
514
+
515
+ return f"❌ Failed to set speed: {result}"
516
+
517
+ async def get_speed(self) -> str:
518
+ """
519
+ Get current speech speed settings.
520
+
521
+ Returns:
522
+ Current speed settings for main and target voices
523
+ """
524
+ result = await self._run_script("speed-manager.sh", ["get"])
525
+ return result if result else "❌ Failed to get speed settings"
526
+
527
+ async def download_extra_voices(self, auto_yes: bool = False) -> str:
528
+ """
529
+ Download extra high-quality Piper voices from HuggingFace.
530
+
531
+ Downloads custom voices: Kristin, Jenny, and Tracy/16Speakers.
532
+
533
+ Args:
534
+ auto_yes: If True, skips confirmation prompt and downloads automatically
535
+
536
+ Returns:
537
+ Success message with download summary
538
+ """
539
+ args = ["--yes"] if auto_yes else []
540
+ result = await self._run_script("download-extra-voices.sh", args)
541
+ if result and ("" in result or "Successfully downloaded" in result or "already downloaded" in result):
542
+ return result
543
+ return f"❌ Failed to download extra voices: {result}"
544
+
545
+ async def get_verbosity(self) -> str:
546
+ """
547
+ Get current verbosity level.
548
+
549
+ Returns:
550
+ Current verbosity level with description
551
+ """
552
+ result = await self._run_script("verbosity-manager.sh", ["get"])
553
+ if result:
554
+ level = result.strip()
555
+ descriptions = {
556
+ "low": "LOW - Acknowledgments + Completions only (minimal)",
557
+ "medium": "MEDIUM - + Major decisions and findings (balanced)",
558
+ "high": "HIGH - All reasoning (maximum transparency)"
559
+ }
560
+ desc = descriptions.get(level, level)
561
+ return f"🎙️ Current Verbosity: {desc}\n\n💡 Change with: set_verbosity(level=\"low|medium|high\")"
562
+ return " Failed to get verbosity level"
563
+
564
+ async def set_verbosity(self, level: str) -> str:
565
+ """
566
+ Set verbosity level to control how much Claude speaks.
567
+
568
+ Args:
569
+ level: Verbosity level (low, medium, or high)
570
+
571
+ Returns:
572
+ Success or error message
573
+ """
574
+ result = await self._run_script("verbosity-manager.sh", ["set", level])
575
+ if result and "" in result:
576
+ return f"{result}\n\n⚠️ Restart Claude Code for changes to take effect"
577
+ return f" Failed to set verbosity: {result}"
578
+
579
+ def _get_mute_files(self) -> list:
580
+ """Get all mute file paths for current platform"""
581
+ files = [
582
+ Path.home() / self.MUTE_FILE_NAME,
583
+ Path.cwd() / self.CLAUDE_DIR_NAME / "agentvibes-muted",
584
+ ]
585
+ # Windows PowerShell scripts check tts-muted.txt in .claude dir
586
+ if self.is_windows:
587
+ files.append(Path.home() / self.CLAUDE_DIR_NAME / "tts-muted.txt")
588
+ return files
589
+
590
+ async def mute(self) -> str:
591
+ """
592
+ Mute all TTS output. Creates a persistent mute flag.
593
+
594
+ Returns:
595
+ Success message confirming mute is active
596
+ """
597
+ try:
598
+ mute_file = Path.home() / self.MUTE_FILE_NAME
599
+ mute_file.touch()
600
+ # On Windows, also write tts-muted.txt for PowerShell script compatibility
601
+ if self.is_windows:
602
+ win_mute = Path.home() / self.CLAUDE_DIR_NAME / "tts-muted.txt"
603
+ win_mute.parent.mkdir(parents=True, exist_ok=True)
604
+ win_mute.write_text("true")
605
+ return "🔇 AgentVibes TTS muted. All voice output is now silenced.\n\n💡 To unmute, use: unmute()"
606
+ except Exception as e:
607
+ return f"❌ Failed to mute: {e}"
608
+
609
+ async def unmute(self) -> str:
610
+ """
611
+ Unmute TTS output. Removes the mute flag.
612
+
613
+ Returns:
614
+ Success message confirming TTS is restored
615
+ """
616
+ removed = []
617
+ try:
618
+ for mute_file in self._get_mute_files():
619
+ if mute_file.exists():
620
+ # tts-muted.txt uses content "true"/"false", others use file existence
621
+ if mute_file.name == "tts-muted.txt":
622
+ content = mute_file.read_text().strip()
623
+ if content == "true":
624
+ mute_file.write_text("false")
625
+ removed.append(str(mute_file.name))
626
+ else:
627
+ mute_file.unlink()
628
+ removed.append(str(mute_file.name))
629
+
630
+ if removed:
631
+ return f"🔊 AgentVibes TTS unmuted. Voice output is now restored.\n (Removed: {', '.join(removed)} mute flag)"
632
+ else:
633
+ return "🔊 AgentVibes TTS was not muted. Voice output is active."
634
+ except Exception as e:
635
+ return f"❌ Failed to unmute: {e}"
636
+
637
+ async def is_muted(self) -> str:
638
+ """
639
+ Check if TTS is currently muted.
640
+
641
+ Returns:
642
+ Current mute status
643
+ """
644
+ for mute_file in self._get_mute_files():
645
+ if mute_file.exists():
646
+ # tts-muted.txt uses content "true"/"false"
647
+ if mute_file.name == "tts-muted.txt":
648
+ content = mute_file.read_text().strip()
649
+ if content == "true":
650
+ return "🔇 TTS is currently MUTED\n\n💡 To unmute, use: unmute()"
651
+ else:
652
+ return "🔇 TTS is currently MUTED\n\n💡 To unmute, use: unmute()"
653
+ return "🔊 TTS is currently ACTIVE\n\n💡 To mute, use: mute()"
654
+
655
+ async def list_background_music(self) -> str:
656
+ """
657
+ List all available background music tracks.
658
+
659
+ Returns:
660
+ Formatted list of all pre-packaged background music files
661
+ """
662
+ result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["list"])
663
+ return result if result else "❌ Failed to list background music"
664
+
665
+ async def set_background_music(self, track_name: str, agent_name: Optional[str] = None) -> str:
666
+ """
667
+ Set background music track for a specific agent, all agents, or as default.
668
+
669
+ Args:
670
+ track_name: Track filename or partial name for fuzzy matching
671
+ agent_name: Agent name ('all' for all agents, None for default)
672
+
673
+ Returns:
674
+ Success or error message
675
+ """
676
+ import re
677
+
678
+ # Get list of available tracks for fuzzy matching
679
+ list_result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["list"])
680
+ if not list_result or "" in list_result:
681
+ return "❌ Failed to list background music tracks"
682
+
683
+ # Parse track names
684
+ tracks = []
685
+ for line in list_result.split("\n"):
686
+ match = re.match(r'\s*\d+\.\s+(.+)', line.strip())
687
+ if match:
688
+ tracks.append(match.group(1).strip())
689
+
690
+ # Try to find a matching track (case-insensitive partial match)
691
+ track_lower = track_name.lower()
692
+ matched_track = None
693
+
694
+ # First try exact match
695
+ for track in tracks:
696
+ if track.lower() == track_lower:
697
+ matched_track = track
698
+ break
699
+
700
+ # If no exact match, try partial match
701
+ if not matched_track:
702
+ for track in tracks:
703
+ if track_lower in track.lower():
704
+ matched_track = track
705
+ break
706
+
707
+ if not matched_track:
708
+ # Show available tracks to help user
709
+ available = "\n".join([f" • {t}" for t in tracks])
710
+ return f" No track matching '{track_name}' found.\n\nAvailable tracks:\n{available}\n\n💡 Try a partial match like 'celtic' or 'chillwave'"
711
+
712
+ # Determine which command to use based on agent_name
713
+ if agent_name and agent_name.lower() == "all":
714
+ # Set for all agents
715
+ result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["set-all", matched_track])
716
+ elif agent_name:
717
+ # Set for specific agent
718
+ result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["set-agent", agent_name, matched_track])
719
+ else:
720
+ # Set as default
721
+ result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["set-default", matched_track])
722
+
723
+ if result and ("✅" in result or "[OK]" in result):
724
+ if matched_track.lower() != track_name.lower():
725
+ return f"{result}\n\n🔍 Matched '{track_name}' to '{matched_track}'"
726
+ return result
727
+ return f"❌ Failed to set background music: {result}"
728
+
729
+ async def enable_background_music(self, enabled: bool) -> str:
730
+ """
731
+ Enable or disable background music globally.
732
+
733
+ Args:
734
+ enabled: True to enable, False to disable
735
+
736
+ Returns:
737
+ Success or error message
738
+ """
739
+ command = "on" if enabled else "off"
740
+ result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, [command])
741
+ # Sync to .agentvibes/config.json (TUI source of truth)
742
+ try:
743
+ import json
744
+ cfg_path = self.agentvibes_root / ".agentvibes" / "config.json"
745
+ cfg = {}
746
+ if cfg_path.exists():
747
+ cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
748
+ if "backgroundMusic" not in cfg:
749
+ cfg["backgroundMusic"] = {}
750
+ cfg["backgroundMusic"]["enabled"] = enabled
751
+ cfg_path.parent.mkdir(parents=True, exist_ok=True)
752
+ cfg_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
753
+ except Exception:
754
+ pass # best-effort sync
755
+ return result if result else f"❌ Failed to {'enable' if enabled else 'disable'} background music"
756
+
757
+ async def set_background_music_volume(self, volume: float) -> str:
758
+ """
759
+ Set background music volume.
760
+
761
+ Args:
762
+ volume: Volume level (0.0-1.0)
763
+
764
+ Returns:
765
+ Success or error message
766
+ """
767
+ result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["volume", str(volume)])
768
+ return result if result else "❌ Failed to set background music volume"
769
+
770
+ async def get_background_music_status(self) -> str:
771
+ """
772
+ Get current background music configuration.
773
+
774
+ Returns:
775
+ Status information
776
+ """
777
+ result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["status"])
778
+ return result if result else "❌ Failed to get background music status"
779
+
780
+ async def set_reverb(self, level: str, agent: str = "default", apply_all: bool = False) -> str:
781
+ """
782
+ Set reverb level for an agent or globally.
783
+
784
+ Args:
785
+ level: Reverb level (off, light, medium, heavy, cathedral)
786
+ agent: Agent name (default: "default")
787
+ apply_all: Apply to all agents (default: False)
788
+
789
+ Returns:
790
+ Success message
791
+ """
792
+ args = ["set-reverb", level, agent]
793
+ if apply_all:
794
+ args.append("--all")
795
+ result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, args)
796
+ return result if result else f"✅ Set reverb to {level}"
797
+
798
+ async def get_reverb(self, agent: str = "default") -> str:
799
+ """
800
+ Get current reverb level for an agent.
801
+
802
+ Args:
803
+ agent: Agent name (default: "default")
804
+
805
+ Returns:
806
+ Current reverb level
807
+ """
808
+ result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, ["get-reverb", agent])
809
+ if result:
810
+ return f"Current reverb level for {agent}: {result.strip()}"
811
+ return f"❌ Failed to get reverb for {agent}"
812
+
813
+ async def list_audio_effects(self) -> str:
814
+ """
815
+ List all audio effects for all agents.
816
+
817
+ Returns:
818
+ Effects configuration
819
+ """
820
+ result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, ["list"])
821
+ return result if result else "❌ Failed to list audio effects"
822
+
823
+ async def clean_audio_cache(self) -> str:
824
+ """
825
+ Clean all TTS audio cache files and report space freed.
826
+
827
+ Non-interactive cleanup suitable for MCP tool usage. Deletes all
828
+ TTS-generated audio files (wav, mp3, aiff) while preserving
829
+ background music tracks.
830
+
831
+ Returns:
832
+ Cleanup results with file count and space freed
833
+ """
834
+ result = await self._run_script("clean-audio-cache.sh", [])
835
+ return result if result else "❌ Failed to clean audio cache"
836
+
837
+ async def set_banner(self, enabled: bool) -> str:
838
+ """
839
+ Enable or disable the TTS output banner (voice info, file path, cache size).
840
+
841
+ Args:
842
+ enabled: True to show banner, False to hide it
843
+
844
+ Returns:
845
+ Confirmation message
846
+ """
847
+ banner_file = Path.home() / ".agentvibes" / "banner-disabled"
848
+ if enabled:
849
+ # Remove the disable flag
850
+ try:
851
+ banner_file.unlink(missing_ok=True)
852
+ except Exception:
853
+ pass
854
+ return "✅ TTS banner enabled — voice info will show after each speech"
855
+ else:
856
+ # Create the disable flag
857
+ banner_file.parent.mkdir(parents=True, exist_ok=True)
858
+ banner_file.touch()
859
+ return "🔇 TTS banner disabled — speech will play without output info"
860
+
861
+ async def get_banner(self) -> str:
862
+ """
863
+ Check if the TTS output banner is enabled or disabled.
864
+
865
+ Returns:
866
+ Current banner status
867
+ """
868
+ banner_file = Path.home() / ".agentvibes" / "banner-disabled"
869
+ if banner_file.exists():
870
+ return "🔇 TTS banner: **disabled**\n\nSay: \"Turn on banner\" to re-enable"
871
+ return "✅ TTS banner: **enabled**\n\nSay: \"Turn off banner\" to disable"
872
+
873
+ # Helper methods
874
+ def _build_script_env(self) -> dict:
875
+ """Build environment dict for script execution (shared by all script runners)"""
876
+ env = os.environ.copy()
877
+
878
+ # Determine where to save settings based on context:
879
+ # 1. If cwd has .claude/ → Use cwd (real Claude Code project)
880
+ # 2. Otherwise → Use global ~/.claude/ (Claude Desktop, Warp, etc.)
881
+ # Note: Hooks are ALWAYS from package .claude/ (self.claude_dir)
882
+ cwd = Path.cwd()
883
+ if (cwd / ".claude").is_dir() and cwd != self.agentvibes_root:
884
+ env["CLAUDE_PROJECT_DIR"] = str(cwd)
885
+
886
+ # Add common locations for piper to PATH (Unix only)
887
+ if not self.is_windows:
888
+ home_dir = Path.home()
889
+ local_bin = str(home_dir / ".local" / "bin")
890
+ if "PATH" in env:
891
+ if local_bin not in env["PATH"]:
892
+ env["PATH"] = f"{local_bin}:{env['PATH']}"
893
+ else:
894
+ env["PATH"] = local_bin
895
+
896
+ return env
897
+
898
+ async def _run_script(self, script_name: str, args: list[str]) -> str:
899
+ """Run a script and return output (bash on Unix, PowerShell on Windows)"""
900
+ # Auto-resolve .sh → .ps1 on Windows (class constants handle special cases)
901
+ if self.is_windows and script_name.endswith('.sh'):
902
+ script_name = script_name[:-3] + '.ps1'
903
+ script_path = self.hooks_dir / script_name
904
+ if not script_path.exists():
905
+ return f"Script not found: {script_path}"
906
+
907
+ # Build command — PowerShell on Windows, bash on Unix
908
+ if self.is_windows:
909
+ cmd = [
910
+ "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
911
+ "-File", str(script_path)
912
+ ] + args
913
+ else:
914
+ cmd = ["bash", str(script_path)] + args
915
+
916
+ env = self._build_script_env()
917
+
918
+ try:
919
+ result = await asyncio.create_subprocess_exec(
920
+ *cmd,
921
+ stdout=asyncio.subprocess.PIPE,
922
+ stderr=asyncio.subprocess.PIPE,
923
+ env=env,
924
+ )
925
+ try:
926
+ stdout, stderr = await result.communicate()
927
+ if result.returncode == 0:
928
+ return stdout.decode().strip()
929
+ else:
930
+ error_msg = stderr.decode().strip()
931
+ if not error_msg: # If stderr is empty, include stdout for debugging
932
+ error_msg = f"Return code {result.returncode}. Stdout: {stdout.decode().strip()}"
933
+ return error_msg
934
+ finally:
935
+ # Ensure process cleanup
936
+ if result.returncode is None:
937
+ result.kill()
938
+ await result.wait()
939
+ except Exception as e:
940
+ return f"Error running script: {e}"
941
+
942
+ async def _get_current_voice(self) -> str:
943
+ """Get the currently active voice"""
944
+ result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["get"])
945
+ return result.strip() if result else "Unknown"
946
+
947
+ async def _get_personality(self) -> str:
948
+ """Get the current personality setting"""
949
+ personality_file = self.claude_dir / "tts-personality.txt"
950
+ if not personality_file.exists():
951
+ # Try global
952
+ personality_file = Path.home() / self.CLAUDE_DIR_NAME / "tts-personality.txt"
953
+
954
+ try:
955
+ if personality_file.exists():
956
+ return personality_file.read_text().strip()
957
+ except (PermissionError, UnicodeDecodeError, OSError) as e:
958
+ # Log error but don't crash - return default
959
+ import sys
960
+ print(f"Warning: Could not read personality file: {e}", file=sys.stderr)
961
+ return "normal"
962
+
963
+ async def _get_language(self) -> str:
964
+ """Get the current language setting"""
965
+ result = await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["code"])
966
+ return result.strip() if result else "english"
967
+
968
+ async def _get_provider(self) -> str:
969
+ """Get the active TTS provider"""
970
+ provider_file = self.claude_dir / "tts-provider.txt"
971
+ if not provider_file.exists():
972
+ provider_file = Path.home() / self.CLAUDE_DIR_NAME / "tts-provider.txt"
973
+
974
+ provider_labels = {
975
+ "macos": "macOS TTS",
976
+ "piper": "Piper TTS (Free, Offline)",
977
+ "termux-ssh": "Termux SSH (Android)",
978
+ "sapi": "Windows SAPI (Built-in)",
979
+ "soprano": "Soprano TTS (Ultra-fast Neural)",
980
+ }
981
+ try:
982
+ if provider_file.exists():
983
+ provider = provider_file.read_text().strip()
984
+ # Strip BOM from PowerShell-written files
985
+ provider = provider.lstrip('\ufeff')
986
+ return provider_labels.get(provider, provider)
987
+ except (PermissionError, UnicodeDecodeError, OSError) as e:
988
+ # Log error but don't crash - return default
989
+ import sys
990
+ print(f"Warning: Could not read provider file: {e}", file=sys.stderr)
991
+ # Default based on platform
992
+ if self.is_windows:
993
+ return "Windows SAPI (Built-in)"
994
+ return "Piper TTS (Free, Offline)"
995
+
996
+
997
+ # Create the MCP server
998
+ app = Server("agentvibes")
999
+ agent_vibes = AgentVibesServer()
1000
+
1001
+
1002
+ @app.list_tools()
1003
+ async def list_tools() -> list[Tool]:
1004
+ """List all available AgentVibes tools"""
1005
+ return [
1006
+ Tool(
1007
+ name="text_to_speech",
1008
+ description="""Convert text to speech using AgentVibes TTS.
1009
+
1010
+ Supports both macOS TTS and Piper (free, offline) providers.
1011
+ Can use different voices, personalities, and languages.
1012
+
1013
+ Perfect for:
1014
+ - Speaking acknowledgments and confirmations
1015
+ - Adding voice to Claude responses
1016
+ - Multi-language communication
1017
+ - Personality-driven interactions
1018
+
1019
+ Examples:
1020
+ - text_to_speech(text="Hello, I'm ready to help!")
1021
+ - text_to_speech(text="Task completed!", personality="flirty")
1022
+ - text_to_speech(text="Hola, ¿cómo estás?", language="spanish")
1023
+ """,
1024
+ inputSchema={
1025
+ "type": "object",
1026
+ "properties": {
1027
+ "text": {
1028
+ "type": "string",
1029
+ "description": "Text to convert to speech (max 500 characters)",
1030
+ },
1031
+ "voice": {
1032
+ "type": "string",
1033
+ "description": "Voice name (optional). Use list_voices to see options.",
1034
+ },
1035
+ "personality": {
1036
+ "type": "string",
1037
+ "description": "Personality style (optional). Examples: flirty, sarcastic, pirate, robot, zen",
1038
+ },
1039
+ "language": {
1040
+ "type": "string",
1041
+ "description": "Language to speak in (optional). Examples: spanish, french, german, italian",
1042
+ },
1043
+ },
1044
+ "required": ["text"],
1045
+ },
1046
+ ),
1047
+ Tool(
1048
+ name="list_voices",
1049
+ description="List all available TTS voices with current selection",
1050
+ inputSchema={"type": "object", "properties": {}},
1051
+ ),
1052
+ Tool(
1053
+ name="set_voice",
1054
+ description="Switch to a different TTS voice",
1055
+ inputSchema={
1056
+ "type": "object",
1057
+ "properties": {
1058
+ "voice_name": {
1059
+ "type": "string",
1060
+ "description": "Name of the voice to switch to",
1061
+ }
1062
+ },
1063
+ "required": ["voice_name"],
1064
+ },
1065
+ ),
1066
+ Tool(
1067
+ name="list_personalities",
1068
+ description="List all available personality styles with descriptions",
1069
+ inputSchema={"type": "object", "properties": {}},
1070
+ ),
1071
+ Tool(
1072
+ name="set_personality",
1073
+ description="Set the personality style for TTS messages",
1074
+ inputSchema={
1075
+ "type": "object",
1076
+ "properties": {
1077
+ "personality": {
1078
+ "type": "string",
1079
+ "description": "Personality name (e.g., flirty, sarcastic, pirate)",
1080
+ }
1081
+ },
1082
+ "required": ["personality"],
1083
+ },
1084
+ ),
1085
+ Tool(
1086
+ name="set_language",
1087
+ description="Set the language for TTS speech (supports 25+ languages)",
1088
+ inputSchema={
1089
+ "type": "object",
1090
+ "properties": {
1091
+ "language": {
1092
+ "type": "string",
1093
+ "description": "Language name (e.g., spanish, french, german)",
1094
+ }
1095
+ },
1096
+ "required": ["language"],
1097
+ },
1098
+ ),
1099
+ Tool(
1100
+ name="get_config",
1101
+ description="Get current voice, personality, language, and provider configuration",
1102
+ inputSchema={"type": "object", "properties": {}},
1103
+ ),
1104
+ Tool(
1105
+ name="replay_audio",
1106
+ description="Replay recently generated TTS audio",
1107
+ inputSchema={
1108
+ "type": "object",
1109
+ "properties": {
1110
+ "n": {
1111
+ "type": "integer",
1112
+ "description": "Which audio to replay (1 = most recent, default: 1)",
1113
+ "minimum": 1,
1114
+ "maximum": 10,
1115
+ }
1116
+ },
1117
+ },
1118
+ ),
1119
+ Tool(
1120
+ name="set_provider",
1121
+ description="Switch between TTS providers" + (
1122
+ ": Windows Piper, Windows SAPI, or Soprano" if agent_vibes.is_windows
1123
+ else ": macOS TTS, Piper (free, offline), Soprano, or Termux SSH (Android)"
1124
+ ),
1125
+ inputSchema={
1126
+ "type": "object",
1127
+ "properties": {
1128
+ "provider": {
1129
+ "type": "string",
1130
+ "description": (
1131
+ "Provider name: 'piper', 'sapi', or 'soprano'"
1132
+ if agent_vibes.is_windows
1133
+ else "Provider name: 'piper', 'macos', 'soprano', or 'termux-ssh'"
1134
+ ),
1135
+ "enum": (
1136
+ ["piper", "sapi", "soprano"]
1137
+ if agent_vibes.is_windows
1138
+ else ["piper", "macos", "soprano", "termux-ssh"]
1139
+ ),
1140
+ }
1141
+ },
1142
+ "required": ["provider"],
1143
+ },
1144
+ ),
1145
+ Tool(
1146
+ name="set_learn_mode",
1147
+ description="Enable or disable language learning mode. When ON, TTS speaks in both your main language and target language for bilingual learning.",
1148
+ inputSchema={
1149
+ "type": "object",
1150
+ "properties": {
1151
+ "enabled": {
1152
+ "type": "boolean",
1153
+ "description": "True to enable learning mode, False to disable"
1154
+ }
1155
+ },
1156
+ "required": ["enabled"],
1157
+ },
1158
+ ),
1159
+ Tool(
1160
+ name="set_speed",
1161
+ description="Set speech speed for main or target voice. Works with both Piper and macOS providers. Use this to make voices faster or slower.",
1162
+ inputSchema={
1163
+ "type": "object",
1164
+ "properties": {
1165
+ "speed": {
1166
+ "type": "string",
1167
+ "description": "Speed value: '0.5x' or 'slow/slower' (half speed, slower), '1x' or 'normal' (normal speed), '2x' or 'fast' (double speed, faster), '3x' or 'faster' (triple speed, very fast)"
1168
+ },
1169
+ "target": {
1170
+ "type": "boolean",
1171
+ "description": "If true, sets target language speed (for learning mode); if false or omitted, sets main voice speed",
1172
+ "default": False
1173
+ }
1174
+ },
1175
+ "required": ["speed"],
1176
+ },
1177
+ ),
1178
+ Tool(
1179
+ name="get_speed",
1180
+ description="Get current speech speed settings for main and target voices",
1181
+ inputSchema={"type": "object", "properties": {}},
1182
+ ),
1183
+ Tool(
1184
+ name="download_extra_voices",
1185
+ description="Download extra high-quality custom Piper voices from HuggingFace. Includes: Kristin (US female), Jenny (UK female with Irish accent), and Tracy/16Speakers (multi-speaker). Perfect for adding variety to your TTS voices.",
1186
+ inputSchema={
1187
+ "type": "object",
1188
+ "properties": {
1189
+ "auto_yes": {
1190
+ "type": "boolean",
1191
+ "description": "Skip confirmation prompt and download automatically (default: False)",
1192
+ "default": False
1193
+ }
1194
+ },
1195
+ },
1196
+ ),
1197
+ Tool(
1198
+ name="get_verbosity",
1199
+ description="Get current AgentVibes verbosity level (low/medium/high). Verbosity controls how much Claude speaks while working - from minimal (acknowledgments only) to maximum transparency (all reasoning spoken).",
1200
+ inputSchema={"type": "object", "properties": {}},
1201
+ ),
1202
+ Tool(
1203
+ name="set_verbosity",
1204
+ description="""Set AgentVibes verbosity level to control how much Claude speaks while working.
1205
+
1206
+ Verbosity Levels:
1207
+ - LOW: Only acknowledgments (start) and completions (end). Minimal interruption.
1208
+ - MEDIUM: + Major decisions and key findings. Balanced transparency.
1209
+ - HIGH: All reasoning, decisions, and findings. Maximum transparency.
1210
+
1211
+ Perfect for:
1212
+ - LOW: Quiet work sessions, minimal distraction
1213
+ - MEDIUM: Understanding major decisions without full narration
1214
+ - HIGH: Full transparency, learning mode, debugging complex tasks
1215
+
1216
+ Note: Changes take effect on next Claude Code session restart.""",
1217
+ inputSchema={
1218
+ "type": "object",
1219
+ "properties": {
1220
+ "level": {
1221
+ "type": "string",
1222
+ "description": "Verbosity level to set",
1223
+ "enum": ["low", "medium", "high"]
1224
+ }
1225
+ },
1226
+ "required": ["level"],
1227
+ },
1228
+ ),
1229
+ Tool(
1230
+ name="mute",
1231
+ description="Mute all AgentVibes TTS output. Creates a persistent mute flag that silences all voice output until unmuted. Persists across sessions.",
1232
+ inputSchema={"type": "object", "properties": {}},
1233
+ ),
1234
+ Tool(
1235
+ name="unmute",
1236
+ description="Unmute AgentVibes TTS output. Removes the mute flag and restores voice output.",
1237
+ inputSchema={"type": "object", "properties": {}},
1238
+ ),
1239
+ Tool(
1240
+ name="is_muted",
1241
+ description="Check if TTS is currently muted.",
1242
+ inputSchema={"type": "object", "properties": {}},
1243
+ ),
1244
+ Tool(
1245
+ name="list_background_music",
1246
+ description="List all available pre-packaged background music tracks. Shows all audio files that can be used as background music for TTS.",
1247
+ inputSchema={"type": "object", "properties": {}},
1248
+ ),
1249
+ Tool(
1250
+ name="set_background_music",
1251
+ description="""Set background music track for a specific agent, all agents, or as default. Supports smart fuzzy matching.
1252
+
1253
+ Perfect for:
1254
+ - "change background music to flamenco" - Sets for all agents
1255
+ - "set John's background music to celtic harp" - Agent-specific
1256
+ - "use chillwave as default background" - Default for new agents
1257
+
1258
+ Fuzzy matching examples:
1259
+ - "flamenco" matches "agentvibes_soft_flamenco_loop.mp3"
1260
+ - "celtic" matches "agent_vibes_celtic_harp_v1_loop.mp3"
1261
+ - "bossa" matches "agent_vibes_bossa_nova_v2_loop.mp3"
1262
+ """,
1263
+ inputSchema={
1264
+ "type": "object",
1265
+ "properties": {
1266
+ "track_name": {
1267
+ "type": "string",
1268
+ "description": "Track filename or partial name for fuzzy matching (e.g., 'celtic', 'flamenco', 'bossa nova')",
1269
+ },
1270
+ "agent_name": {
1271
+ "type": "string",
1272
+ "description": "Agent name to configure (optional). Use 'all' for all agents, omit for default",
1273
+ },
1274
+ },
1275
+ "required": ["track_name"],
1276
+ },
1277
+ ),
1278
+ Tool(
1279
+ name="enable_background_music",
1280
+ description="Enable or disable background music globally. When enabled, TTS audio will be mixed with background music at configured volume (default 30%).",
1281
+ inputSchema={
1282
+ "type": "object",
1283
+ "properties": {
1284
+ "enabled": {
1285
+ "type": "boolean",
1286
+ "description": "True to enable background music, False to disable",
1287
+ }
1288
+ },
1289
+ "required": ["enabled"],
1290
+ },
1291
+ ),
1292
+ Tool(
1293
+ name="set_background_music_volume",
1294
+ description="Set the volume level for background music (0.0-1.0). Recommended: 0.20-0.40 for subtle background ambiance.",
1295
+ inputSchema={
1296
+ "type": "object",
1297
+ "properties": {
1298
+ "volume": {
1299
+ "type": "number",
1300
+ "description": "Volume level (0.0 = silent, 0.30 = default, 1.0 = full volume)",
1301
+ "minimum": 0.0,
1302
+ "maximum": 1.0,
1303
+ }
1304
+ },
1305
+ "required": ["volume"],
1306
+ },
1307
+ ),
1308
+ Tool(
1309
+ name="get_background_music_status",
1310
+ description="Get current background music configuration including enabled status, volume, default track, and number of available tracks.",
1311
+ inputSchema={"type": "object", "properties": {}},
1312
+ ),
1313
+ Tool(
1314
+ name="set_reverb",
1315
+ description="""Set reverb level for TTS audio. Can apply globally (default agent), to a specific agent, or to all agents.
1316
+
1317
+ Reverb adds room/space ambiance to the voice, making it sound like it's in a small room, conference room, or large hall.
1318
+
1319
+ Examples:
1320
+ - set_reverb(level="medium") - Set reverb for default agent
1321
+ - set_reverb(level="cathedral", agent="Winston") - Set cathedral reverb for Winston
1322
+ - set_reverb(level="light", apply_all=True) - Set light reverb for all agents
1323
+ - set_reverb(level="off") - Turn off reverb for default agent
1324
+ """,
1325
+ inputSchema={
1326
+ "type": "object",
1327
+ "properties": {
1328
+ "level": {
1329
+ "type": "string",
1330
+ "description": "Reverb level",
1331
+ "enum": ["off", "light", "medium", "heavy", "cathedral"]
1332
+ },
1333
+ "agent": {
1334
+ "type": "string",
1335
+ "description": "Agent name (optional, defaults to 'default'). Examples: Winston, John, Mary, Amelia",
1336
+ },
1337
+ "apply_all": {
1338
+ "type": "boolean",
1339
+ "description": "Apply to all agents (optional, default: false)",
1340
+ }
1341
+ },
1342
+ "required": ["level"],
1343
+ },
1344
+ ),
1345
+ Tool(
1346
+ name="get_reverb",
1347
+ description="Get current reverb level for a specific agent or default",
1348
+ inputSchema={
1349
+ "type": "object",
1350
+ "properties": {
1351
+ "agent": {
1352
+ "type": "string",
1353
+ "description": "Agent name (optional, defaults to 'default')",
1354
+ }
1355
+ },
1356
+ },
1357
+ ),
1358
+ Tool(
1359
+ name="list_audio_effects",
1360
+ description="List current audio effects configuration for all agents, including reverb levels and other effects",
1361
+ inputSchema={"type": "object", "properties": {}},
1362
+ ),
1363
+ Tool(
1364
+ name="clean_audio_cache",
1365
+ description="Clean all TTS audio cache files and report space freed. Non-interactive cleanup that removes all wav/mp3/aiff files while preserving background music tracks.",
1366
+ inputSchema={"type": "object", "properties": {}},
1367
+ ),
1368
+ ]
1369
+
1370
+
1371
+ @app.call_tool()
1372
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
1373
+ """Handle tool calls"""
1374
+ try:
1375
+ if name == "text_to_speech":
1376
+ result = await agent_vibes.text_to_speech(
1377
+ text=arguments["text"],
1378
+ voice=arguments.get("voice"),
1379
+ personality=arguments.get("personality"),
1380
+ language=arguments.get("language"),
1381
+ )
1382
+ elif name == "list_voices":
1383
+ result = await agent_vibes.list_voices()
1384
+ elif name == "set_voice":
1385
+ result = await agent_vibes.set_voice(arguments["voice_name"])
1386
+ elif name == "list_personalities":
1387
+ result = await agent_vibes.list_personalities()
1388
+ elif name == "set_personality":
1389
+ result = await agent_vibes.set_personality(arguments["personality"])
1390
+ elif name == "set_language":
1391
+ result = await agent_vibes.set_language(arguments["language"])
1392
+ elif name == "get_config":
1393
+ result = await agent_vibes.get_config()
1394
+ elif name == "replay_audio":
1395
+ n = arguments.get("n", 1)
1396
+ result = await agent_vibes.replay_audio(n)
1397
+ elif name == "set_provider":
1398
+ result = await agent_vibes.set_provider(arguments["provider"])
1399
+ elif name == "set_learn_mode":
1400
+ result = await agent_vibes.set_learn_mode(arguments["enabled"])
1401
+ elif name == "set_speed":
1402
+ target = arguments.get("target", False)
1403
+ result = await agent_vibes.set_speed(arguments["speed"], target)
1404
+ elif name == "get_speed":
1405
+ result = await agent_vibes.get_speed()
1406
+ elif name == "download_extra_voices":
1407
+ auto_yes = arguments.get("auto_yes", False)
1408
+ result = await agent_vibes.download_extra_voices(auto_yes)
1409
+ elif name == "get_verbosity":
1410
+ result = await agent_vibes.get_verbosity()
1411
+ elif name == "set_verbosity":
1412
+ result = await agent_vibes.set_verbosity(arguments["level"])
1413
+ elif name == "mute":
1414
+ result = await agent_vibes.mute()
1415
+ elif name == "unmute":
1416
+ result = await agent_vibes.unmute()
1417
+ elif name == "is_muted":
1418
+ result = await agent_vibes.is_muted()
1419
+ elif name == "list_background_music":
1420
+ result = await agent_vibes.list_background_music()
1421
+ elif name == "set_background_music":
1422
+ track_name = arguments.get("track_name")
1423
+ agent_name = arguments.get("agent_name")
1424
+ result = await agent_vibes.set_background_music(track_name, agent_name)
1425
+ elif name == "enable_background_music":
1426
+ enabled = arguments.get("enabled")
1427
+ result = await agent_vibes.enable_background_music(enabled)
1428
+ elif name == "set_background_music_volume":
1429
+ volume = arguments.get("volume")
1430
+ result = await agent_vibes.set_background_music_volume(volume)
1431
+ elif name == "get_background_music_status":
1432
+ result = await agent_vibes.get_background_music_status()
1433
+ elif name == "set_reverb":
1434
+ level = arguments["level"]
1435
+ agent = arguments.get("agent", "default")
1436
+ apply_all = arguments.get("apply_all", False)
1437
+ result = await agent_vibes.set_reverb(level, agent, apply_all)
1438
+ elif name == "get_reverb":
1439
+ agent = arguments.get("agent", "default")
1440
+ result = await agent_vibes.get_reverb(agent)
1441
+ elif name == "list_audio_effects":
1442
+ result = await agent_vibes.list_audio_effects()
1443
+ elif name == "clean_audio_cache":
1444
+ result = await agent_vibes.clean_audio_cache()
1445
+ else:
1446
+ result = f"Unknown tool: {name}"
1447
+
1448
+ return [TextContent(type="text", text=result)]
1449
+
1450
+ except Exception as e:
1451
+ return [TextContent(type="text", text=f"Error: {str(e)}")]
1452
+
1453
+
1454
+ async def main():
1455
+ """Run the MCP server"""
1456
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
1457
+ await app.run(
1458
+ read_stream,
1459
+ write_stream,
1460
+ app.create_initialization_options(),
1461
+ )
1462
+
1463
+
1464
+ if __name__ == "__main__":
1465
+ asyncio.run(main())