agentvibes 5.12.0-alpha.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.agentvibes/config.json +15 -1
  2. package/.agentvibes/install-manifest.json +342 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/commands/agent-vibes/commands.json +0 -20
  5. package/.claude/commands/agent-vibes/unmute.md +6 -2
  6. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  7. package/.claude/commands/agent-vibes-rdp.md +24 -24
  8. package/.claude/config/audio-effects.cfg +6 -6
  9. package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
  10. package/.claude/config/background-music-enabled.txt +1 -1
  11. package/.claude/config/background-music-position.txt +27 -0
  12. package/.claude/config/background-music-volume.txt +1 -0
  13. package/.claude/config/background-music.cfg +1 -0
  14. package/.claude/config/background-music.txt +1 -0
  15. package/.claude/config/language.txt +1 -0
  16. package/.claude/config/personality.txt +1 -0
  17. package/.claude/config/tts-speech-rate.txt +1 -0
  18. package/.claude/config/tts-verbosity.txt +1 -0
  19. package/.claude/docs/TERMUX_SETUP.md +408 -408
  20. package/.claude/github-star-reminder.txt +1 -1
  21. package/.claude/hooks/agentvibes-session-id.sh +69 -0
  22. package/.claude/hooks/bmad-party-speak.sh +20 -4
  23. package/.claude/hooks/bmad-speak.sh +60 -2
  24. package/.claude/hooks/bmad-tts-injector.sh +20 -1
  25. package/.claude/hooks/bmad-voice-manager.sh +25 -3
  26. package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
  27. package/.claude/hooks/clawdbot-receiver.sh +19 -1
  28. package/.claude/hooks/elevenlabs-voices.sh +62 -0
  29. package/.claude/hooks/kokoro-installer.sh +20 -10
  30. package/.claude/hooks/language-manager.sh +10 -3
  31. package/.claude/hooks/party-set-room.sh +71 -0
  32. package/.claude/hooks/party-stage-roster.py +328 -0
  33. package/.claude/hooks/personality-manager.sh +19 -2
  34. package/.claude/hooks/piper-voice-manager.sh +3 -2
  35. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
  36. package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
  37. package/.claude/hooks/play-tts-kokoro.sh +33 -6
  38. package/.claude/hooks/play-tts-soprano.sh +3 -2
  39. package/.claude/hooks/play-tts-ssh-remote.sh +72 -27
  40. package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
  41. package/.claude/hooks/play-tts.sh +86 -69
  42. package/.claude/hooks/provider-catalog.json +352 -0
  43. package/.claude/hooks/provider-catalog.sh +161 -0
  44. package/.claude/hooks/provider-commands.sh +2 -1
  45. package/.claude/hooks/provider-manager.sh +47 -9
  46. package/.claude/hooks/python-resolver.sh +117 -0
  47. package/.claude/hooks/session-id.sh +56 -0
  48. package/.claude/hooks/session-start-tts.sh +39 -0
  49. package/.claude/hooks/speed-manager.sh +1 -1
  50. package/.claude/hooks/translate-manager.sh +3 -2
  51. package/.claude/hooks/translator.py +1 -1
  52. package/.claude/hooks/voice-manager.sh +242 -10
  53. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  54. package/.claude/hooks-windows/language-manager.ps1 +7 -1
  55. package/.claude/hooks-windows/personality-manager.ps1 +16 -1
  56. package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
  57. package/.claude/hooks-windows/play-tts.ps1 +32 -3
  58. package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
  59. package/.claude/hooks-windows/provider-manager.ps1 +63 -8
  60. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  61. package/.claude/hooks-windows/tts-watcher.ps1 +84 -8
  62. package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
  63. package/.claude/piper-voices-dir.txt +1 -0
  64. package/.clawdbot/README.md +105 -105
  65. package/.mcp.json +15 -0
  66. package/LICENSE +190 -190
  67. package/README.md +20 -6
  68. package/RELEASE_NOTES.md +81 -0
  69. package/WINDOWS-SETUP.md +208 -208
  70. package/bin/agent-vibes +39 -39
  71. package/bin/mcp-server.js +121 -121
  72. package/bin/test-bmad-pr +78 -78
  73. package/mcp-server/QUICK_START.md +203 -203
  74. package/mcp-server/README.md +345 -345
  75. package/mcp-server/examples/claude_desktop_config.json +11 -11
  76. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  77. package/mcp-server/examples/custom_instructions.md +169 -169
  78. package/mcp-server/pyproject.toml +52 -52
  79. package/mcp-server/requirements.txt +2 -2
  80. package/mcp-server/server.py +146 -49
  81. package/mcp-server/test_mcp_correctness.py +20 -2
  82. package/mcp-server/test_server.py +395 -395
  83. package/mcp-server/test_windows_script_parity.py +0 -2
  84. package/package.json +1 -1
  85. package/src/cli/list-personalities.js +110 -110
  86. package/src/cli/list-voices.js +115 -11
  87. package/src/commands/bmad-voices.js +394 -394
  88. package/src/console/app.js +1 -9
  89. package/src/console/bling.js +71 -0
  90. package/src/console/brand-colors.js +13 -13
  91. package/src/console/constants/personalities.js +44 -44
  92. package/src/console/footer-config.js +0 -4
  93. package/src/console/music-preview.js +79 -0
  94. package/src/console/navigation.js +0 -1
  95. package/src/console/tabs/agents-tab.js +14 -2
  96. package/src/console/tabs/help-tab.js +314 -314
  97. package/src/console/tabs/music-tab.js +81 -5
  98. package/src/console/tabs/placeholder-tab.js +0 -2
  99. package/src/console/tabs/readme-tab.js +272 -272
  100. package/src/console/tabs/settings-tab.js +207 -14
  101. package/src/console/tabs/setup-tab.js +39 -36
  102. package/src/console/tabs/voices-tab.js +102 -23
  103. package/src/console/widgets/destroy-list.js +25 -25
  104. package/src/console/widgets/notice.js +55 -55
  105. package/src/console/widgets/track-picker.js +82 -0
  106. package/src/installer/language-screen.js +31 -31
  107. package/src/installer/music-file-input.js +304 -304
  108. package/src/installer.js +124 -10
  109. package/src/services/language-service.js +47 -47
  110. package/src/services/navigation-service.js +1 -1
  111. package/src/services/provider-catalog.js +412 -0
  112. package/src/services/provider-voice-catalog.js +52 -73
  113. package/src/services/tts-engine-service.js +29 -0
  114. package/src/utils/audio-format-validator.js +277 -277
  115. package/src/utils/dependency-checker.js +469 -469
  116. package/src/utils/file-ownership-verifier.js +358 -358
  117. package/src/utils/music-file-validator.js +285 -285
  118. package/src/utils/preview-list-prompt.js +144 -144
  119. package/src/utils/provider-validator.js +62 -12
  120. package/src/utils/secure-music-storage.js +412 -412
  121. package/voice-assignments.json +8244 -8244
  122. package/.claude/commands/agent-vibes/language.md +0 -23
  123. package/.claude/commands/agent-vibes/learn.md +0 -67
  124. package/.claude/commands/agent-vibes/replay-target.md +0 -14
  125. package/.claude/commands/agent-vibes/target-voice.md +0 -26
  126. package/.claude/commands/agent-vibes/target.md +0 -30
  127. package/.claude/hooks/learn-manager.sh +0 -492
  128. package/.claude/hooks/replay-target-audio.sh +0 -95
  129. package/.claude/hooks-windows/learn-manager.ps1 +0 -241
