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.
- package/.claude/commands/agent-vibes/commands.json +0 -20
- package/.claude/commands/agent-vibes/unmute.md +6 -2
- package/.claude/config/audio-effects.cfg +6 -6
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/agentvibes-session-id.sh +69 -0
- package/.claude/hooks/bmad-party-speak.sh +20 -4
- package/.claude/hooks/bmad-speak.sh +60 -2
- package/.claude/hooks/bmad-tts-injector.sh +20 -1
- package/.claude/hooks/bmad-voice-manager.sh +25 -3
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
- package/.claude/hooks/clawdbot-receiver.sh +19 -1
- package/.claude/hooks/elevenlabs-voices.sh +62 -0
- package/.claude/hooks/kokoro-installer.sh +20 -10
- package/.claude/hooks/language-manager.sh +10 -3
- package/.claude/hooks/party-set-room.sh +71 -0
- package/.claude/hooks/party-stage-roster.py +328 -0
- package/.claude/hooks/personality-manager.sh +19 -2
- package/.claude/hooks/piper-voice-manager.sh +3 -2
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
- package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
- package/.claude/hooks/play-tts-kokoro.sh +33 -6
- package/.claude/hooks/play-tts-soprano.sh +3 -2
- package/.claude/hooks/play-tts-ssh-remote.sh +37 -29
- package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
- package/.claude/hooks/play-tts.sh +66 -61
- package/.claude/hooks/provider-catalog.json +352 -0
- package/.claude/hooks/provider-catalog.sh +161 -0
- package/.claude/hooks/provider-commands.sh +2 -1
- package/.claude/hooks/provider-manager.sh +47 -9
- package/.claude/hooks/python-resolver.sh +117 -0
- package/.claude/hooks/session-id.sh +56 -0
- package/.claude/hooks/session-start-tts.sh +39 -0
- package/.claude/hooks/speed-manager.sh +1 -1
- package/.claude/hooks/translate-manager.sh +3 -2
- package/.claude/hooks/translator.py +1 -1
- package/.claude/hooks/voice-manager.sh +242 -10
- package/.claude/hooks-windows/language-manager.ps1 +7 -1
- package/.claude/hooks-windows/personality-manager.ps1 +16 -1
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
- package/.claude/hooks-windows/play-tts.ps1 +32 -3
- package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
- package/.claude/hooks-windows/provider-manager.ps1 +63 -8
- package/.claude/hooks-windows/tts-watcher.ps1 +33 -12
- package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
- package/.mcp.json +0 -7
- package/README.md +12 -3
- package/RELEASE_NOTES.md +43 -0
- package/mcp-server/server.py +146 -49
- package/mcp-server/test_mcp_correctness.py +20 -2
- package/mcp-server/test_windows_script_parity.py +0 -2
- package/package.json +1 -1
- package/src/cli/list-voices.js +218 -114
- package/src/console/bling.js +71 -0
- package/src/console/music-preview.js +79 -0
- package/src/console/tabs/music-tab.js +16 -39
- package/src/console/tabs/settings-tab.js +195 -13
- package/src/console/tabs/setup-tab.js +9 -34
- package/src/console/tabs/voices-tab.js +83 -14
- package/src/console/widgets/track-picker.js +82 -0
- package/src/installer.js +124 -10
- package/src/services/provider-catalog.js +412 -0
- package/src/services/provider-voice-catalog.js +52 -73
- package/src/services/tts-engine-service.js +29 -0
- package/src/utils/provider-validator.js +62 -12
- package/.claude/commands/agent-vibes/language.md +0 -23
- package/.claude/commands/agent-vibes/learn.md +0 -67
- package/.claude/commands/agent-vibes/replay-target.md +0 -14
- package/.claude/commands/agent-vibes/target-voice.md +0 -26
- package/.claude/commands/agent-vibes/target.md +0 -30
- package/.claude/hooks/learn-manager.sh +0 -492
- package/.claude/hooks/replay-target-audio.sh +0 -95
- 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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
87
|
-
#
|
|
88
|
-
#
|
|
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
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
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
|
-
## 🆕
|
|
61
|
+
## 🆕 Your voices everywhere (v5.13.0)
|
|
62
62
|
|
|
63
|
-
|
|
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.
|
|
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`
|
package/mcp-server/server.py
CHANGED
|
@@ -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
|
|
864
|
+
Switch TTS provider between the supported synthesis engines.
|
|
732
865
|
|
|
733
866
|
Args:
|
|
734
|
-
provider: Provider name
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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 /
|
|
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.
|
|
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": [
|