agentvibes 5.12.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 (72) hide show
  1. package/.claude/commands/agent-vibes/commands.json +0 -20
  2. package/.claude/commands/agent-vibes/unmute.md +6 -2
  3. package/.claude/config/audio-effects.cfg +6 -6
  4. package/.claude/github-star-reminder.txt +1 -1
  5. package/.claude/hooks/agentvibes-session-id.sh +69 -0
  6. package/.claude/hooks/bmad-party-speak.sh +20 -4
  7. package/.claude/hooks/bmad-speak.sh +60 -2
  8. package/.claude/hooks/bmad-tts-injector.sh +20 -1
  9. package/.claude/hooks/bmad-voice-manager.sh +25 -3
  10. package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
  11. package/.claude/hooks/clawdbot-receiver.sh +19 -1
  12. package/.claude/hooks/elevenlabs-voices.sh +62 -0
  13. package/.claude/hooks/kokoro-installer.sh +20 -10
  14. package/.claude/hooks/language-manager.sh +10 -3
  15. package/.claude/hooks/party-set-room.sh +71 -0
  16. package/.claude/hooks/party-stage-roster.py +328 -0
  17. package/.claude/hooks/personality-manager.sh +19 -2
  18. package/.claude/hooks/piper-voice-manager.sh +3 -2
  19. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
  20. package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
  21. package/.claude/hooks/play-tts-kokoro.sh +33 -6
  22. package/.claude/hooks/play-tts-soprano.sh +3 -2
  23. package/.claude/hooks/play-tts-ssh-remote.sh +37 -29
  24. package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
  25. package/.claude/hooks/play-tts.sh +66 -61
  26. package/.claude/hooks/provider-catalog.json +352 -0
  27. package/.claude/hooks/provider-catalog.sh +161 -0
  28. package/.claude/hooks/provider-commands.sh +2 -1
  29. package/.claude/hooks/provider-manager.sh +47 -9
  30. package/.claude/hooks/python-resolver.sh +117 -0
  31. package/.claude/hooks/session-id.sh +56 -0
  32. package/.claude/hooks/session-start-tts.sh +39 -0
  33. package/.claude/hooks/speed-manager.sh +1 -1
  34. package/.claude/hooks/translate-manager.sh +3 -2
  35. package/.claude/hooks/translator.py +1 -1
  36. package/.claude/hooks/voice-manager.sh +242 -10
  37. package/.claude/hooks-windows/language-manager.ps1 +7 -1
  38. package/.claude/hooks-windows/personality-manager.ps1 +16 -1
  39. package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
  40. package/.claude/hooks-windows/play-tts.ps1 +32 -3
  41. package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
  42. package/.claude/hooks-windows/provider-manager.ps1 +63 -8
  43. package/.claude/hooks-windows/tts-watcher.ps1 +33 -12
  44. package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
  45. package/.mcp.json +0 -7
  46. package/README.md +12 -3
  47. package/RELEASE_NOTES.md +43 -0
  48. package/mcp-server/server.py +146 -49
  49. package/mcp-server/test_mcp_correctness.py +20 -2
  50. package/mcp-server/test_windows_script_parity.py +0 -2
  51. package/package.json +1 -1
  52. package/src/cli/list-voices.js +218 -114
  53. package/src/console/bling.js +71 -0
  54. package/src/console/music-preview.js +79 -0
  55. package/src/console/tabs/music-tab.js +16 -39
  56. package/src/console/tabs/settings-tab.js +195 -13
  57. package/src/console/tabs/setup-tab.js +9 -34
  58. package/src/console/tabs/voices-tab.js +83 -14
  59. package/src/console/widgets/track-picker.js +82 -0
  60. package/src/installer.js +124 -10
  61. package/src/services/provider-catalog.js +412 -0
  62. package/src/services/provider-voice-catalog.js +52 -73
  63. package/src/services/tts-engine-service.js +29 -0
  64. package/src/utils/provider-validator.js +62 -12
  65. package/.claude/commands/agent-vibes/language.md +0 -23
  66. package/.claude/commands/agent-vibes/learn.md +0 -67
  67. package/.claude/commands/agent-vibes/replay-target.md +0 -14
  68. package/.claude/commands/agent-vibes/target-voice.md +0 -26
  69. package/.claude/commands/agent-vibes/target.md +0 -30
  70. package/.claude/hooks/learn-manager.sh +0 -492
  71. package/.claude/hooks/replay-target-audio.sh +0 -95
  72. package/.claude/hooks-windows/learn-manager.ps1 +0 -241