@@ -46,6 +46,7 @@ import os
46
46
  import platform
47
47
  import re as _re
48
48
  import subprocess
49
+ import sys
49
50
  from dataclasses import dataclass
50
51
  from pathlib import Path
51
52
  from typing import Optional
@@ -61,6 +62,53 @@ import mcp.server.stdio
61
62
  DEFAULT_SCRIPT_TIMEOUT = 30.0
62
63
 
63
64
 
65
+ # ── Provider Catalog (SSOT Layer 2) — platform allowlists + display names ────
66
+ #
67
+ # server.py DERIVES its per-platform provider allowlists and provider display
68
+ # names from the shipped provider-catalog.json (generated from
69
+ # src/services/provider-catalog.js). The literals below are an EMBEDDED FALLBACK
70
+ # used ONLY when that JSON is missing or unreadable (installed-tree skew) —
71
+ # degraded, never dead: the MCP server MUST start without the file.
72
+ #
73
+ # Bidirectional parity (embedded fallback ≡ catalog, per platform, BOTH
74
+ # directions) is asserted by test/unit/provider-catalog-conformance.test.js so
75
+ # the fallback can never silently drift from the catalog (design §8 — a
76
+ # one-directional check is the exact gap that let elevenlabs-on-Windows through
77
+ # the old positive-only test).
78
+ #
79
+ # The non-Windows fallback mirrors the catalog's DARWIN set (the superset that
80
+ # also covers Linux: darwin adds macOS `say`, which a Linux box simply won't
81
+ # have installed — availability is enforced by the dispatchers downstream, not
82
+ # here). Windows uses the catalog WINDOWS set. elevenlabs is deliberately ABSENT
83
+ # from Windows: there is NO play-tts-elevenlabs.ps1 runtime (AVI-S9.1 /
84
+ # provider-catalog.js: elevenlabs.runtime.windows === null).
85
+ _FALLBACK_PROVIDERS_WINDOWS = ["windows-piper", "windows-sapi", "soprano", "kokoro"]
86
+ _FALLBACK_PROVIDERS_NON_WINDOWS = ["piper", "macos", "soprano", "kokoro", "elevenlabs"]
87
+
88
+ # termux-ssh is a TRANSPORT (relays TTS over SSH to a phone), NOT a synthesis
89
+ # provider: it has no catalog record and no play-tts-termux-ssh runtime. It is
90
+ # accepted on non-Windows as a documented non-catalog transport token, and is
91
+ # EXCLUDED BY NAME from the catalog parity assertion (AC6) so it can neither be
92
+ # silently dropped nor silently drift INTO the catalog.
93
+ _TRANSPORT_TOKENS = ["termux-ssh"]
94
+
95
+ # Embedded fallback display names — byte-equal to catalog.json `displayNames`
96
+ # (group-8 parity). termux-ssh is NOT here (it has no catalog record); its
97
+ # display name is added separately as a transport token.
98
+ _FALLBACK_DISPLAY_NAMES = {
99
+ "soprano": "Soprano TTS",
100
+ "piper": "Piper TTS",
101
+ "kokoro": "Kokoro TTS",
102
+ "elevenlabs": "ElevenLabs",
103
+ "macos": "macOS Say",
104
+ "windows-sapi": "Windows SAPI",
105
+ "windows-piper": "Piper TTS",
106
+ }
107
+ _TRANSPORT_DISPLAY_NAMES = {
108
+ "termux-ssh": "Termux SSH",
109
+ }
110
+
111
+
64
112
  @dataclass
