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
@@ -43,6 +43,18 @@ to_lower() {
43
43
  echo "$1" | tr '[:upper:]' '[:lower:]'
44
44
  }
45
45
 
46
+ # Source the generated Provider Catalog (bash-3.2-safe SSOT accessors) when present.
47
+ # FAIL-SAFE: consumers probe `type catalog_* >/dev/null 2>&1` and fall back to the
48
+ # legacy regex/hardcoded behavior when the artifact is missing (installed-tree skew)
49
+ # — mirrors play-tts.sh's PLAN_OK legacy fallback. Switch/list never break on a
50
+ # missing catalog. Double-sourcing is guarded inside provider-catalog.sh.
51
+ _load_provider_catalog() {
52
+ if [[ -f "$SCRIPT_DIR/provider-catalog.sh" ]]; then
53
+ # shellcheck source=/dev/null
54
+ source "$SCRIPT_DIR/provider-catalog.sh" 2>/dev/null || true
55
+ fi
56
+ }
57
+
46
58
  # Determine target .claude directory based on context
47
59
  # Priority:
48
60
  # 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings)
@@ -82,13 +94,26 @@ get_default_voice() {
82
94
 
83
95
  case "$active_provider" in
84
96
  piper)
85
- echo "en_US-lessac-medium" # Piper default
97
+ # Piper default sourced from the generated Provider Catalog (SSOT, AVI-S9.4).
98
+ # FAIL-SAFE: legacy literal if the catalog artifact is missing (installed-tree skew).
99
+ _load_provider_catalog
100
+ if type catalog_default_voice >/dev/null 2>&1; then
101
+ catalog_default_voice piper
102
+ else
103
+ echo "en_US-lessac-medium" # Piper default (legacy fallback)
104
+ fi
86
105
  ;;
87
106
  macos)
88
107
  echo "Samantha" # macOS default
89
108
  ;;
90
109
  *)
91
- echo "en_US-lessac-medium" # Default to Piper
110
+ # Default to Piper — same catalog-sourced value, same fail-safe.
111
+ _load_provider_catalog
112
+ if type catalog_default_voice >/dev/null 2>&1; then
113
+ catalog_default_voice piper
114
+ else
115
+ echo "en_US-lessac-medium" # Default to Piper (legacy fallback)
116
+ fi
92
117
  ;;
93
118
  esac
94
119
  }
@@ -103,7 +128,7 @@ case "$1" in
103
128
 
104
129
  ACTIVE_PROVIDER="piper" # default
105
130
  if [ -f "$PROVIDER_FILE" ]; then
106
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
131
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
107
132
  fi
108
133
 
109
134
  CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || get_default_voice)
@@ -170,12 +195,76 @@ case "$1" in
170
195
  fi
171
196
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
172
197
  fi
198
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
199
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
200
+ node "$FORMATTER" "kokoro" "$CURRENT_VOICE"
201
+ else
202
+ echo "🎤 Available Kokoro TTS Voices:"
203
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
204
+ _load_provider_catalog
205
+ if type catalog_list_voices >/dev/null 2>&1; then
206
+ # All ~54 ids from the SSOT, one per line (catalog order = grouped by lang prefix).
207
+ catalog_list_voices kokoro | sed 's/^/ /'
208
+ else
209
+ # FAIL-SAFE legacy path (catalog artifact missing) — degraded, never dead.
210
+ echo " af_heart af_bella af_nicole af_sarah af_sky (American, female)"
211
+ echo " am_adam am_michael am_onyx am_puck (American, male)"
212
+ echo " bf_emma bf_lily bm_george bm_daniel (British)"
213
+ echo " jf_alpha (Japanese) zf_xiaoxiao (Mandarin) ef_dora (Spanish)"
214
+ fi
215
+ echo " (current: $CURRENT_VOICE)"
216
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
217
+ fi
218
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
219
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
220
+ node "$FORMATTER" "elevenlabs" "$CURRENT_VOICE"
221
+ else
222
+ echo "🎤 Available ElevenLabs Voices:"
223
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
224
+ if [[ -f "$SCRIPT_DIR/elevenlabs-voices.sh" ]]; then
225
+ # shellcheck source=/dev/null
226
+ source "$SCRIPT_DIR/elevenlabs-voices.sh"
227
+ elevenlabs_voice_names | sort | sed 's/^/ /'
228
+ fi
229
+ echo " (current: $CURRENT_VOICE)"
230
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
231
+ fi
232
+ elif [[ "$ACTIVE_PROVIDER" == "soprano" ]]; then
233
+ # Soprano is a single-voice provider (voiceModel: single) — list its one
234
+ # canonical voice from the generated Provider Catalog (SSOT, AVI-S9.4).
235
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
236
+ node "$FORMATTER" "soprano" "$CURRENT_VOICE"
237
+ else
238
+ echo "🎤 Available Soprano TTS Voices:"
239
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
240
+ _load_provider_catalog
241
+ if type catalog_list_voices >/dev/null 2>&1; then
242
+ catalog_list_voices soprano | sed 's/^/ /'
243
+ else
244
+ # FAIL-SAFE legacy path (catalog artifact missing): single canonical voice.
245
+ echo " soprano-default"
246
+ fi
247
+ echo " (current: $CURRENT_VOICE)"
248
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
249
+ fi
173
250
  else
