agentvibes 5.11.2 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-processor.sh +1 -1
- package/.claude/hooks/background-music-manager.sh +2 -1
- package/.claude/hooks/bmad-speak-enhanced.sh +37 -22
- package/.claude/hooks/bmad-speak.sh +26 -8
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +1 -1
- package/.claude/hooks/play-tts-kokoro.sh +9 -0
- package/.claude/hooks/play-tts-piper.sh +13 -1
- package/.claude/hooks/play-tts-ssh-remote.sh +117 -7
- package/.claude/hooks/play-tts-termux-ssh.sh +1 -1
- package/.claude/hooks/play-tts.sh +106 -13
- package/.claude/hooks-windows/background-music-manager.ps1 +2 -1
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +14 -0
- package/.claude/hooks-windows/play-tts.ps1 +163 -30
- package/.claude/hooks-windows/tts-watcher.ps1 +55 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +101 -1
- package/README.md +11 -6
- package/RELEASE_NOTES.md +67 -0
- package/bin/mcp-server.sh +6 -19
- package/bin/resolve-utterance.js +137 -0
- package/mcp-server/server.py +280 -99
- package/mcp-server/test_mcp_correctness.py +486 -0
- package/mcp-server/test_windows_script_parity.py +341 -336
- package/package.json +3 -2
- package/setup-windows.ps1 +867 -815
- package/src/console/app.js +14 -11
- package/src/console/footer-config.js +0 -4
- package/src/console/navigation.js +0 -1
- package/src/console/tabs/agents-tab.js +96 -11
- package/src/console/tabs/music-tab.js +108 -3
- package/src/console/tabs/placeholder-tab.js +0 -2
- package/src/console/tabs/settings-tab.js +41 -2
- package/src/console/tabs/setup-tab.js +34 -6
- package/src/console/tabs/voices-tab.js +19 -9
- package/src/console/widgets/track-picker.js +3 -3
- package/src/i18n/de.js +0 -1
- package/src/i18n/en.js +0 -1
- package/src/i18n/es.js +0 -1
- package/src/i18n/fr.js +0 -1
- package/src/i18n/hi.js +0 -1
- package/src/i18n/ja.js +0 -1
- package/src/i18n/ko.js +0 -1
- package/src/i18n/pt.js +0 -1
- package/src/i18n/zh-CN.js +0 -1
- package/src/installer.js +205 -15
- package/src/services/config-service.js +264 -264
- package/src/services/llm-provider-service.js +7 -7
- package/src/services/navigation-service.js +1 -1
- package/src/services/utterance-loader.js +280 -0
- package/src/services/utterance-resolver.js +526 -0
- package/templates/agentvibes-receiver.sh +74 -12
- package/voice-assignments.json +8245 -0
package/mcp-server/server.py
CHANGED
|
@@ -46,12 +46,44 @@ import os
|
|
|
46
46
|
import platform
|
|
47
47
|
import re as _re
|
|
48
48
|
import subprocess
|
|
49
|
+
from dataclasses import dataclass
|
|
49
50
|
from pathlib import Path
|
|
50
51
|
from typing import Optional
|
|
51
52
|
|
|
52
53
|
from mcp.server import Server
|
|
53
|
-
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
|
|
54
|
+
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource, CallToolResult
|
|
54
55
|
import mcp.server.stdio
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Default per-script timeout for _run_script(). Keeps a hung/interactive
|
|
59
|
+
# manager script (e.g. a `read -p` prompt reached by mistake) from wedging
|
|
60
|
+
# the MCP server forever and eating the stdio JSON-RPC stream.
|
|
61
|
+
DEFAULT_SCRIPT_TIMEOUT = 30.0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ScriptResult:
|
|
66
|
+
"""Structured result of running a hook script.
|
|
67
|
+
|
|
68
|
+
Callers MUST branch on `ok`/`returncode`, never on emoji/text sniffing —
|
|
69
|
+
Windows manager scripts print plain text (no ✅/✓/🎭), so any
|
|
70
|
+
`"<emoji>" in stdout` check silently reports failure on Windows even when
|
|
71
|
+
the script succeeded.
|
|
72
|
+
"""
|
|
73
|
+
returncode: int
|
|
74
|
+
stdout: str
|
|
75
|
+
stderr: str
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def ok(self) -> bool:
|
|
79
|
+
return self.returncode == 0
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def error_detail(self) -> str:
|
|
83
|
+
"""Best-effort human-readable error text for failure messages."""
|
|
84
|
+
return self.stderr or self.stdout or f"exit code {self.returncode}"
|
|
85
|
+
|
|
86
|
+
|
|
55
87
|
class AgentVibesServer:
|
|
56
88
|
"""MCP Server for AgentVibes TTS functionality"""
|
|
57
89
|
|
|
@@ -87,6 +119,14 @@ class AgentVibesServer:
|
|
|
87
119
|
# Store AgentVibes root directory for environment variable
|
|
88
120
|
self.agentvibes_root = self.claude_dir.parent
|
|
89
121
|
|
|
122
|
+
# Serializes the "mutate global personality/language -> speak -> restore"
|
|
123
|
+
# critical section in text_to_speech() so concurrent MCP tool calls
|
|
124
|
+
# cannot interleave and corrupt persistent state (residual risk: this
|
|
125
|
+
# only protects against concurrent calls *within this process* — a
|
|
126
|
+
# second MCP server process or a slash-command CLI invocation writing
|
|
127
|
+
# the same file at the same time is not covered; see story 8.2 notes).
|
|
128
|
+
self._override_lock = asyncio.Lock()
|
|
129
|
+
|
|
90
130
|
def _find_claude_dir(self) -> Path:
|
|
91
131
|
"""Find the .claude directory relative to this script"""
|
|
92
132
|
# Get the AgentVibes root directory (parent of mcp-server)
|
|
@@ -278,14 +318,33 @@ class AgentVibesServer:
|
|
|
278
318
|
Returns:
|
|
279
319
|
Success message with audio file path
|
|
280
320
|
"""
|
|
281
|
-
# Store original settings to restore later
|
|
321
|
+
# Store original settings to restore later. Mutating the personality/
|
|
322
|
+
# language files is inherently racy across processes; the lock below
|
|
323
|
+
# only protects against concurrent tool calls within *this* server
|
|
324
|
+
# instance (see the residual-risk note on self._override_lock).
|
|
282
325
|
original_personality = None
|
|
326
|
+
personality_file_existed = True
|
|
283
327
|
original_language = None
|
|
328
|
+
needs_override = bool(personality or language)
|
|
329
|
+
|
|
330
|
+
if needs_override:
|
|
331
|
+
await self._override_lock.acquire()
|
|
284
332
|
|
|
285
333
|
try:
|
|
286
334
|
# Temporarily set personality if specified
|
|
335
|
+
personality_path = self._get_config_dir() / "tts-personality.txt"
|
|
287
336
|
if personality:
|
|
288
|
-
|
|
337
|
+
# Read the ORIGINAL from the SAME file the manager writes to
|
|
338
|
+
# (the config dir). _get_personality() reads a different set of
|
|
339
|
+
# dirs (package dir, then global ~/.claude) and, from inside a
|
|
340
|
+
# host project, returns the wrong value — restoring that would
|
|
341
|
+
# overwrite the project's real personality. Non-Destructive Rule.
|
|
342
|
+
personality_file_existed = personality_path.exists()
|
|
343
|
+
if personality_file_existed:
|
|
344
|
+
try:
|
|
345
|
+
original_personality = personality_path.read_text(encoding="utf-8").strip()
|
|
346
|
+
except OSError:
|
|
347
|
+
original_personality = None # can't read → don't clobber on restore
|
|
289
348
|
await self._run_script(
|
|
290
349
|
self.PERSONALITY_MANAGER_SCRIPT, ["set", personality]
|
|
291
350
|
)
|
|
@@ -325,8 +384,16 @@ class AgentVibesServer:
|
|
|
325
384
|
|
|
326
385
|
env = self._build_script_env()
|
|
327
386
|
|
|
387
|
+
# Declare voice provenance so the resolver treats an MCP-requested
|
|
388
|
+
# voice as a genuine explicit pick (user-explicit), never demoting it
|
|
389
|
+
# to a per-LLM/default row the way it would an LLM echo (F-1). Only
|
|
390
|
+
# set when the caller actually asked for a specific voice.
|
|
391
|
+
if voice:
|
|
392
|
+
env["AGENTVIBES_VOICE_SOURCE"] = "user-explicit"
|
|
393
|
+
|
|
328
394
|
result = await asyncio.create_subprocess_exec(
|
|
329
395
|
*args,
|
|
396
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
330
397
|
stdout=asyncio.subprocess.PIPE,
|
|
331
398
|
stderr=asyncio.subprocess.PIPE,
|
|
332
399
|
env=env,
|
|
@@ -377,15 +444,48 @@ class AgentVibesServer:
|
|
|
377
444
|
await result.wait()
|
|
378
445
|
|
|
379
446
|
finally:
|
|
380
|
-
# Restore original
|
|
381
|
-
if
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
447
|
+
# Restore original personality. personality-manager.sh has no
|
|
448
|
+
# delete-on-default behavior (unlike language-manager.sh), so if
|
|
449
|
+
# no personality file existed before this call, restore by
|
|
450
|
+
# deleting the file rather than writing "normal" into it — the
|
|
451
|
+
# non-destructive-config rule means a temporary per-call override
|
|
452
|
+
# must not leave a permanent file behind that wasn't there before.
|
|
453
|
+
if personality:
|
|
454
|
+
if personality_file_existed:
|
|
455
|
+
if original_personality is not None:
|
|
456
|
+
restore = await self._run_script(
|
|
457
|
+
self.PERSONALITY_MANAGER_SCRIPT, ["set", original_personality]
|
|
458
|
+
)
|
|
459
|
+
if not restore.ok:
|
|
460
|
+
import sys
|
|
461
|
+
print(
|
|
462
|
+
f"Warning: failed to restore personality "
|
|
463
|
+
f"'{original_personality}': {restore.error_detail}",
|
|
464
|
+
file=sys.stderr,
|
|
465
|
+
)
|
|
466
|
+
else:
|
|
467
|
+
# No personality file existed before this call — restore by
|
|
468
|
+
# deleting (same config dir we captured existence from).
|
|
469
|
+
try:
|
|
470
|
+
personality_path.unlink()
|
|
471
|
+
except OSError:
|
|
472
|
+
pass
|
|
473
|
+
if original_language is not None:
|
|
474
|
+
# "english"/"reset" makes language-manager.sh *delete* the
|
|
475
|
+
# language file rather than write it, so this naturally
|
|
476
|
+
# restores "no override was ever set" correctly too.
|
|
477
|
+
restore_lang = await self._run_script(
|
|
387
478
|
self.LANGUAGE_MANAGER_SCRIPT, ["set", original_language]
|
|
388
479
|
)
|
|
480
|
+
if not restore_lang.ok:
|
|
481
|
+
import sys
|
|
482
|
+
print(
|
|
483
|
+
f"Warning: failed to restore language "
|
|
484
|
+
f"'{original_language}': {restore_lang.error_detail}",
|
|
485
|
+
file=sys.stderr,
|
|
486
|
+
)
|
|
487
|
+
if needs_override:
|
|
488
|
+
self._override_lock.release()
|
|
389
489
|
|
|
390
490
|
async def list_voices(self) -> str:
|
|
391
491
|
"""
|
|
@@ -400,8 +500,8 @@ class AgentVibesServer:
|
|
|
400
500
|
|
|
401
501
|
# voice-manager.sh list-simple is now provider-aware
|
|
402
502
|
result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["list-simple"])
|
|
403
|
-
if result:
|
|
404
|
-
voices = result.strip().split("\n")
|
|
503
|
+
if result.ok and result.stdout:
|
|
504
|
+
voices = result.stdout.strip().split("\n")
|
|
405
505
|
voices = [v for v in voices if v] # Filter empty strings
|
|
406
506
|
|
|
407
507
|
if not voices:
|
|
@@ -460,7 +560,7 @@ class AgentVibesServer:
|
|
|
460
560
|
output += f"\n💡 Switch to {alternative_provider}? Use: set_provider(provider=\"{alternative_provider.lower()}\")\n"
|
|
461
561
|
|
|
462
562
|
return output
|
|
463
|
-
return "❌ Failed to list voices"
|
|
563
|
+
return f"❌ Failed to list voices: {result.error_detail}"
|
|
464
564
|
|
|
465
565
|
async def set_voice(self, voice_name: str) -> str:
|
|
466
566
|
"""
|
|
@@ -512,11 +612,14 @@ class AgentVibesServer:
|
|
|
512
612
|
result = await self._run_script(
|
|
513
613
|
self.VOICE_MANAGER_SCRIPT, ["switch", resolved_name, "--silent"]
|
|
514
614
|
)
|
|
515
|
-
if result
|
|
615
|
+
if result.ok:
|
|
516
616
|
if original_name.lower() != resolved_name.lower():
|
|
517
617
|
return f"✅ Voice switched to: {original_name} ({resolved_name})"
|
|
518
618
|
return f"✅ Voice switched to: {voice_name}"
|
|
519
|
-
return
|
|
619
|
+
return (
|
|
620
|
+
f"❌ Failed to switch voice — could not resolve '{voice_name}'. "
|
|
621
|
+
f"Try 'list_voices' to see available names. ({result.error_detail})"
|
|
622
|
+
)
|
|
520
623
|
|
|
521
624
|
async def list_personalities(self) -> str:
|
|
522
625
|
"""
|
|
@@ -526,7 +629,9 @@ class AgentVibesServer:
|
|
|
526
629
|
Formatted list of personalities with descriptions
|
|
527
630
|
"""
|
|
528
631
|
result = await self._run_script(self.PERSONALITY_MANAGER_SCRIPT, ["list"])
|
|
529
|
-
|
|
632
|
+
if result.ok:
|
|
633
|
+
return result.stdout
|
|
634
|
+
return f"❌ Failed to list personalities: {result.error_detail}"
|
|
530
635
|
|
|
531
636
|
async def set_personality(self, personality: str) -> str:
|
|
532
637
|
"""
|
|
@@ -538,12 +643,18 @@ class AgentVibesServer:
|
|
|
538
643
|
Returns:
|
|
539
644
|
Success or error message
|
|
540
645
|
"""
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
646
|
+
# Serialize against text_to_speech's temporary override/restore so a
|
|
647
|
+
# deliberate set here can't be silently reverted by an in-flight call's
|
|
648
|
+
# restore step (they mutate the same tts-personality.txt).
|
|
649
|
+
async with self._override_lock:
|
|
650
|
+
result = await self._run_script(
|
|
651
|
+
self.PERSONALITY_MANAGER_SCRIPT, ["set", personality]
|
|
652
|
+
)
|
|
653
|
+
if result.ok:
|
|
654
|
+
# Windows (.ps1) scripts print plain text with no 🎭 marker; add
|
|
655
|
+
# our own so the tool's output stays consistent across platforms.
|
|
656
|
+
return result.stdout if "🎭" in result.stdout else f"🎭 {result.stdout}"
|
|
657
|
+
return f"❌ Failed to set personality: {result.error_detail}"
|
|
547
658
|
|
|
548
659
|
async def get_config(self) -> str:
|
|
549
660
|
"""
|
|
@@ -591,10 +702,14 @@ class AgentVibesServer:
|
|
|
591
702
|
Returns:
|
|
592
703
|
Success or error message
|
|
593
704
|
"""
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
705
|
+
# Serialize against text_to_speech's temporary override/restore (both
|
|
706
|
+
# mutate the same language config), so a deliberate set here isn't
|
|
707
|
+
# silently reverted by an in-flight call's restore step.
|
|
708
|
+
async with self._override_lock:
|
|
709
|
+
result = await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
|
|
710
|
+
if result.ok:
|
|
711
|
+
return result.stdout if "✓" in result.stdout else f"✓ {result.stdout}"
|
|
712
|
+
return f"❌ Failed to set language: {result.error_detail}"
|
|
598
713
|
|
|
599
714
|
async def replay_audio(self, n: int = 1) -> str:
|
|
600
715
|
"""
|
|
@@ -607,9 +722,9 @@ class AgentVibesServer:
|
|
|
607
722
|
Success or error message
|
|
608
723
|
"""
|
|
609
724
|
result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["replay", str(n)])
|
|
610
|
-
if result
|
|
611
|
-
return result
|
|
612
|
-
return f"❌ Failed to replay audio: {result}"
|
|
725
|
+
if result.ok:
|
|
726
|
+
return result.stdout if "🔊" in result.stdout else f"🔊 {result.stdout}"
|
|
727
|
+
return f"❌ Failed to replay audio: {result.error_detail}"
|
|
613
728
|
|
|
614
729
|
async def set_provider(self, provider: str) -> str:
|
|
615
730
|
"""
|
|
@@ -630,7 +745,7 @@ class AgentVibesServer:
|
|
|
630
745
|
return f"❌ Invalid provider: {provider}. Choose from: {', '.join(valid_providers)}"
|
|
631
746
|
|
|
632
747
|
result = await self._run_script("provider-manager.sh", ["switch", provider])
|
|
633
|
-
if result
|
|
748
|
+
if result.ok:
|
|
634
749
|
# Automatically speak confirmation in the new provider's voice
|
|
635
750
|
provider_names = {
|
|
636
751
|
"macos": "macOS",
|
|
@@ -650,15 +765,15 @@ class AgentVibesServer:
|
|
|
650
765
|
timeout=5.0
|
|
651
766
|
)
|
|
652
767
|
# Return the provider switch result plus TTS confirmation
|
|
653
|
-
return f"{result}\n🔊 Spoken confirmation: {confirmation_text}"
|
|
768
|
+
return f"{result.stdout}\n🔊 Spoken confirmation: {confirmation_text}"
|
|
654
769
|
except asyncio.TimeoutError:
|
|
655
770
|
# Timeout - provider may need setup (e.g., Piper not installed)
|
|
656
|
-
return f"{result}\n⚠️ Provider switched (TTS confirmation timed out - provider may need setup)"
|
|
771
|
+
return f"{result.stdout}\n⚠️ Provider switched (TTS confirmation timed out - provider may need setup)"
|
|
657
772
|
except Exception as e:
|
|
658
773
|
# If TTS fails, still return success for the provider switch
|
|
659
|
-
return f"{result}\n⚠️ Provider switched but TTS confirmation failed: {e}"
|
|
774
|
+
return f"{result.stdout}\n⚠️ Provider switched but TTS confirmation failed: {e}"
|
|
660
775
|
|
|
661
|
-
return f"❌ Failed to switch provider: {result}"
|
|
776
|
+
return f"❌ Failed to switch provider: {result.error_detail}"
|
|
662
777
|
|
|
663
778
|
async def set_learn_mode(self, enabled: bool) -> str:
|
|
664
779
|
"""
|
|
@@ -674,9 +789,9 @@ class AgentVibesServer:
|
|
|
674
789
|
"""
|
|
675
790
|
action = "enable" if enabled else "disable"
|
|
676
791
|
result = await self._run_script("learn-manager.sh", [action])
|
|
677
|
-
if result
|
|
678
|
-
return result
|
|
679
|
-
return f"❌ Failed to set learn mode: {result}"
|
|
792
|
+
if result.ok:
|
|
793
|
+
return result.stdout if "✓" in result.stdout else f"✓ {result.stdout}"
|
|
794
|
+
return f"❌ Failed to set learn mode: {result.error_detail}"
|
|
680
795
|
|
|
681
796
|
async def set_speed(self, speed: str, target: bool = False) -> str:
|
|
682
797
|
"""
|
|
@@ -697,7 +812,7 @@ class AgentVibesServer:
|
|
|
697
812
|
|
|
698
813
|
args = ["target", speed] if target else [speed]
|
|
699
814
|
result = await self._run_script("speed-manager.sh", args)
|
|
700
|
-
if result
|
|
815
|
+
if result.ok:
|
|
701
816
|
# Simple test messages to demonstrate the new speed
|
|
702
817
|
test_messages = [
|
|
703
818
|
"Testing speed change",
|
|
@@ -713,12 +828,12 @@ class AgentVibesServer:
|
|
|
713
828
|
try:
|
|
714
829
|
# Speak the test message to demonstrate the new speed
|
|
715
830
|
await self.text_to_speech(test_message)
|
|
716
|
-
return f"{result}\n🔊 Testing new speed: \"{test_message}\""
|
|
831
|
+
return f"{result.stdout}\n🔊 Testing new speed: \"{test_message}\""
|
|
717
832
|
except Exception as e:
|
|
718
833
|
# If TTS fails, still return success for the speed change
|
|
719
|
-
return f"{result}\n⚠️ Speed changed but demo failed: {e}"
|
|
834
|
+
return f"{result.stdout}\n⚠️ Speed changed but demo failed: {e}"
|
|
720
835
|
|
|
721
|
-
return f"❌ Failed to set speed: {result}"
|
|
836
|
+
return f"❌ Failed to set speed: {result.error_detail}"
|
|
722
837
|
|
|
723
838
|
async def get_speed(self) -> str:
|
|
724
839
|
"""
|
|
@@ -728,7 +843,7 @@ class AgentVibesServer:
|
|
|
728
843
|
Current speed settings for main and target voices
|
|
729
844
|
"""
|
|
730
845
|
result = await self._run_script("speed-manager.sh", ["get"])
|
|
731
|
-
return result if result else "❌ Failed to get speed settings"
|
|
846
|
+
return result.stdout if result.ok else f"❌ Failed to get speed settings: {result.error_detail}"
|
|
732
847
|
|
|
733
848
|
async def download_extra_voices(self, auto_yes: bool = False) -> str:
|
|
734
849
|
"""
|
|
@@ -742,11 +857,23 @@ class AgentVibesServer:
|
|
|
742
857
|
Returns:
|
|
743
858
|
Success message with download summary
|
|
744
859
|
"""
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return
|
|
749
|
-
|
|
860
|
+
if not auto_yes:
|
|
861
|
+
# download-extra-voices.sh hits `read -p "...? [Y/n]: "` when no
|
|
862
|
+
# --yes flag is given. Since stdin is always DEVNULL (see
|
|
863
|
+
# _run_script), that read would return EOF/empty rather than
|
|
864
|
+
# hang — but reaching it at all is still the wrong behavior for
|
|
865
|
+
# an MCP tool: an LLM caller can't answer an interactive prompt.
|
|
866
|
+
# Fail fast with a clear, actionable error instead of ever
|
|
867
|
+
# spawning the script.
|
|
868
|
+
return (
|
|
869
|
+
"⚠️ Confirmation required: call download_extra_voices(auto_yes=True) "
|
|
870
|
+
"to download the extra voices. This tool cannot answer an interactive "
|
|
871
|
+
"Y/n prompt, so it refuses to start the download without explicit consent."
|
|
872
|
+
)
|
|
873
|
+
result = await self._run_script("download-extra-voices.sh", ["--yes"], timeout=180.0)
|
|
874
|
+
if result.ok:
|
|
875
|
+
return result.stdout
|
|
876
|
+
return f"❌ Failed to download extra voices: {result.error_detail}"
|
|
750
877
|
|
|
751
878
|
async def get_verbosity(self) -> str:
|
|
752
879
|
"""
|
|
@@ -756,8 +883,8 @@ class AgentVibesServer:
|
|
|
756
883
|
Current verbosity level with description
|
|
757
884
|
"""
|
|
758
885
|
result = await self._run_script("verbosity-manager.sh", ["get"])
|
|
759
|
-
if result:
|
|
760
|
-
level = result.strip()
|
|
886
|
+
if result.ok:
|
|
887
|
+
level = result.stdout.strip()
|
|
761
888
|
descriptions = {
|
|
762
889
|
"low": "LOW - Acknowledgments + Completions only (minimal)",
|
|
763
890
|
"medium": "MEDIUM - + Major decisions and findings (balanced)",
|
|
@@ -765,7 +892,7 @@ class AgentVibesServer:
|
|
|
765
892
|
}
|
|
766
893
|
desc = descriptions.get(level, level)
|
|
767
894
|
return f"🎙️ Current Verbosity: {desc}\n\n💡 Change with: set_verbosity(level=\"low|medium|high\")"
|
|
768
|
-
return "❌ Failed to get verbosity level"
|
|
895
|
+
return f"❌ Failed to get verbosity level: {result.error_detail}"
|
|
769
896
|
|
|
770
897
|
async def set_verbosity(self, level: str) -> str:
|
|
771
898
|
"""
|
|
@@ -778,9 +905,10 @@ class AgentVibesServer:
|
|
|
778
905
|
Success or error message
|
|
779
906
|
"""
|
|
780
907
|
result = await self._run_script("verbosity-manager.sh", ["set", level])
|
|
781
|
-
if result
|
|
782
|
-
|
|
783
|
-
|
|
908
|
+
if result.ok:
|
|
909
|
+
body = result.stdout if "✅" in result.stdout else f"✅ {result.stdout}"
|
|
910
|
+
return f"{body}\n\n⚠️ Restart Claude Code for changes to take effect"
|
|
911
|
+
return f"❌ Failed to set verbosity: {result.error_detail}"
|
|
784
912
|
|
|
785
913
|
def _get_mute_files(self) -> list:
|
|
786
914
|
"""Get all mute file paths for current platform"""
|
|
@@ -866,7 +994,7 @@ class AgentVibesServer:
|
|
|
866
994
|
Formatted list of all pre-packaged background music files
|
|
867
995
|
"""
|
|
868
996
|
result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["list"])
|
|
869
|
-
return result if result else "❌ Failed to list background music"
|
|
997
|
+
return result.stdout if result.ok else f"❌ Failed to list background music: {result.error_detail}"
|
|
870
998
|
|
|
871
999
|
async def set_background_music(self, track_name: str, agent_name: Optional[str] = None) -> str:
|
|
872
1000
|
"""
|
|
@@ -883,12 +1011,12 @@ class AgentVibesServer:
|
|
|
883
1011
|
|
|
884
1012
|
# Get list of available tracks for fuzzy matching
|
|
885
1013
|
list_result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["list"])
|
|
886
|
-
if not list_result
|
|
887
|
-
return "❌ Failed to list background music tracks"
|
|
1014
|
+
if not list_result.ok:
|
|
1015
|
+
return f"❌ Failed to list background music tracks: {list_result.error_detail}"
|
|
888
1016
|
|
|
889
1017
|
# Parse track names
|
|
890
1018
|
tracks = []
|
|
891
|
-
for line in list_result.split("\n"):
|
|
1019
|
+
for line in list_result.stdout.split("\n"):
|
|
892
1020
|
match = re.match(r'\s*\d+\.\s+(.+)', line.strip())
|
|
893
1021
|
if match:
|
|
894
1022
|
tracks.append(match.group(1).strip())
|
|
@@ -926,11 +1054,11 @@ class AgentVibesServer:
|
|
|
926
1054
|
# Set as default
|
|
927
1055
|
result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["set-default", matched_track])
|
|
928
1056
|
|
|
929
|
-
if result
|
|
1057
|
+
if result.ok:
|
|
930
1058
|
if matched_track.lower() != track_name.lower():
|
|
931
|
-
return f"{result}\n\n🔍 Matched '{track_name}' to '{matched_track}'"
|
|
932
|
-
return result
|
|
933
|
-
return f"❌ Failed to set background music: {result}"
|
|
1059
|
+
return f"{result.stdout}\n\n🔍 Matched '{track_name}' to '{matched_track}'"
|
|
1060
|
+
return result.stdout
|
|
1061
|
+
return f"❌ Failed to set background music: {result.error_detail}"
|
|
934
1062
|
|
|
935
1063
|
async def enable_background_music(self, enabled: bool) -> str:
|
|
936
1064
|
"""
|
|
@@ -958,7 +1086,9 @@ class AgentVibesServer:
|
|
|
958
1086
|
cfg_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
|
|
959
1087
|
except Exception:
|
|
960
1088
|
pass # best-effort sync
|
|
961
|
-
|
|
1089
|
+
if result.ok:
|
|
1090
|
+
return result.stdout
|
|
1091
|
+
return f"❌ Failed to {'enable' if enabled else 'disable'} background music: {result.error_detail}"
|
|
962
1092
|
|
|
963
1093
|
async def set_background_music_volume(self, volume: float) -> str:
|
|
964
1094
|
"""
|
|
@@ -971,7 +1101,7 @@ class AgentVibesServer:
|
|
|
971
1101
|
Success or error message
|
|
972
1102
|
"""
|
|
973
1103
|
result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["volume", str(volume)])
|
|
974
|
-
return result if result else "❌ Failed to set background music volume"
|
|
1104
|
+
return result.stdout if result.ok else f"❌ Failed to set background music volume: {result.error_detail}"
|
|
975
1105
|
|
|
976
1106
|
async def get_background_music_status(self) -> str:
|
|
977
1107
|
"""
|
|
@@ -981,7 +1111,7 @@ class AgentVibesServer:
|
|
|
981
1111
|
Status information
|
|
982
1112
|
"""
|
|
983
1113
|
result = await self._run_script(self.BACKGROUND_MUSIC_MANAGER_SCRIPT, ["status"])
|
|
984
|
-
return result if result else "❌ Failed to get background music status"
|
|
1114
|
+
return result.stdout if result.ok else f"❌ Failed to get background music status: {result.error_detail}"
|
|
985
1115
|
|
|
986
1116
|
async def set_reverb(self, level: str, agent: str = "default", apply_all: bool = False) -> str:
|
|
987
1117
|
"""
|
|
@@ -999,7 +1129,9 @@ class AgentVibesServer:
|
|
|
999
1129
|
if apply_all:
|
|
1000
1130
|
args.append("--all")
|
|
1001
1131
|
result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, args)
|
|
1002
|
-
|
|
1132
|
+
if result.ok:
|
|
1133
|
+
return result.stdout if result.stdout else f"✅ Set reverb to {level}"
|
|
1134
|
+
return f"❌ Failed to set reverb: {result.error_detail}"
|
|
1003
1135
|
|
|
1004
1136
|
async def get_reverb(self, agent: str = "default") -> str:
|
|
1005
1137
|
"""
|
|
@@ -1012,9 +1144,9 @@ class AgentVibesServer:
|
|
|
1012
1144
|
Current reverb level
|
|
1013
1145
|
"""
|
|
1014
1146
|
result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, ["get-reverb", agent])
|
|
1015
|
-
if result:
|
|
1016
|
-
return f"Current reverb level for {agent}: {result.strip()}"
|
|
1017
|
-
return f"❌ Failed to get reverb for {agent}"
|
|
1147
|
+
if result.ok:
|
|
1148
|
+
return f"Current reverb level for {agent}: {result.stdout.strip()}"
|
|
1149
|
+
return f"❌ Failed to get reverb for {agent}: {result.error_detail}"
|
|
1018
1150
|
|
|
1019
1151
|
async def list_audio_effects(self) -> str:
|
|
1020
1152
|
"""
|
|
@@ -1024,7 +1156,7 @@ class AgentVibesServer:
|
|
|
1024
1156
|
Effects configuration
|
|
1025
1157
|
"""
|
|
1026
1158
|
result = await self._run_script(self.EFFECTS_MANAGER_SCRIPT, ["list"])
|
|
1027
|
-
return result if result else "❌ Failed to list audio effects"
|
|
1159
|
+
return result.stdout if result.ok else f"❌ Failed to list audio effects: {result.error_detail}"
|
|
1028
1160
|
|
|
1029
1161
|
async def clean_audio_cache(self) -> str:
|
|
1030
1162
|
"""
|
|
@@ -1038,7 +1170,7 @@ class AgentVibesServer:
|
|
|
1038
1170
|
Cleanup results with file count and space freed
|
|
1039
1171
|
"""
|
|
1040
1172
|
result = await self._run_script("clean-audio-cache.sh", [])
|
|
1041
|
-
return result if result else "❌ Failed to clean audio cache"
|
|
1173
|
+
return result.stdout if result.ok else f"❌ Failed to clean audio cache: {result.error_detail}"
|
|
1042
1174
|
|
|
1043
1175
|
# ── Hermes config helpers ────────────────────────────────────────────────
|
|
1044
1176
|
|
|
@@ -1189,14 +1321,28 @@ class AgentVibesServer:
|
|
|
1189
1321
|
|
|
1190
1322
|
return env
|
|
1191
1323
|
|
|
1192
|
-
async def _run_script(
|
|
1193
|
-
|
|
1324
|
+
async def _run_script(
|
|
1325
|
+
self,
|
|
1326
|
+
script_name: str,
|
|
1327
|
+
args: list[str],
|
|
1328
|
+
timeout: float = DEFAULT_SCRIPT_TIMEOUT,
|
|
1329
|
+
) -> ScriptResult:
|
|
1330
|
+
"""Run a script and return its (returncode, stdout, stderr) as a ScriptResult.
|
|
1331
|
+
|
|
1332
|
+
Callers MUST branch on `.ok`/`.returncode` — never on text/emoji
|
|
1333
|
+
content — because Windows manager scripts (.ps1) print plain text
|
|
1334
|
+
where the Unix (.sh) scripts print an emoji marker.
|
|
1335
|
+
|
|
1336
|
+
`stdin` is always DEVNULL and a timeout is always enforced so a
|
|
1337
|
+
script that reaches an interactive prompt (e.g. `read -p`) cannot
|
|
1338
|
+
inherit the MCP stdio JSON-RPC stream or hang the server forever.
|
|
1339
|
+
"""
|
|
1194
1340
|
# Auto-resolve .sh → .ps1 on Windows (class constants handle special cases)
|
|
1195
1341
|
if self.is_windows and script_name.endswith('.sh'):
|
|
1196
1342
|
script_name = script_name[:-3] + '.ps1'
|
|
1197
1343
|
script_path = self.hooks_dir / script_name
|
|
1198
1344
|
if not script_path.exists():
|
|
1199
|
-
return f"Script not found: {script_path}"
|
|
1345
|
+
return ScriptResult(127, "", f"Script not found: {script_path}")
|
|
1200
1346
|
|
|
1201
1347
|
# Build command — PowerShell on Windows, bash on Unix
|
|
1202
1348
|
if self.is_windows:
|
|
@@ -1209,34 +1355,42 @@ class AgentVibesServer:
|
|
|
1209
1355
|
|
|
1210
1356
|
env = self._build_script_env()
|
|
1211
1357
|
|
|
1358
|
+
proc = None
|
|
1212
1359
|
try:
|
|
1213
|
-
|
|
1360
|
+
proc = await asyncio.create_subprocess_exec(
|
|
1214
1361
|
*cmd,
|
|
1362
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
1215
1363
|
stdout=asyncio.subprocess.PIPE,
|
|
1216
1364
|
stderr=asyncio.subprocess.PIPE,
|
|
1217
1365
|
env=env,
|
|
1218
1366
|
)
|
|
1219
1367
|
try:
|
|
1220
|
-
stdout, stderr = await
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
if
|
|
1231
|
-
|
|
1232
|
-
|
|
1368
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
1369
|
+
except asyncio.TimeoutError:
|
|
1370
|
+
proc.kill()
|
|
1371
|
+
await proc.wait()
|
|
1372
|
+
return ScriptResult(
|
|
1373
|
+
-1, "",
|
|
1374
|
+
f"Script '{script_name}' timed out after {timeout:.0f}s "
|
|
1375
|
+
"(it may have reached an interactive prompt)"
|
|
1376
|
+
)
|
|
1377
|
+
return ScriptResult(
|
|
1378
|
+
proc.returncode if proc.returncode is not None else -1,
|
|
1379
|
+
stdout.decode(errors="replace").strip(),
|
|
1380
|
+
stderr.decode(errors="replace").strip(),
|
|
1381
|
+
)
|
|
1233
1382
|
except Exception as e:
|
|
1234
|
-
return f"Error running script: {e}"
|
|
1383
|
+
return ScriptResult(-2, "", f"Error running script: {e}")
|
|
1384
|
+
finally:
|
|
1385
|
+
# Ensure process cleanup
|
|
1386
|
+
if proc is not None and proc.returncode is None:
|
|
1387
|
+
proc.kill()
|
|
1388
|
+
await proc.wait()
|
|
1235
1389
|
|
|
1236
1390
|
async def _get_current_voice(self) -> str:
|
|
1237
1391
|
"""Get the currently active voice"""
|
|
1238
1392
|
result = await self._run_script(self.VOICE_MANAGER_SCRIPT, ["get"])
|
|
1239
|
-
return result.strip() if result else "Unknown"
|
|
1393
|
+
return result.stdout.strip() if result.ok and result.stdout else "Unknown"
|
|
1240
1394
|
|
|
1241
1395
|
async def _get_personality(self) -> str:
|
|
1242
1396
|
"""Get the current personality setting"""
|
|
@@ -1257,7 +1411,7 @@ class AgentVibesServer:
|
|
|
1257
1411
|
async def _get_language(self) -> str:
|
|
1258
1412
|
"""Get the current language setting"""
|
|
1259
1413
|
result = await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["code"])
|
|
1260
|
-
return result.strip() if result else "english"
|
|
1414
|
+
return result.stdout.strip() if result.ok and result.stdout else "english"
|
|
1261
1415
|
|
|
1262
1416
|
async def _get_provider(self) -> str:
|
|
1263
1417
|
"""Get the active TTS provider"""
|
|
@@ -1477,13 +1631,24 @@ Examples:
|
|
|
1477
1631
|
),
|
|
1478
1632
|
Tool(
|
|
1479
1633
|
name="download_extra_voices",
|
|
1480
|
-
description=
|
|
1634
|
+
description=(
|
|
1635
|
+
"Download extra high-quality custom Piper voices from HuggingFace. "
|
|
1636
|
+
"Includes: Kristin (US female), Jenny (UK female with Irish accent), "
|
|
1637
|
+
"and Tracy/16Speakers (multi-speaker). Perfect for adding variety to "
|
|
1638
|
+
"your TTS voices. This tool never proceeds without explicit consent: "
|
|
1639
|
+
"call it with auto_yes=True to actually start the download, or it "
|
|
1640
|
+
"returns a 'confirmation required' message and does nothing."
|
|
1641
|
+
),
|
|
1481
1642
|
inputSchema={
|
|
1482
1643
|
"type": "object",
|
|
1483
1644
|
"properties": {
|
|
1484
1645
|
"auto_yes": {
|
|
1485
1646
|
"type": "boolean",
|
|
1486
|
-
"description":
|
|
1647
|
+
"description": (
|
|
1648
|
+
"Must be True to download. False (the default) returns a "
|
|
1649
|
+
"confirmation-required message without downloading anything — "
|
|
1650
|
+
"this tool cannot answer an interactive Y/n prompt."
|
|
1651
|
+
),
|
|
1487
1652
|
"default": False
|
|
1488
1653
|
}
|
|
1489
1654
|
},
|
|
@@ -1574,7 +1739,7 @@ Fuzzy matching examples:
|
|
|
1574
1739
|
),
|
|
1575
1740
|
Tool(
|
|
1576
1741
|
name="enable_background_music",
|
|
1577
|
-
description="Enable or disable background music globally. When enabled, TTS audio will be mixed with background music at configured volume (default
|
|
1742
|
+
description="Enable or disable background music globally. When enabled, TTS audio will be mixed with background music at configured volume (default 20%).",
|
|
1578
1743
|
inputSchema={
|
|
1579
1744
|
"type": "object",
|
|
1580
1745
|
"properties": {
|
|
@@ -1594,7 +1759,7 @@ Fuzzy matching examples:
|
|
|
1594
1759
|
"properties": {
|
|
1595
1760
|
"volume": {
|
|
1596
1761
|
"type": "number",
|
|
1597
|
-
"description": "Volume level (0.0 = silent, 0.
|
|
1762
|
+
"description": "Volume level (0.0 = silent, 0.20 = default, 1.0 = full volume)",
|
|
1598
1763
|
"minimum": 0.0,
|
|
1599
1764
|
"maximum": 1.0,
|
|
1600
1765
|
}
|
|
@@ -1701,8 +1866,15 @@ Examples:
|
|
|
1701
1866
|
|
|
1702
1867
|
|
|
1703
1868
|
@app.call_tool()
|
|
1704
|
-
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
1705
|
-
"""Handle tool calls
|
|
1869
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent] | CallToolResult:
|
|
1870
|
+
"""Handle tool calls.
|
|
1871
|
+
|
|
1872
|
+
Every AgentVibesServer method below returns a string that starts with
|
|
1873
|
+
"❌" on failure (a marker produced by our own code from the script's
|
|
1874
|
+
*exit code*, not sniffed from the child script's stdout — see H1/#1 in
|
|
1875
|
+
story 8.2). We use that marker here, once, to set MCP's `isError` so
|
|
1876
|
+
clients can distinguish failure without parsing text.
|
|
1877
|
+
"""
|
|
1706
1878
|
try:
|
|
1707
1879
|
if name == "text_to_speech":
|
|
1708
1880
|
result = await agent_vibes.text_to_speech(
|
|
@@ -1785,12 +1957,21 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
|
1785
1957
|
voice=arguments.get("voice"),
|
|
1786
1958
|
)
|
|
1787
1959
|
else:
|
|
1788
|
-
|
|
1960
|
+
return CallToolResult(
|
|
1961
|
+
content=[TextContent(type="text", text=f"Unknown tool: {name}")],
|
|
1962
|
+
isError=True,
|
|
1963
|
+
)
|
|
1789
1964
|
|
|
1790
|
-
|
|
1965
|
+
content = [TextContent(type="text", text=result)]
|
|
1966
|
+
if isinstance(result, str) and result.startswith("❌"):
|
|
1967
|
+
return CallToolResult(content=content, isError=True)
|
|
1968
|
+
return content
|
|
1791
1969
|
|
|
1792
1970
|
except Exception as e:
|
|
1793
|
-
return
|
|
1971
|
+
return CallToolResult(
|
|
1972
|
+
content=[TextContent(type="text", text=f"Error: {str(e)}")],
|
|
1973
|
+
isError=True,
|
|
1974
|
+
)
|
|
1794
1975
|
|
|
1795
1976
|
|
|
1796
1977
|
async def main():
|