65
113
  class ScriptResult:
66
114
  """Structured result of running a hook script.
@@ -127,6 +175,91 @@ class AgentVibesServer:
127
175
  # the same file at the same time is not covered; see story 8.2 notes).
128
176
  self._override_lock = asyncio.Lock()
129
177
 
178
+ # provider-catalog.json is read lazily once and cached (design: no
179
+ # blocking I/O per call; load at first use, not per set_provider).
180
+ self._provider_catalog = None
181
+ self._provider_catalog_loaded = False
182
+
183
+ def _load_provider_catalog(self) -> Optional[dict]:
184
+ """Load the shipped provider-catalog.json ONCE (cached).
185
+
186
+ Generated from src/services/provider-catalog.js into
187
+ .claude/hooks/provider-catalog.json and shipped beside the hooks, it is
188
+ the SSOT for per-platform provider allowlists and display names. Returns
189
+ the parsed dict, or None on ANY failure (missing / unreadable /
190
+ malformed) so the MCP server always starts — callers then fall back to
191
+ the embedded literals that conformance asserts are equivalent.
192
+ """
193
+ if self._provider_catalog_loaded:
194
+ return self._provider_catalog
195
+ self._provider_catalog_loaded = True
196
+ catalog = None
197
+ try:
198
+ # catalog.json lives under hooks/ on EVERY platform (program data,
199
+ # not a per-platform hook script — the generator only writes it there).
200
+ path = self.claude_dir / "hooks" / "provider-catalog.json"
201
+ if path.exists() and not path.is_symlink():
202
+ parsed = json.loads(path.read_text(encoding="utf-8"))
203
+ if isinstance(parsed, dict):
204
+ catalog = parsed
205
+ except (OSError, ValueError):
206
+ catalog = None
207
+ if catalog is None:
208
+ # Degraded, never dead. Warn on stderr ONLY (stdout is the MCP
209
+ # JSON-RPC stream); one terse line, since a missing file is the
210
+ # common installed-tree-skew case.
211
+ print(
212
+ "agentvibes: provider-catalog.json unavailable; using embedded provider fallback",
213
+ file=sys.stderr,
214
+ )
215
+ self._provider_catalog = catalog
216
+ return catalog
217
+
218
+ def _valid_providers(self) -> list:
219
+ """Per-platform provider allowlist, DERIVED from provider-catalog.json.
220
+
221
+ Windows → catalog `platforms.windows`; non-Windows → catalog
222
+ `platforms.darwin` (the superset covering both Linux and macOS). Falls
223
+ back to the embedded literals when the catalog is unavailable. Transport
224
+ tokens (termux-ssh) are appended on non-Windows only.
225
+ """
226
+ catalog = self._load_provider_catalog()
227
+ base = None
228
+ if catalog:
229
+ platforms = catalog.get("platforms")
230
+ if isinstance(platforms, dict):
231
+ key = "windows" if self.is_windows else "darwin"
232
+ derived = platforms.get(key)
233
+ if isinstance(derived, list) and derived:
234
+ base = list(derived)
235
+ if base is None:
236
+ base = list(
237
+ _FALLBACK_PROVIDERS_WINDOWS if self.is_windows else _FALLBACK_PROVIDERS_NON_WINDOWS
238
+ )
239
+ if not self.is_windows:
240
+ for token in _TRANSPORT_TOKENS:
241
+ if token not in base:
242
+ base.append(token)
243
+ return base
244
+
245
+ def _provider_display_names(self) -> dict:
246
+ """Provider display names, DERIVED from provider-catalog.json.
247
+
248
+ Falls back to the embedded dict when the catalog is unavailable. Non-
249
+ catalog transport tokens (termux-ssh) are always merged in.
250
+ """
251
+ catalog = self._load_provider_catalog()
252
+ names = None
253
+ if catalog:
254
+ derived = catalog.get("displayNames")
255
+ if isinstance(derived, dict) and derived:
256
+ names = dict(derived)
257
+ if names is None:
258
+ names = dict(_FALLBACK_DISPLAY_NAMES)
259
+ for token, label in _TRANSPORT_DISPLAY_NAMES.items():
260
+ names.setdefault(token, label)
261
+ return names
262
+
130
263
  def _find_claude_dir(self) -> Path:
131
264
  """Find the .claude directory relative to this script"""
132
265
  # Get the AgentVibes root directory (parent of mcp-server)
@@ -728,33 +861,31 @@ class AgentVibesServer:
728
861
 
729
862
  async def set_provider(self, provider: str) -> str:
730
863
  """