174
251
  echo "❌ Unknown provider: $ACTIVE_PROVIDER"
175
252
  echo ""
176
253
  echo "Available providers:"
177
- echo " - piper (Free, Offline)"
178
- echo " - macos (Built-in, macOS only)"
254
+ # Derived from the generated Provider Catalog (SSOT, AVI-S9.4) so a new
255
+ # provider never silently goes missing from this help text again.
256
+ _load_provider_catalog
257
+ if type catalog_providers_for_platform >/dev/null 2>&1; then
258
+ while IFS= read -r _pid; do
259
+ echo " - $_pid ($(catalog_display_name "$_pid"))"
260
+ done < <(catalog_providers_for_platform unix)
261
+ else
262
+ # FAIL-SAFE legacy path (catalog artifact missing).
263
+ echo " - piper (Free, Offline)"
264
+ echo " - macos (Built-in, macOS only)"
265
+ echo " - kokoro (Neural, local)"
266
+ echo " - elevenlabs (Cloud, API key)"
267
+ fi
179
268
  echo ""
180
269
  echo "Switch provider with: /agent-vibes:provider switch piper"
181
270
  fi
@@ -221,7 +310,7 @@ case "$1" in
221
310
 
222
311
  ACTIVE_PROVIDER="piper" # default
223
312
  if [[ -n "$PROVIDER_FILE" ]]; then
224
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
313
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
225
314
  fi
226
315
 
227
316
  # Voice lookup strategy depends on active provider
@@ -363,12 +452,123 @@ case "$1" in
363
452
  echo "Download extra voices with: /agent-vibes:provider download"
364
453
  exit 1
365
454
  fi