@@ -15,7 +15,25 @@ param(
15
15
 
16
16
  $ClaudeDir = "$env:USERPROFILE\.claude"
17
17
  $ProviderFile = "$ClaudeDir\tts-provider.txt"
18
- $ValidProviders = @('windows-piper', 'windows-sapi', 'soprano')
18
+ # $ValidProviders DERIVES from the generated Provider Catalog (SSOT): the Windows
19
+ # platform set (kokoro is cross-platform — play-tts-kokoro.ps1 exists; elevenlabs
20
+ # is NOT playable on Windows and is therefore absent). Dot-source the generated
21
+ # provider-catalog.ps1 and pull `windows`. FAIL-SAFE: the literal below is the
22
+ # legacy fallback used when the catalog artifact is missing (installed-tree skew)
23
+ # — mirrors AVI-S9.3's probe/fallback pattern. See AVI-S8.1 / AVI-S9.5.
24
+ $ValidProviders = @('windows-piper', 'windows-sapi', 'soprano', 'kokoro')
25
+ $__CatalogPs1 = Join-Path $PSScriptRoot 'provider-catalog.ps1'
26
+ if (Test-Path $__CatalogPs1) {
27
+ try {
28
+ . $__CatalogPs1
29
+ if (Get-Command Get-CatalogProvidersForPlatform -ErrorAction SilentlyContinue) {
30
+ $__WinSet = @(Get-CatalogProvidersForPlatform 'windows')
31
+ if ($__WinSet.Count -gt 0) { $ValidProviders = $__WinSet }
32
+ }
33
+ } catch {
34
+ # Keep the legacy fallback literal on any catalog load error.
35
+ }
36
+ }
19
37
 
20
38
  # Ensure claude directory exists
21
39
  if (-not (Test-Path $ClaudeDir)) {
@@ -35,6 +53,14 @@ function Get-ActiveProvider {
35
53
  function Get-AvailableProviders {
36
54
  $available = @()
37
55
 
56
+ # Fast path: skip the live install probes (soprano HTTP + kokoro python
57
+ # spawns). Those probes are what make `provider list` occasionally stall for
58
+ # tens of seconds under load; when the caller only needs the provider NAMES
59
+ # (e.g. the parallel test harness, or an offline listing) set
60
+ # AGENTVIBES_PROVIDER_LIST_NO_PROBE=1 to report installed=$false without
61
+ # touching the network or spawning interpreters. Default behavior unchanged.
62
+ $noProbe = ($env:AGENTVIBES_PROVIDER_LIST_NO_PROBE -eq '1')
63
+
38
64
  # Always available
39
65
  $available += @{
40
66
  name = "windows-sapi"
@@ -59,15 +85,18 @@ function Get-AvailableProviders {
59
85
  $null = Get-Command soprano -ErrorAction Stop
60
86
  $sopranoInstalled = $true
61
87
  } catch {
62
- # Also check if a Soprano server is running (try both API paths)
63
- try {
64
- $null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/gradio_api/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
65
- $sopranoInstalled = $true
66
- } catch {
88
+ # Also check if a Soprano server is running (try both API paths) — unless
89
+ # the fast path is requested, in which case skip the network probes.
90
+ if (-not $noProbe) {
67
91
  try {
68
- $null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
92
+ $null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/gradio_api/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
69
93
  $sopranoInstalled = $true
70
- } catch {}
94
+ } catch {
95
+ try {
96
+ $null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
97
+ $sopranoInstalled = $true
98
+ } catch {}
99
+ }
71
100
  }
72
101
  }
73
102
 
@@ -77,6 +106,32 @@ function Get-AvailableProviders {
77
106
  installed = $sopranoInstalled
78
107
  }
79
108
 
109
+ # Check if Kokoro is installed. Mirror validateKokoroInstallation() in
110
+ # src/utils/provider-validator.js, but use importlib.util.find_spec so we do
111
+ # NOT actually import kokoro — importing the 82M-param neural lib takes
112
+ # several seconds and would make `provider list` hang (AVI-S8.1). find_spec
113
+ # only confirms the packages are importable, which is all we need here.
114
+ $kokoroInstalled = $false
115
+ $kokoroProbe = "import importlib.util as u,sys; sys.exit(0 if all(u.find_spec(m) for m in ('kokoro','soundfile','numpy')) else 1)"
116
+ foreach ($py in ($(if ($noProbe) { @() } else { @('py', 'python', 'python3') }))) {
117
+ if (-not (Get-Command $py -ErrorAction SilentlyContinue)) { continue }
118
+ try {
119
+ $null = & $py -c $kokoroProbe 2>$null
120
+ # Only stop once an interpreter actually HAS the packages — mirrors
121
+ # validateKokoroInstallation(), which tries every interpreter (the
122
+ # packages may live under python/python3, not the `py` launcher).
123
+ if ($LASTEXITCODE -eq 0) { $kokoroInstalled = $true; break }
124
+ } catch {
125
+ # interpreter present but failed to run the probe — try next
126
+ }
127
+ }
128
+
129
+ $available += @{
130
+ name = "kokoro"
131
+ description = "Kokoro TTS (Neural, pip install kokoro-onnx soundfile numpy)"
132
+ installed = $kokoroInstalled
133
+ }
134
+
80
135
  return $available
81
136
  }
82
137
 
@@ -52,7 +52,7 @@ try {
52
52
  }
53
53
  if ($req.kind -eq 'music') {
54
54
  $trackName = [string]$req.music
55
- if ($trackName -match '^[A-Za-z0-9._][A-Za-z0-9._\-]*\.mp3$') {
55
+ if ($trackName -match '^[A-Za-z0-9._][A-Za-z0-9._ \-]*\.mp3$') {
56
56
  $tracksDir = Join-Path $env:USERPROFILE '.claude\audio\tracks'
57
57
  $full = [System.IO.Path]::GetFullPath((Join-Path $tracksDir $trackName))
58
58
  $baseFull = [System.IO.Path]::GetFullPath($tracksDir)
@@ -61,6 +61,17 @@ try {
61
61
  $ffplay = Get-Command ffplay -ErrorAction SilentlyContinue
62
62
  if ($ffplay) {
63
63
  Write-WatcherLog "INFO" "music-preview id=$($req.id) track=$trackName"
64
+ # Readiness bling — a short cue played in THIS (audio-capable) watcher
65
+ # session BEFORE the track so a listener knows music is incoming. Launched
66
+ # async (NOT -Wait — the wav's shimmer tail is long); a fixed 0.3s gap then
67
+ # lets the music follow tightly. Best-effort: a missing wav / player error
68
+ # never blocks the track.
69
+ $blingWav = Join-Path $env:USERPROFILE '.claude\audio\ui\bling-success.wav'
70
+ if (-not (Test-Path $blingWav)) { $blingWav = Join-Path $env:USERPROFILE '.agentvibes\bling-success.wav' }
71
+ if (Test-Path $blingWav) {
72
+ try { Start-Process -FilePath $ffplay.Source -ArgumentList @('-autoexit','-nodisp','-loglevel','quiet',$blingWav) -WindowStyle Hidden | Out-Null } catch { }
73
+ Start-Sleep -Milliseconds 300
74
+ }
64
75
  $mp = Start-Process -FilePath $ffplay.Source -ArgumentList @('-autoexit','-nodisp','-loglevel','quiet','-volume','80',$full) -WindowStyle Hidden -PassThru
65
76
  if ($mp) { Set-Content -Path $pidFile -Value $mp.Id -NoNewline -ErrorAction SilentlyContinue }
66
77
  } else {
@@ -83,9 +94,9 @@ try {
83
94
  $safeVoice = if ($req.voice -and $req.voice -match '^[a-zA-Z0-9_\-\. :]+$') { $req.voice } else { "" }
84
95
  $env:CLAUDE_PROJECT_DIR = $env:USERPROFILE
85
96
  $env:AGENTVIBES_NO_PRETEXT = "1"
86
- # Carry the sender's project (folder name/path) through to the
87
- # TalkingHead forward so the avatar badge/tab shows the real remote
88
- # origin instead of this machine's own profile dir. (Grafted from master WIP.)
97
+ # Carry the sender's project (folder name/path) through so a
98
+ # downstream receiver shows the real remote origin instead of
99
+ # this machine's own profile dir.
89
100
  if ($req.project) { $env:AGENTVIBES_PROJECT = $req.project }
90
101
  else { [System.Environment]::SetEnvironmentVariable("AGENTVIBES_PROJECT", $null, "Process") }
91
102
  if ($req.projectPath) { $env:AGENTVIBES_PROJECT_PATH = $req.projectPath }
@@ -99,16 +110,26 @@ try {
99
110
  else { [System.Environment]::SetEnvironmentVariable("AGENTVIBES_OVERRIDE_EFFECTS", $null, "Process") }
100
111
 
101
112
  if (Test-Path $PlayTts) {
102
- # Play remote arrival prefix sound if configured
103
- $prefixSoundFile = "$env:USERPROFILE\.agentvibes\remote-prefix-sound.txt"
104
- if (Test-Path $prefixSoundFile) {
105
- $prefixSound = (Get-Content $prefixSoundFile -Raw).Trim()
106
- if ($prefixSound -and (Test-Path $prefixSound)) {
107
- $ffplay = Get-Command ffplay -ErrorAction SilentlyContinue
108
- if ($ffplay) {
109
- & $ffplay.Source -autoexit -nodisp -loglevel quiet $prefixSound 2>$null
113
+ # Always play a crisp local "incoming" bling so the user KNOWS audio is
114
+ # on the way — even when audio is routed to a downstream receiver
115
+ # instead of the local speakers. Standard bling-success.wav, async
116
+ # with a 0.3s gap (same as the music-preview path); fall back to the
117
+ # user-configured remote-prefix-sound.txt if the bling wav is missing.
118
+ $ffplay = Get-Command ffplay -ErrorAction SilentlyContinue
119
+ if ($ffplay) {
120
+ $blingWav = Join-Path $env:USERPROFILE '.claude\audio\ui\bling-success.wav'
121
+ if (-not (Test-Path $blingWav)) { $blingWav = Join-Path $env:USERPROFILE '.agentvibes\bling-success.wav' }
122
+ if (-not (Test-Path $blingWav)) {
123
+ $prefixSoundFile = "$env:USERPROFILE\.agentvibes\remote-prefix-sound.txt"
124
+ if (Test-Path $prefixSoundFile) {
125
+ $ps = (Get-Content $prefixSoundFile -Raw).Trim()
126
+ if ($ps -and (Test-Path $ps)) { $blingWav = $ps }
110
127
  }
111
128
  }
129
+ if (Test-Path $blingWav) {
130
+ try { Start-Process -FilePath $ffplay.Source -ArgumentList @('-autoexit','-nodisp','-loglevel','quiet',$blingWav) -WindowStyle Hidden | Out-Null } catch { }
131
+ Start-Sleep -Milliseconds 300
132
+ }
112
133
  }
113
134
 
114
135
  $tempText = Join-Path $env:TEMP "agentvibes-tts-$($req.id).txt"
@@ -17,6 +17,14 @@ $ClaudeDir = "$env:USERPROFILE\.claude"
17
17
  $ProviderFile = "$ClaudeDir\tts-provider.txt"
18
18
  $VoiceSapiFile = "$ClaudeDir\tts-voice-sapi.txt"
19
19
  $VoicePiperFile = "$ClaudeDir\tts-voice-piper.txt"
20
+ $VoiceKokoroFile = "$ClaudeDir\tts-voice-kokoro.txt"
21
+
22
+ # Load the generated Provider Catalog (SSOT) for kokoro switch-time validation.
23
+ # FAIL-SAFE: consumers probe `Get-Command Test-CatalogVoice` and fall back to the
24
+ # legacy shape regex when the artifact is missing (installed-tree skew) — mirrors
25
+ # play-tts.sh's PLAN_OK legacy fallback. Switch never breaks on a missing catalog.
26
+ $CatalogPs1 = Join-Path $PSScriptRoot 'provider-catalog.ps1'
27
+ if (Test-Path $CatalogPs1) { . $CatalogPs1 }
20
28
 
21
29
  # Get active provider
22
30
  $ActiveProvider = "windows-sapi"
@@ -129,6 +137,47 @@ function Switch-Voice {
129
137
  Write-Host "[INFO] Soprano uses a single fixed voice (Soprano-1.1-80M)" -ForegroundColor Cyan
130
138
  return $true
131
139
  }
140
+ elseif ($ActiveProvider -eq "kokoro") {
141
+ # Kokoro switch-time validation via the generated Provider Catalog (SSOT):
142
+ # MEMBERSHIP + canonical case-fold, so a typo like af_hart is rejected HERE
143
+ # and never reaches tts-voice-kokoro.txt to die silently at the model.
144
+ # Escape hatch AGENTVIBES_ALLOW_UNLISTED_VOICE=1 bypasses MEMBERSHIP (not
145
+ # shape) so a voice from a newer kokoro model still switches. FAIL-SAFE:
146
+ # fall back to the legacy shape regex when the catalog artifact is missing.
147
+ $kokoroVoice = $NewVoice.ToLower()
148
+ $allowUnlisted = ($env:AGENTVIBES_ALLOW_UNLISTED_VOICE -eq '1')
149
+ if (Get-Command Test-CatalogVoice -ErrorAction SilentlyContinue) {
150
+ $canon = Test-CatalogVoice -Provider 'kokoro' -Voice $NewVoice
151
+ if ($canon) {
152
+ Set-Content -Path $VoiceKokoroFile -Value $canon
153
+ Write-Host "[OK] Voice set to: $canon" -ForegroundColor Green
154
+ return $true
155
+ }
156
+ elseif ($allowUnlisted -and $kokoroVoice -match '^[a-z]{2}_[a-z0-9_]+$') {
157
+ Write-Host "[WARN] '$NewVoice' is not in the shipped Kokoro catalog; allowing it (AGENTVIBES_ALLOW_UNLISTED_VOICE=1)." -ForegroundColor Yellow
158
+ Set-Content -Path $VoiceKokoroFile -Value $kokoroVoice
159
+ Write-Host "[OK] Voice set to: $kokoroVoice" -ForegroundColor Green
160
+ return $true
161
+ }
162
+ else {
163
+ Write-Host "[ERROR] Kokoro voice not found: $NewVoice" -ForegroundColor Red
164
+ Write-Host "That is not a known Kokoro voice. Examples: af_heart, am_michael, bf_emma, bm_george, jf_alpha, zf_xiaoxiao, ef_dora." -ForegroundColor Yellow
165
+ Write-Host "Set AGENTVIBES_ALLOW_UNLISTED_VOICE=1 to allow a newer-model voice." -ForegroundColor Yellow
166
+ return $false
167
+ }
168
+ }
169
+ else {
170
+ # FAIL-SAFE legacy path (catalog artifact missing): shape-only regex.
171
+ if ($kokoroVoice -notmatch '^[a-z]{2}_[a-z0-9_]+$') {
172
+ Write-Host "[ERROR] Kokoro voice not found: $NewVoice" -ForegroundColor Red
173
+ Write-Host "Kokoro ids look like <lang><sex>_name, e.g. af_heart, am_michael, bf_emma." -ForegroundColor Yellow
174
+ return $false
175
+ }
176
+ Set-Content -Path $VoiceKokoroFile -Value $kokoroVoice
177
+ Write-Host "[OK] Voice set to: $kokoroVoice" -ForegroundColor Green
178
+ return $true
179
+ }
180
+ }
132
181
 
133
182
  if ($ValidVoices -notcontains $NewVoice) {
134
183
  Write-Host "[ERROR] Voice not found: $NewVoice" -ForegroundColor Red
package/.mcp.json CHANGED
@@ -1,12 +1,5 @@
1
1
  {
2
2
  "mcpServers": {
3
- "firecrawl-mcp": {
4
- "command": "npx",
5
- "args": [
6
- "-y",
7
- "firecrawl-mcp"
8
- ]
9
- },
10
3
  "agentvibes": {
11
4
  "command": "npx",
12
5
  "args": [
package/README.md CHANGED
@@ -58,9 +58,18 @@ New here? The [**Quick Start guide**](docs/quick-start.md) walks you through you
58
58
 
59
59
  ---
60
60
 
61
- ## 🆕 A stronger core (v5.12.0)
61
+ ## 🆕 Your voices everywhere (v5.13.0)
62
62
 
63
- During a week of early access to Anthropic's new **Fable** model, we rebuilt the heart of AgentVibes into **one shared core**. The voice / engine / routing / volume / mute logic that used to be copied across four scripts (and drift apart) now lives in a single place — simpler, more consistent, and steadier.
63
+ Pick the voices built into **Windows** or **Mac** and hear them wherever you're listening even when your agents run on another computer. Plus a friendly heads-up chime so you always know sound is coming.
64
+
65
+ - **🖥️ Your computer's own voices, from anywhere** — choose Windows or Mac voices and hear them on your machine; every voice is shown, with unavailable ones clearly marked.
66
+ - **🗂️ All voices in one list** — Piper, Kokoro, ElevenLabs, Windows, Mac, and Soprano in one place, so what you see is what you can use.
67
+ - **🔔 Heads-up chime** — a short sound plays just before a voice or music preview, so you know audio is on the way.
68
+ - **🆔 Agents that introduce themselves** — optional self-introductions so you know who's talking in a team.
69
+
70
+ ### v5.12.0 — A stronger core
71
+
72
+ During a week of early access to Anthropic's new **Fable** model, we rebuilt the heart of AgentVibes into **one shared core**. The voice / engine / routing / volume / mute logic now lives in a single place — simpler, more consistent, and steadier.
64
73
 
65
74
  - **🔊 Previews play in the right place** — with SSH remote configured, voice and **music previews** play on your receiver; otherwise they play locally.
66
75
  - **🧠 One shared core** — Kokoro-on-Linux silence and per-voice drift fixed at the source, with a safe fallback if needed.
@@ -224,7 +233,7 @@ Running a full BMAD agent team? **Every agent gets its own distinct voice**, aut
224
233
 
225
234
  ## About
226
235
 
227
- **AgentVibes** · v5.12.0 · Licensed under [Apache-2.0](LICENSE)
236
+ **AgentVibes** · v5.13.0 · Licensed under [Apache-2.0](LICENSE)
228
237
 
229
238
  Built by **Paul Preibisch** — [@997Fire on X](https://x.com/997Fire) · [agentvibes.org](https://agentvibes.org) · [github.com/paulpreibisch/AgentVibes](https://github.com/paulpreibisch/AgentVibes)
230
239
 
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # AgentVibes Release Notes
2
2
 
3
+ ## 🎉 v5.13.0 — Your Voices Everywhere, With a Heads-Up
4
+
5
+ **Released:** 2026-07-16 · on `latest` — `npm install agentvibes`
6
+
7
+ What's new:
8
+
9
+ ### 🖥️ Use your computer's own voices, from anywhere
10
+
11
+ Run your agents on one machine and listen on another? You can now pick the voices built into **Windows** (David, Zira, Mark) or **Mac**, and hear them right where you're sitting. AgentVibes shows you every voice and clearly marks the ones your listening device can play.
12
+
13
+ ### 🗂️ All your voices in one tidy list
14
+
15
+ Piper, Kokoro, ElevenLabs, Windows, Mac, Soprano — every voice now comes from a single list, so what you see is always what you can use.
16
+
17
+ ### 🔔 A "heads-up" chime before sound plays
18
+
19
+ Just before a voice line or music preview starts, you'll hear a short chime — so you always know audio is on the way, even if it takes a moment.
20
+
21
+ ### 🎵 Music previews follow your sound
22
+
23
+ Preview a track and it plays wherever your audio is set to go — including on another computer.
24
+
25
+ ### 🆔 Agents that introduce themselves
26
+
27
+ Turn on self-introductions and each agent says who it is when it starts — handy when a whole team is talking.
28
+
29
+ ### 🛟 Your own edits are kept safe when you update
30
+
31
+ Tinkered with one of the AgentVibes files in your `.claude/hooks` folder? From this version on, updating never throws your work away. If we need to update a file you'd changed, we tuck your copy in beside it with `.user.bak` on the end — like `play-tts.sh.user.bak`.
32
+
33
+ **That file is made by AgentVibes — nothing is broken and nothing else put it there.** It's simply your old version, saved so you can peek at it or copy your changes back into the new one. Delete it whenever you're done with it.
34
+
35
+ If you customized files in an older version, it's worth a quick look in `.claude/hooks` for anything you'd want to put back.
36
+
37
+ ### ✨ More voices, smoother ride
38
+
39
+ - **ElevenLabs** voices fully supported
40
+ - More **Kokoro** voices, working great on Windows
41
+ - Faster, more reliable setup on Windows
42
+ - **3,263 automated tests passing** — steady and dependable
43
+
44
+ ---
45
+
3
46
  ## 🎉 v5.12.0 — The Fable Week Overhaul (Stable)
4
47
 
5
48
  **Released:** 2026-07-05 · now on `latest` — `npm install agentvibes`
@@ -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"]))
@@ -42,7 +42,6 @@ HOOKS_LINUX = PROJECT_ROOT / ".claude" / "hooks"
42
42
  # The auto-conversion (server.py:902-903) converts .sh -> .ps1
43
43
  MCP_HARDCODED_SCRIPTS = [
44
44
  "provider-manager",
45
- "learn-manager",
46
45
  "speed-manager",
47
46
  "download-extra-voices",
48
47
  "verbosity-manager",
@@ -159,7 +158,6 @@ def test_run_script_error_for_missing():
159
158
  MCP_TOOL_SCRIPT_MAP = {
160
159
  "set_speed": "speed-manager",
161
160
  "get_speed": "speed-manager",
162
- "set_learn_mode": "learn-manager",
163
161
  "set_verbosity": "verbosity-manager",
164
162
  "get_verbosity": "verbosity-manager",
165
163
  "mute": "verbosity-manager",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "agentvibes",
4
- "version": "5.12.0",
4
+ "version": "5.13.0",
5
5
  "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
6
  "homepage": "https://agentvibes.org",
7
7
  "keywords": [