731
- Switch TTS provider between Piper, macOS, and Termux SSH.
864
+ Switch TTS provider between the supported synthesis engines.
732
865
 
733
866
  Args:
734
- provider: Provider name ("piper", "macos", or "termux-ssh")
867
+ provider: Provider name. Non-Windows: "piper", "macos", "termux-ssh",
868
+ "soprano", "kokoro", "elevenlabs". Windows: "windows-piper",
869
+ "windows-sapi", "soprano", "kokoro".
735
870
 
736
871
  Returns:
737
872
  Success or error message
738
873
  """
739
874
  provider = provider.lower()
740
- if self.is_windows:
741
- valid_providers = ["windows-piper", "windows-sapi", "soprano"]
742
- else:
743
- valid_providers = ["piper", "macos", "termux-ssh", "soprano"]
875
+ # Platform allowlist + display names DERIVE from provider-catalog.json
876
+ # (SSOT), with embedded fallbacks (module constants above). kokoro is
877
+ # cross-platform; elevenlabs is Unix-only (NO play-tts-elevenlabs.ps1, so
878
+ # it is absent from the Windows set — switching to it on Windows would be
879
+ # silently unplayable). See AVI-S9.1 / AVI-S9.5 and provider-catalog.js
880
+ # (elevenlabs.runtime.windows === null).
881
+ valid_providers = self._valid_providers()
744
882
  if provider not in valid_providers:
745
883
  return f"❌ Invalid provider: {provider}. Choose from: {', '.join(valid_providers)}"
746
884
 
747
885
  result = await self._run_script("provider-manager.sh", ["switch", provider])
748
886
  if result.ok:
749
- # Automatically speak confirmation in the new provider's voice
750
- provider_names = {
751
- "macos": "macOS",
752
- "termux-ssh": "Termux SSH",
753
- "piper": "Piper",
754
- "windows-piper": "Windows Piper",
755
- "windows-sapi": "Windows SAPI",
756
- "soprano": "Soprano",
757
- }
887
+ # Automatically speak confirmation in the new provider's voice.
888
+ provider_names = self._provider_display_names()
758
889
  provider_name = provider_names.get(provider, provider.title())
759
890
  confirmation_text = f"Successfully switched to {provider_name} provider"
760
891
 
@@ -775,24 +906,6 @@ class AgentVibesServer:
775
906
 
776
907
  return f"❌ Failed to switch provider: {result.error_detail}"
777
908
 
778
- async def set_learn_mode(self, enabled: bool) -> str:
779
- """
780
- Enable or disable language learning mode.
781
-
782
- When enabled, TTS speaks in both your main language and target language.
783
-
784
- Args:
785
- enabled: True to enable, False to disable
786
-
787
- Returns:
788
- Success or error message
789
- """
790
- action = "enable" if enabled else "disable"
791
- result = await self._run_script("learn-manager.sh", [action])
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}"
795
-
796
909
  async def set_speed(self, speed: str, target: bool = False) -> str:
797
910
  """