455
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
456
+ # Kokoro switch-time validation via the generated Provider Catalog (SSOT):
457
+ # MEMBERSHIP + canonical case-fold (AF_HEART → af_heart), so a typo like
458
+ # `af_hart` is rejected HERE and never reaches tts-voice.txt to die silently
459
+ # at the model. Escape hatch AGENTVIBES_ALLOW_UNLISTED_VOICE=1 bypasses
460
+ # MEMBERSHIP (not shape) so a voice from a newer kokoro model still switches.
461
+ # FAIL-SAFE: if the catalog artifact is missing (installed-tree skew) we fall
462
+ # back to the legacy shape-only regex — degraded, never dead.
463
+ _load_provider_catalog
464
+ KOKORO_VOICE_LC="$(to_lower "$VOICE_NAME")"
465
+ if type catalog_validate_voice >/dev/null 2>&1; then
466
+ _KOKORO_CANON="$(catalog_validate_voice kokoro "$VOICE_NAME" 2>/dev/null || true)"
467
+ if [[ -n "$_KOKORO_CANON" ]]; then
468
+ FOUND="$_KOKORO_CANON"
469
+ elif [[ "${AGENTVIBES_ALLOW_UNLISTED_VOICE:-}" == "1" ]] && [[ "$KOKORO_VOICE_LC" =~ ^[a-z]{2}_[a-z0-9_]+$ ]]; then
470
+ echo "⚠️ '$VOICE_NAME' is not in the shipped Kokoro catalog; allowing it (AGENTVIBES_ALLOW_UNLISTED_VOICE=1)." >&2
471
+ FOUND="$KOKORO_VOICE_LC"
472
+ else
473
+ echo "❌ Kokoro voice not found: $VOICE_NAME"
474
+ echo ""
475
+ echo "That is not a known Kokoro voice. Examples:"
476
+ echo " - af_heart, af_bella, af_nicole (American English, female)"
477
+ echo " - am_adam, am_michael (American English, male)"
478
+ echo " - bf_emma, bf_lily (British English, female)"
479
+ echo " - bm_george, bm_daniel (British English, male)"
480
+ echo " - jf_alpha (Japanese), zf_xiaoxiao (Mandarin), ef_dora (Spanish)"
481
+ echo ""
482
+ echo "Set AGENTVIBES_ALLOW_UNLISTED_VOICE=1 to allow a newer-model voice."
483
+ echo "List all Kokoro voices with: /agent-vibes:list"
484
+ exit 1
485
+ fi
486
+ else
487
+ # FAIL-SAFE legacy path (catalog artifact missing): shape-only regex.
488
+ if [[ "$KOKORO_VOICE_LC" =~ ^[a-z]{2}_[a-z0-9_]+$ ]]; then
489
+ FOUND="$KOKORO_VOICE_LC"
490
+ else
491
+ echo "❌ Kokoro voice not found: $VOICE_NAME"
492
+ echo ""
493
+ echo "Kokoro voice ids look like <lang><sex>_name. Examples:"
494
+ echo " - af_heart, af_bella, af_nicole (American English, female)"
495
+ echo " - am_adam, am_michael (American English, male)"
496
+ echo " - bf_emma, bf_lily (British English, female)"
497
+ echo " - bm_george, bm_daniel (British English, male)"
498
+ echo " - jf_alpha (Japanese), zf_xiaoxiao (Mandarin), ef_dora (Spanish)"
499
+ echo ""
500
+ echo "List all Kokoro voices with: /agent-vibes:list"
501
+ exit 1
502
+ fi
503
+ fi
504
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
505
+ # ElevenLabs voice lookup — resolve a friendly name (case-insensitive) OR a
506
+ # raw voice_id to the canonical voice_id and save THAT (play-tts-elevenlabs.sh
507
+ # accepts a raw id directly). Resolution is bash-native via the shared catalog
508
+ # (elevenlabs-voices.sh) — the SAME source the synth hook uses — so it works
509
+ # in installed deployments with no Node dependency.
510
+ EL_RESOLVED=""
511
+ if [[ -f "$SCRIPT_DIR/elevenlabs-voices.sh" ]]; then
512
+ # shellcheck source=/dev/null
513
+ source "$SCRIPT_DIR/elevenlabs-voices.sh"
514
+ EL_RESOLVED="$(elevenlabs_resolve_voice "$VOICE_NAME" 2>/dev/null || true)"
515
+ elif [[ "$VOICE_NAME" =~ ^[A-Za-z0-9]{20}$ ]]; then
516
+ # Catalog missing but the input is already a raw voice_id — accept it.
517
+ EL_RESOLVED="$VOICE_NAME"
518
+ fi
519
+ if [[ -n "$EL_RESOLVED" ]]; then
520
+ FOUND="$EL_RESOLVED"
521
+ else
522
+ echo "❌ ElevenLabs voice not found: $VOICE_NAME"
523
+ echo ""
524
+ echo "ElevenLabs voices include: Sarah, Roger, Laura, George, Callum, Alice,"
525
+ echo "Matilda, Will, Jessica, Eric, Brian, Daniel, Lily, Bill (or a raw voice_id)."
526
+ echo ""
527
+ echo "List all ElevenLabs voices with: /agent-vibes:list"
528
+ exit 1
529
+ fi
530
+ elif [[ "$ACTIVE_PROVIDER" == "soprano" ]]; then
531
+ # Soprano switch-time validation via the generated Provider Catalog (SSOT):
532
+ # voiceModel `single` — accepts '', 'soprano', 'soprano-default' (case-insensitive)
533
+ # and canonicalizes to 'soprano-default'; anything else is rejected here so a
534
+ # typo never reaches tts-voice.txt. FAIL-SAFE: if the catalog artifact is
535
+ # missing, the same fixed rule is applied hardcoded (soprano has exactly one
536
+ # voice — this rule never drifts).
537
+ _load_provider_catalog
538
+ SOPRANO_CANON=""
539
+ if type catalog_validate_voice >/dev/null 2>&1; then
540
+ SOPRANO_CANON="$(catalog_validate_voice soprano "$VOICE_NAME" 2>/dev/null || true)"
541
+ else
542
+ case "$(to_lower "$VOICE_NAME")" in
543
+ ''|soprano|soprano-default) SOPRANO_CANON="soprano-default" ;;
544
+ esac
545
+ fi
546
+ if [[ -n "$SOPRANO_CANON" ]]; then
547
+ FOUND="$SOPRANO_CANON"
548
+ else
549
+ echo "❌ Soprano voice not found: $VOICE_NAME"
550
+ echo ""
551
+ echo "Soprano TTS has a single voice: soprano-default"
552
+ echo "(accepts '', 'soprano', or 'soprano-default')."
553
+ exit 1
554
+ fi
366
555
  else
367
556
  echo "❌ Unknown provider: $ACTIVE_PROVIDER"
368
557
  echo ""
369
558
  echo "Available providers:"
370
- echo " - piper (Free, Offline)"
371
- echo " - macos (Built-in, macOS only)"
559
+ # Derived from the generated Provider Catalog (SSOT, AVI-S9.4).
560
+ _load_provider_catalog
561
+ if type catalog_providers_for_platform >/dev/null 2>&1; then
562
+ while IFS= read -r _pid; do
563
+ echo " - $_pid ($(catalog_display_name "$_pid"))"
564
+ done < <(catalog_providers_for_platform unix)
565
+ else
566
+ # FAIL-SAFE legacy path (catalog artifact missing).
567
+ echo " - piper (Free, Offline)"
568
+ echo " - macos (Built-in, macOS only)"
569
+ echo " - kokoro (Neural, local)"
570
+ echo " - elevenlabs (Cloud, API key)"
571
+ fi
372
572
  echo ""
373
573
  echo "Switch provider with: /agent-vibes:provider switch piper"
374
574
  exit 1
@@ -412,7 +612,7 @@ case "$1" in
412
612
  fi
413
613
 
414
614
  if [ -f "$PROVIDER_FILE" ]; then
415
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
615
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
416
616
  if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
417
617
  echo "Provider: Piper TTS (Free, Offline)"
418
618
  elif [[ "$ACTIVE_PROVIDER" == "ssh-remote" ]]; then
@@ -421,6 +621,10 @@ case "$1" in
421
621
  echo "Provider: Piper TTS (via AgentVibes Receiver)"
422
622
  elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
423
623
  echo "Provider: macOS Say (Built-in, Free)"
624
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
625
+ echo "Provider: Kokoro TTS (Neural, Local)"
626
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
627
+ echo "Provider: ElevenLabs (Cloud, API Key)"
424
628
  else
425
629
  echo "Provider: $ACTIVE_PROVIDER"
426
630
  fi
@@ -466,7 +670,7 @@ case "$1" in
466
670
 
467
671
  ACTIVE_PROVIDER="piper" # default
468
672
  if [ -f "$PROVIDER_FILE" ]; then
469
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
673
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
470
674
  fi
471
675
 
472
676
  if [[ "$ACTIVE_PROVIDER" == "piper" || "$ACTIVE_PROVIDER" == "ssh-remote" || "$ACTIVE_PROVIDER" == "agentvibes-receiver" ]]; then
@@ -487,6 +691,34 @@ case "$1" in
487
691
  else
488
692
  echo "(macOS voices only available on macOS)"
489
693
  fi
694
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
695
+ # Kokoro's catalog is fixed; list ALL ids from the generated Provider Catalog (SSOT).
696
+ _load_provider_catalog
697
+ if type catalog_list_voices >/dev/null 2>&1; then
698
+ catalog_list_voices kokoro | sort
699
+ else
700
+ # FAIL-SAFE legacy path (catalog artifact missing): representative subset.
701
+ printf '%s\n' af_heart af_bella af_nicole af_sarah af_sky \
702
+ am_adam am_michael am_onyx am_puck \
703
+ bf_emma bf_lily bm_george bm_daniel \
704
+ jf_alpha zf_xiaoxiao ef_dora | sort
705
+ fi
706
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
707
+ if [[ -f "$SCRIPT_DIR/elevenlabs-voices.sh" ]]; then
708
+ # shellcheck source=/dev/null
709
+ source "$SCRIPT_DIR/elevenlabs-voices.sh"
710
+ elevenlabs_voice_names | sort
711
+ fi
712
+ elif [[ "$ACTIVE_PROVIDER" == "soprano" ]]; then
713
+ # Soprano's catalog is a single fixed voice; list it from the generated
714
+ # Provider Catalog (SSOT, AVI-S9.4).
715
+ _load_provider_catalog
716
+ if type catalog_list_voices >/dev/null 2>&1; then
717
+ catalog_list_voices soprano
718
+ else
719
+ # FAIL-SAFE legacy path (catalog artifact missing).
720
+ echo "soprano-default"
721
+ fi
490
722
  else
491
723
  echo "(Unknown provider: $ACTIVE_PROVIDER)"
492
724
  fi
@@ -146,7 +146,13 @@ switch ($Command) {
146
146
  Write-Output "Usage: language-manager.ps1 check-voice <voice-name>"
147
147
  exit 1
148
148
  }