798
911
  Set speech speed for main or target voice.
@@ -1591,20 +1704,6 @@ Examples:
1591
1704
  "required": ["provider"],
1592
1705
  },
1593
1706
  ),
1594
- Tool(
1595
- name="set_learn_mode",
1596
- description="Enable or disable language learning mode. When ON, TTS speaks in both your main language and target language for bilingual learning.",
1597
- inputSchema={
1598
- "type": "object",
1599
- "properties": {
1600
- "enabled": {
1601
- "type": "boolean",
1602
- "description": "True to enable learning mode, False to disable"
1603
- }
1604
- },
1605
- "required": ["enabled"],
1606
- },
1607
- ),
1608
1707
  Tool(
1609
1708
  name="set_speed",
1610
1709
  description="Set speech speed for main or target voice. Works with both Piper and macOS providers. Use this to make voices faster or slower.",
@@ -1900,8 +1999,6 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent] | CallToolR
1900
1999
  result = await agent_vibes.replay_audio(n)
1901
2000
  elif name == "set_provider":
1902
2001
  result = await agent_vibes.set_provider(arguments["provider"])
1903
- elif name == "set_learn_mode":
1904
- result = await agent_vibes.set_learn_mode(arguments["enabled"])
1905
2002
  elif name == "set_speed":
1906
2003
  target = arguments.get("target", False)
1907
2004
  result = await agent_vibes.set_speed(arguments["speed"], target)
@@ -122,13 +122,12 @@ def test_set_personality_fails_on_nonzero_exit_despite_matching_marker_text():
122
122
  "method_name,call_args,plain_stdout",
123
123
  [
124
124
  ("set_language", ("spanish",), "Language set to: spanish"),
125
- ("set_learn_mode", (True,), "Learn mode enabled"),
126
125
  ("set_verbosity", ("high",), "Verbosity set to: high (project-local)"),
127
126
  ("replay_audio", (1,), "Replaying audio #1: tts-foo.wav"),
128
127
  ],
129
128
  )
130
129
  def test_windows_style_plain_text_success_is_not_treated_as_failure(method_name, call_args, plain_stdout):
131
- """set_language / set_learn_mode / set_verbosity / replay_audio must all
130
+ """set_language / set_verbosity / replay_audio must all
132
131
  report success from returncode==0 alone, even when stdout carries none of
133
132
  the emoji markers the old code sniffed for (✓ / ✅ / 🔊)."""
134
133
  server = AgentVibesServer()
@@ -482,5 +481,24 @@ def test_tool_descriptions_state_20_percent_default_not_30():
482
481
  assert "0.30" not in volume_desc
483
482
 
484
483
 
484
+ def test_valid_providers_derives_from_catalog_and_excludes_elevenlabs_on_windows():
485
+ """AVI-S9.5: _valid_providers() derives the per-platform allowlist from
486
+ provider-catalog.json (or the embedded fallback ≡ catalog). ElevenLabs has
487
+ no Windows runtime (no play-tts-elevenlabs.ps1), so it must be ABSENT from
488
+ the Windows allowlist and PRESENT on non-Windows — the exact bug class the
489
+ Provider Catalog closes."""
490
+ server = AgentVibesServer()
491
+
492
+ server.is_windows = True
493
+ win = server._valid_providers()
494
+ assert "elevenlabs" not in win, f"elevenlabs must not be a Windows provider: {win}"
495
+ assert "kokoro" in win and "soprano" in win
496
+
497
+ server.is_windows = False
498
+ non_win = server._valid_providers()
499
+ assert "elevenlabs" in non_win, f"elevenlabs must be a non-Windows provider: {non_win}"
500
+ assert "kokoro" in non_win and "soprano" in non_win
501
+
502
+
485
503
  if __name__ == "__main__":
486
504
  sys.exit(pytest.main([__file__, "-v"]))