149
- $multilingual = @("Antoni", "Rachel", "Domi", "Bella", "Charlotte", "Matilda")
149
+ # AVI-S9.6 AC1: trimmed to names that exist in the 21-voice ElevenLabs
150
+ # catalog (src/services/provider-catalog.js ELEVENLABS_VOICES) — the
151
+ # list previously included "Antoni"/"Rachel"/"Domi"/"Charlotte", none of
152
+ # which exist there (the same class of bug design row 21 found in the
153
+ # now-deleted learn-manager.sh). Parity-asserted against the catalog by
154
+ # test/unit/provider-catalog-conformance.test.js.
155
+ $multilingual = @("Bella", "Matilda")
150
156
  if ($Arg1 -in $multilingual) { Write-Output "yes" } else { Write-Output "no" }
151
157
  }
152
158
 
@@ -29,6 +29,13 @@ if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude"))
29
29
 
30
30
  $PersonalityFile = Join-Path $ConfigClaudeDir "tts-personality.txt"
31
31
 
32
+ # Load the generated Provider Catalog (SSOT) for the Piper default voice.
33
+ # FAIL-SAFE: consumers probe `Get-Command Get-CatalogDefaultVoice` and fall back
34
+ # to the legacy literal when the artifact is missing (installed-tree skew) —
35
+ # mirrors voice-manager-windows.ps1 / play-tts-kokoro.ps1 (AVI-S9.3/9.4).
36
+ $CatalogPs1 = Join-Path $ScriptDir 'provider-catalog.ps1'
37
+ if (Test-Path $CatalogPs1) { . $CatalogPs1 }
38
+
32
39
  function Get-PersonalityData {
33
40
  param([string]$Personality, [string]$Field)
34
41
  $file = Join-Path $PersonalitiesDir "$Personality.md"
@@ -116,7 +123,15 @@ switch ($Command) {
116
123
 
117
124
  # Get assigned voice
118
125
  $assignedVoice = Get-PersonalityData $personality "piper_voice"
119
- if (-not $assignedVoice) { $assignedVoice = "en_US-lessac-medium" }
126
+ if (-not $assignedVoice) {
127
+ # Fallback to default Piper voice — sourced from the generated Provider
128
+ # Catalog SSOT (AVI-S9.4). FAIL-SAFE: legacy literal if artifact missing.
129
+ $assignedVoice = $null
130
+ if (Get-Command Get-CatalogDefaultVoice -ErrorAction SilentlyContinue) {
131
+ $assignedVoice = Get-CatalogDefaultVoice -Provider 'piper'
132
+ }
133
+ if (-not $assignedVoice) { $assignedVoice = "en_US-lessac-medium" }
134
+ }
120
135
 
121
136
  if ($assignedVoice) {
122
137
  Write-Output "Switching to assigned voice: $assignedVoice"
@@ -27,6 +27,20 @@ $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
27
27
  $ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
28
28
  if (Test-Path $ProjectClaudeDir) { $ClaudeDir = $ProjectClaudeDir } else { $ClaudeDir = "$env:USERPROFILE\.claude" }
29
29
 
30
+ # --- Default voice from the generated Provider Catalog (SSOT) -----------------
31
+ # FAIL-SAFE: keep the literal fallback for installed-tree skew (mirrors play-tts.sh
32
+ # PLAN_OK). The synth-time shape check below stays LENIENT (design §3.3) so a voice
33
+ # from a newer kokoro model still degrades audibly instead of bricking.
34
+ $KokoroDefault = "af_heart"
35
+ $CatalogPs1 = Join-Path $ScriptPath 'provider-catalog.ps1'
36
+ if (Test-Path $CatalogPs1) {
37
+ . $CatalogPs1
38
+ if (Get-Command Get-CatalogDefaultVoice -ErrorAction SilentlyContinue) {
39
+ $d = Get-CatalogDefaultVoice -Provider 'kokoro'
40
+ if ($d) { $KokoroDefault = $d }
41
+ }
42
+ }
43
+
30
44
  $AudioDir = "$ClaudeDir\audio"
31
45
  $ConfigDir = "$ClaudeDir\config"
32
46
  if (-not (Test-Path $AudioDir)) { New-Item -ItemType Directory -Path $AudioDir -Force | Out-Null }
@@ -70,13 +84,15 @@ if ($VoiceOverride) {
70
84
  }
71
85
  # Kokoro voice ids look like af_heart, am_michael, bf_emma. Strip any ::display suffix.
72
86
  if ($VoiceName -match '::') { $VoiceName = ($VoiceName -split '::')[0] }
73
- if (-not $VoiceName) { $VoiceName = "af_heart" }
87
+ if (-not $VoiceName) { $VoiceName = $KokoroDefault }
74
88
 
75
89
  # Security: validate voice id (lowercase prefix + underscore + name).
76
- # Rejects path-traversal / injection before it reaches the command line.
90
+ # LENIENT shape check (design §3.3) — rejects path-traversal / injection but lets
91
+ # an unknown-but-shape-valid voice from a newer kokoro model through; falls back
92
+ # to the catalog default only on a shape violation.
77
93
  if ($VoiceName -notmatch '^[a-z]{2}_[a-z0-9_]+$') {
78
- Write-Host "[WARNING] Invalid Kokoro voice '$VoiceName' - falling back to af_heart" -ForegroundColor Yellow
79
- $VoiceName = "af_heart"
94
+ Write-Host "[WARNING] Invalid Kokoro voice '$VoiceName' - falling back to $KokoroDefault" -ForegroundColor Yellow
95
+ $VoiceName = $KokoroDefault
80
96
  }
81
97
 
82
98
  # --- Optional speed ----------------------------------------------------------
@@ -116,8 +116,9 @@ switch ($ActiveProvider) {
116
116
  # by the SSH-receiver watcher via -ProviderOverride). This lets the Linux-side
117
117
  # audio-effects.cfg row for llm:claude-code specify "piper" and have it honoured
118
118
  # on Windows without requiring the Windows tts-provider.txt to be reconfigured.
119
- # Priority: lower than per-LLM $_LlmEngine (audio-effects.cfg row, set later), higher
120
- # than the global tts-provider.txt default set above.
119
+ # Priority: an explicit -ProviderOverride is AUTHORITATIVE it wins over both the
120
+ # per-LLM $_LlmEngine column and the global tts-provider.txt default (see the
121
+ # $ProviderOverride guard in the engine-resolution block below).
121
122
  if ($ProviderOverride) {
122
123
  switch ($ProviderOverride) {
123
124
  { $_ -in "windows-piper", "piper" } {
@@ -308,8 +309,30 @@ $PlanVoiceIsOverride = $false
308
309
  if ($ProviderOverride) {
309
310
  # A fresh -ProviderOverride for THIS request must win over any stale/inherited
310
311
  # AGENTVIBES_FORCE_PROVIDER in the environment (a validated allowlist only).
312
+ #
313
+ # The allowlist DERIVES from the Provider Catalog (SSOT): the Windows platform
314
+ # set PLUS the cross-platform forwarding aliases this script normalizes
315
+ # (piper→windows-piper, sapi→windows-sapi, and macos forwarded verbatim for
316
+ # SSH-relayed senders). FAIL-SAFE: the literal below is the legacy fallback
317
+ # used when the generated provider-catalog.ps1 is missing (installed-tree skew).
318
+ $ProviderOverrideAllowlist = @('piper','soprano','macos','windows-sapi','sapi','kokoro','windows-piper')
319
+ $__CatalogPs1 = Join-Path $ScriptPath 'provider-catalog.ps1'
320
+ if (Test-Path $__CatalogPs1) {
321
+ try {
322
+ . $__CatalogPs1
323
+ if (Get-Command Get-CatalogProvidersForPlatform -ErrorAction SilentlyContinue) {
324
+ $__WinSet = @(Get-CatalogProvidersForPlatform 'windows')
325
+ if ($__WinSet.Count -gt 0) {
326
+ # Windows synth providers + the forwarding aliases play-tts.ps1 normalizes.
327
+ $ProviderOverrideAllowlist = @($__WinSet + @('piper','sapi','macos') | Select-Object -Unique)
328
+ }
329
+ }
330
+ } catch {
331
+ # Keep the legacy fallback allowlist on any catalog load error.
332
+ }
333
+ }
311
334
  switch ($ProviderOverride) {
312
- { $_ -in @('piper','soprano','macos','windows-sapi','sapi','kokoro','elevenlabs','windows-piper') } {
335
+ { $_ -in $ProviderOverrideAllowlist } {
313
336
  $env:AGENTVIBES_FORCE_PROVIDER = $ProviderOverride
314
337
  }
315
338
  }
@@ -522,6 +545,12 @@ else {
522
545
  # ENGINE column or the global tts-provider.txt default.
523
546
  $ProviderScript = "$HooksDir\play-tts-kokoro.ps1"
524
547
  }
548
+ elseif ($ProviderOverride) {
549
+ # An explicit -ProviderOverride (forwarded from the SSH payload's "provider"
550
+ # field, or a preview) is AUTHORITATIVE and was already applied above. Do NOT
551
+ # let the per-LLM ENGINE column override it, or a receiver whose llm row
552
+ # defaults to piper silently swallows a forwarded windows-sapi request.
553
+ }
525
554
  elseif ($_LlmEngine) {
526
555
  # Accept both canonical Windows names and the cross-platform aliases the TUI
527
556
  # writes (e.g. "piper" saved on a Linux/WSL install that is later read on
@@ -0,0 +1,140 @@
1
+ # provider-catalog.ps1 — GENERATED FILE. DO NOT EDIT.
2
+ # DO NOT EDIT — generated from src/services/provider-catalog.js
3
+ # Regenerate with: node scripts/generate-provider-catalog.mjs
4
+ # content-hash: sha256:5e717024f1250a8f54d07291256e532606dba7b4a15ab9589305d1af6bba7f35
5
+ # Dot-source this file to load the catalog accessors; do not execute it.
6
+
7
+ $script:AgentVibesCatalogVersion = '5e717024f1250a8f'
8
+
9
+ $script:CatalogDefaults = @{
10
+ 'soprano' = 'soprano-default'
11
+ 'piper' = 'en_US-lessac-medium'
12
+ 'kokoro' = 'af_heart'
13
+ 'elevenlabs' = 'Sarah'
14
+ 'macos' = 'Samantha'
15
+ 'windows-sapi' = ''
16
+ 'windows-piper' = 'en_US-lessac-medium'
17
+ }
18
+
19
+ $script:CatalogDisplayNames = @{
20
+ 'soprano' = 'Soprano TTS'
21
+ 'piper' = 'Piper TTS'
22
+ 'kokoro' = 'Kokoro TTS'
23
+ 'elevenlabs' = 'ElevenLabs'
24
+ 'macos' = 'macOS Say'
25
+ 'windows-sapi' = 'Windows SAPI'
26
+ 'windows-piper' = 'Piper TTS'
27
+ }
28
+
29
+ $script:CatalogAlias = @{
30
+ 'macos-say' = 'macos'
31
+ 'say' = 'macos'
32
+ 'sapi' = 'windows-sapi'
33
+ }
34
+
35
+ $script:CatalogPlatform = @{
36
+ 'unix' = @('soprano','piper','kokoro','elevenlabs')
37
+ 'darwin' = @('soprano','piper','kokoro','elevenlabs','macos')
38
+ 'windows' = @('soprano','kokoro','windows-sapi','windows-piper')
39
+ }
40
+
41
+ $script:CatalogVoices = @{
42
+ 'soprano' = @('soprano-default')
43
+ 'piper' = @()
44
+ 'kokoro' = @('af_heart','af_alloy','af_aoede','af_bella','af_jessica','af_kore','af_nicole','af_nova','af_river','af_sarah','af_sky','am_adam','am_echo','am_eric','am_fenrir','am_liam','am_michael','am_onyx','am_puck','bf_alice','bf_emma','bf_isabella','bf_lily','bm_daniel','bm_fable','bm_george','bm_lewis','jf_alpha','jf_gongitsune','jf_nezumi','jf_tebukuro','jm_kumo','zf_xiaobei','zf_xiaoni','zf_xiaoxiao','zf_xiaoyi','zm_yunxi','zm_yunxia','zm_yunyang','ef_dora','em_alex','em_santa','ff_siwis','hf_alpha','hm_omega','if_sara','im_nicola','pf_dora','pm_alex','pm_santa','kf_alpha','km_hyunsu')
45
+ 'elevenlabs' = @('Sarah','Roger','Laura','Charlie','George','Callum','River','Harry','Liam','Alice','Matilda','Will','Jessica','Eric','Bella','Chris','Brian','Daniel','Lily','Adam','Bill')
46
+ 'macos' = @()
47
+ 'windows-sapi' = @()
48
+ 'windows-piper' = @()
49
+ }
50
+
51
+ $script:CatalogElevenLabs = @{
52
+ 'sarah' = 'EXAVITQu4vr4xnSDxMaL'
53
+ 'roger' = 'CwhRBWXzGAHq8TQ4Fs17'
54
+ 'laura' = 'FGY2WhTYpPnrIDTdsKH5'
55
+ 'charlie' = 'IKne3meq5aSn9XLyUdCD'
56
+ 'george' = 'JBFqnCBsd6RMkjVDRZzb'
57
+ 'callum' = 'N2lVS1w4EtoT3dr4eOWO'
58
+ 'river' = 'SAz9YHcvj6GT2YYXdXww'
59
+ 'harry' = 'SOYHLrjzK2X1ezoPC6cr'
60
+ 'liam' = 'TX3LPaxmHKxFdv7VOQHJ'
61
+ 'alice' = 'Xb7hH8MSUJpSbSDYk0k2'
62
+ 'matilda' = 'XrExE9yKIg1WjnnlVkGX'
63
+ 'will' = 'bIHbv24MWmeRgasZH58o'
64
+ 'jessica' = 'cgSgspJ2msm6clMCkdW9'
65
+ 'eric' = 'cjVigY5qzO86Huf0OWal'
66
+ 'bella' = 'hpp4J3VqNfWAUOO0d1Us'
67
+ 'chris' = 'iP95p4xoKVk53GoZ742B'
68
+ 'brian' = 'nPczCjzI2devNBz1zQrb'
69
+ 'daniel' = 'onwK4e9ZLuTAKqWW03F9'
70
+ 'lily' = 'pFZP5JQG7iQjIQuC4Bku'
71
+ 'adam' = 'pNInz6obpgDQGcFmaJgB'
72
+ 'bill' = 'pqHfZKP75CvOlQylNhV4'
73
+ }
74
+
75
+ function Get-CatalogCanonicalProvider {
76
+ param([Parameter(Mandatory)][string]$Provider)
77
+ $key = $Provider.Trim().ToLowerInvariant()
78
+ if ($script:CatalogDefaults.ContainsKey($key)) { return $key }
79
+ if ($script:CatalogAlias.ContainsKey($key)) { return $script:CatalogAlias[$key] }
80
+ return $null
81
+ }
82
+
83
+ function Get-CatalogDefaultVoice {
84
+ param([Parameter(Mandatory)][string]$Provider)
85
+ $p = Get-CatalogCanonicalProvider $Provider
86
+ if ($null -eq $p) { return $null }
87
+ return $script:CatalogDefaults[$p]
88
+ }
89
+
90
+ function Get-CatalogDisplayName {
91
+ param([Parameter(Mandatory)][string]$Provider)
92
+ $p = Get-CatalogCanonicalProvider $Provider
93
+ if ($null -eq $p) { return $Provider }
94
+ return $script:CatalogDisplayNames[$p]
95
+ }
96
+
97
+ function Get-CatalogProvidersForPlatform {
98
+ param([Parameter(Mandatory)][string]$Platform)
99
+ $key = $Platform.Trim().ToLowerInvariant()
100
+ if ($script:CatalogPlatform.ContainsKey($key)) { return $script:CatalogPlatform[$key] }
101
+ return @()
102
+ }
103
+
104
+ function Get-CatalogVoices {
105
+ param([Parameter(Mandatory)][string]$Provider)
106
+ $p = Get-CatalogCanonicalProvider $Provider
107
+ if ($null -eq $p) { return @() }
108
+ return $script:CatalogVoices[$p]
109
+ }
110
+
111
+ function Test-CatalogVoice {
112
+ param([Parameter(Mandatory)][string]$Provider, [Parameter(Mandatory)][AllowEmptyString()][string]$Voice)
113
+ $p = Get-CatalogCanonicalProvider $Provider
114
+ if ($null -eq $p) { return $null }
115
+ $lc = $Voice.Trim().ToLowerInvariant()
116
+ switch ($p) {
117
+ 'kokoro' {
118
+ if ($script:CatalogVoices['kokoro'] -contains $lc) { return $lc }
119
+ return $null
120
+ }
121
+ 'elevenlabs' {
122
+ if ($script:CatalogElevenLabs.ContainsKey($lc)) { return $script:CatalogElevenLabs[$lc] }
123
+ if ($Voice -match '^[A-Za-z0-9]{20}$') { return $Voice }
124
+ return $null
125
+ }
126
+ 'soprano' {
127
+ if ($lc -eq '' -or $lc -eq 'soprano' -or $lc -eq 'soprano-default') { return 'soprano-default' }
128
+ return $null
129
+ }
130
+ { $_ -eq 'piper' -or $_ -eq 'windows-piper' } {
131
+ if ($Voice -match '^[a-z]{2,3}_[A-Z]{2}-' -or $Voice -like '*::*') { return $Voice }
132
+ return $null
133
+ }
134
+ default {
135
+ if ($Voice -ne '') { return $Voice }
136
+ if ($p -eq 'windows-sapi') { return '' }
137
+ return $null
138
+ }
139
+ }
140
+ }