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
@@ -49,22 +49,6 @@
49
49
  "name": "get",
50
50
  "description": "Get personality details"
51
51
  },
52
- {
53
- "name": "language",
54
- "description": "Set main/native language"
55
- },
56
- {
57
- "name": "learn",
58
- "description": "Enable/disable language learning mode"
59
- },
60
- {
61
- "name": "target",
62
- "description": "Set target language to learn"
63
- },
64
- {
65
- "name": "target-voice",
66
- "description": "Set voice for target language"
67
- },
68
52
  {
69
53
  "name": "set-speed",
70
54
  "description": "Set speech speed for Piper TTS voices"
@@ -93,10 +77,6 @@
93
77
  "name": "unmute",
94
78
  "description": "Unmute AgentVibes TTS output"
95
79
  },
96
- {
97
- "name": "replay-target",
98
- "description": "Replay the last target language audio"
99
- },
100
80
  {
101
81
  "name": "translate",
102
82
  "description": "Configure automatic TTS translation"
@@ -37,9 +37,13 @@ fi
37
37
 
38
38
  **Advanced Options:**
39
39
 
40
- To unmute globally (removes global mute AND project mute):
40
+ To unmute globally this deliberately opts EVERY session into the TTS protocol
41
+ (removes global mute AND drops a global opt-in marker the session-start hook
42
+ checks). Use with care: with a global install this makes all open sessions speak.
41
43
  ```bash
42
44
  rm -f "$HOME/.agentvibes-muted"
43
45
  rm -f "$(pwd)/.claude/agentvibes-muted" 2>/dev/null || true
44
- echo "🔊 **AgentVibes TTS unmuted globally.** Voice output restored for all projects."
46
+ mkdir -p "$HOME/.claude"
47
+ touch "$HOME/.claude/agentvibes-unmuted" # global opt-in for the injection gate
48
+ echo "🔊 **AgentVibes TTS unmuted globally.** Every session will now announce. Run /agent-vibes:mute to stop."
45
49
  ```
@@ -1,7 +1,7 @@
1
+ default||agent_vibes_arabic_v2_loop.mp3|0.30
1
2
 
2
- llm:default|light||0.15|en_US-lessac-high||piper
3
- llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.05|en_US-amy-medium|Agent Vibes local here|piper
4
- llm:copilot|light|agent_vibes_bossa_nova_v2_loop.mp3|0.15|en_US-libritts-high::Anna-11|Copilot here|piper
5
- llm:codex|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Codex here|piper
6
- llm:hermes|light|agent_vibes_bachata_v1_loop.mp3|0.15|en_US-libritts-high::Leo-8|Hermes here|piper
7
- default||agent_vibes_arabic_v2_loop.mp3|0.30
3
+ llm:default|light||0.20|en_US-lessac-high||piper
4
+ llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.20|en_US-lessac-high||piper
5
+ llm:copilot|light|agent_vibes_bossa_nova_v2_loop.mp3|0.20|en_US-libritts-high::Anna-11||piper
6
+ llm:codex|light|agent_vibes_chillwave_v2_loop.mp3|0.20|en_US-lessac-high||piper
7
+ llm:hermes|light|agent_vibes_bachata_v1_loop.mp3|0.20|en_US-libritts-high::Leo-8||piper
@@ -1 +1 @@
1
- 20260703
1
+ 20260716
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/agentvibes-session-id.sh
4
+ #
5
+ # AgentVibes - Text-to-Speech WITH personality for AI Assistants
6
+ # Website: https://agentvibes.org
7
+ # Repository: https://github.com/paulpreibisch/AgentVibes
8
+ #
9
+ # Licensed under the Apache License, Version 2.0
10
+ #
11
+ # @fileoverview Print a short spoken self-identifier for THIS session, e.g.
12
+ # "Claude on AgentVibes in Ghostty"
13
+ # @why With several agent sessions open at once, every one saying the same thing
14
+ # makes it impossible to tell which tab just spoke. Prefixing each session's
15
+ # speech with who+where answers "which one is that?" (community request).
16
+ # @usage agentvibes-session-id.sh <llm-key> <project-dir>
17
+ # Also used via the {{session}} token in a per-LLM PRETEXT (see play-tts.sh).
18
+ #
19
+ set -uo pipefail
20
+ export LC_ALL=C
21
+
22
+ LLM_KEY="${1:-claude-code}"
23
+ PROJ_DIR="${2:-$PWD}"
24
+
25
+ # LLM display name
26
+ case "$LLM_KEY" in
27
+ claude-code|claude*) LLM_NAME="Claude" ;;
28
+ gemini*) LLM_NAME="Gemini" ;;
29
+ codex|openai*|gpt*) LLM_NAME="Codex" ;;
30
+ cursor*) LLM_NAME="Cursor" ;;
31
+ default) LLM_NAME="Agent" ;;
32
+ *) LLM_NAME="$LLM_KEY" ;;
33
+ esac
34
+
35
+ # Project name = basename of the project dir
36
+ PROJ_NAME="$(basename "$PROJ_DIR" 2>/dev/null || true)"
37
+ [[ -z "$PROJ_NAME" || "$PROJ_NAME" == "/" || "$PROJ_NAME" == "." ]] && PROJ_NAME="this project"
38
+
39
+ # Terminal detection — prefer the most specific signal available.
40
+ TERM_NAME=""
41
+ if [[ -n "${WT_SESSION:-}" ]]; then
42
+ TERM_NAME="Windows Terminal"
43
+ elif [[ -n "${TERM_PROGRAM:-}" ]]; then
44
+ case "$TERM_PROGRAM" in
45
+ vscode) TERM_NAME="VS Code" ;;
46
+ iTerm.app) TERM_NAME="iTerm" ;;
47
+ Apple_Terminal) TERM_NAME="Terminal" ;;
48
+ ghostty|Ghostty) TERM_NAME="Ghostty" ;;
49
+ WezTerm) TERM_NAME="WezTerm" ;;
50
+ Hyper) TERM_NAME="Hyper" ;;
51
+ tmux) TERM_NAME="tmux" ;;
52
+ Tabby) TERM_NAME="Tabby" ;;
53
+ *) TERM_NAME="$TERM_PROGRAM" ;;
54
+ esac
55
+ elif [[ -n "${TERMINAL_EMULATOR:-}" ]]; then
56
+ TERM_NAME="$TERMINAL_EMULATOR"
57
+ elif [[ -n "${KITTY_WINDOW_ID:-}" ]]; then
58
+ TERM_NAME="kitty"
59
+ elif [[ -n "${ALACRITTY_WINDOW_ID:-}" ]]; then
60
+ TERM_NAME="Alacritty"
61
+ elif [[ -n "${TMUX:-}" ]]; then
62
+ TERM_NAME="tmux"
63
+ fi
64
+
65
+ if [[ -n "$TERM_NAME" ]]; then
66
+ printf '%s on %s in %s' "$LLM_NAME" "$PROJ_NAME" "$TERM_NAME"
67
+ else
68
+ printf '%s on %s' "$LLM_NAME" "$PROJ_NAME"
69
+ fi
@@ -18,6 +18,11 @@
18
18
  set -euo pipefail
19
19
 
20
20
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ source "$SCRIPT_DIR/python-resolver.sh"
22
+ # Party speech is best-effort and this hook runs under `set -e`; with no Python
23
+ # the `"$PYTHON_BIN" …` calls below would be `: command not found` (127) and abort
24
+ # the hook, surfacing an error on every Agent call. Degrade silently instead.
25
+ [[ -n "$PYTHON_BIN" ]] || exit 0
21
26
  LOCK_FILE="/tmp/agentvibes-party-queue.lock"
22
27
  DEBUG_LOG="/tmp/agentvibes-party-debug.log"
23
28
 
@@ -33,7 +38,7 @@ _dbg "fired (stdin ${#raw} bytes)"
33
38
 
34
39
  # --- Parse all needed fields in one python3 call (fixes M5: 3x subprocess, echo safety) ---
35
40
  # Outputs: TOOL_NAME|DISPLAY_NAME|RESPONSE_TEXT (newlines in response encoded as \n literals)
36
- parsed="$(printf '%s' "$raw" | python3 - <<'PYEOF'
41
+ parsed="$(printf '%s' "$raw" | "$PYTHON_BIN" - <<'PYEOF'
37
42
  import sys, json, re
38
43
 
39
44
  try:
@@ -92,6 +97,12 @@ if [[ "$raw" != *"BMAD agent in a collaborative roundtable"* ]]; then
92
97
  fi
93
98
  _dbg "fingerprint HIT: display='$display_name' text_len=${#response_text}"
94
99
 
100
+ # Party marker (Phase 2): this hook alone knows -- from the roundtable
101
+ # fingerprint -- that we are in party mode. Export the marker so the child
102
+ # bmad-speak.sh can stage the cast on the first party line (stage-on-first-speak)
103
+ # without having to re-derive party context. Additive + harmless outside party.
104
+ export AGENTVIBES_PARTY_MODE=1
105
+
95
106
  if [[ -z "$display_name" ]]; then
96
107
  _dbg "skip: empty display_name"
97
108
  exit 0
@@ -104,6 +115,11 @@ fi
104
115
  # --- Resolve project root ---
105
116
  project_root="${CLAUDE_PROJECT_DIR:-}"
106
117
 
118
+ # Thread the real project dir to the child speak path so its forwarded messages
119
+ # carry the correct routing session id (not the install/HOME basename). Export
120
+ # only when known so the no-project case is unchanged.
121
+ [[ -n "$project_root" ]] && export CLAUDE_PROJECT_DIR="$project_root"
122
+
107
123
  # --- Find bmad-speak.sh (prefer project-local, fall back to global) ---
108
124
  bmad_speak=""
109
125
  if [[ -n "$project_root" && -f "$project_root/.claude/hooks/bmad-speak.sh" ]]; then
@@ -117,7 +133,7 @@ fi
117
133
  agent_id="$display_name" # fallback
118
134
  if [[ -n "$project_root" && -f "$project_root/_bmad/_config/agent-manifest.csv" ]]; then
119
135
  manifest="$project_root/_bmad/_config/agent-manifest.csv"
120
- matched="$(python3 - "$manifest" "$display_name" <<'PYEOF'
136
+ matched="$("$PYTHON_BIN" - "$manifest" "$display_name" <<'PYEOF'
121
137
  import sys, csv
122
138
  manifest_path, target = sys.argv[1], sys.argv[2].lower()
123
139
  try:
@@ -147,7 +163,7 @@ fi
147
163
  case "$verbosity" in
148
164
  low)
149
165
  # First sentence — fall back to full text if no punctuation (fixes m1)
150
- first="$(printf '%s' "$response_text" | python3 -c "
166
+ first="$(printf '%s' "$response_text" | "$PYTHON_BIN" -c "
151
167
  import sys, re
152
168
  t = sys.stdin.read()
153
169
  m = re.match(r'^.*?[.!?]', t)
@@ -157,7 +173,7 @@ print(m.group(0) if m else t)
157
173
  ;;
158
174
  medium)
159
175
  # First 2 sentences — fall back to full text if no punctuation (fixes m1)
160
- two="$(printf '%s' "$response_text" | python3 -c "
176
+ two="$(printf '%s' "$response_text" | "$PYTHON_BIN" -c "
161
177
  import sys, re
162
178
  t = sys.stdin.read()
163
179
  parts = re.findall(r'.*?[.!?]', t)
@@ -49,6 +49,56 @@ if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
49
49
  exit 0
50
50
  fi
51
51
 
52
+ # ---------------------------------------------------------------------------
53
+ # Stage-on-first-speak: the FIRST party line for a session notifies a compatible
54
+ # receiver of the full party roster up front, rather than letting it learn the
55
+ # cast one line at a time. Everything here is additive + fail-safe: if python is
56
+ # missing or the notification errors, the line still speaks.
57
+ #
58
+ # Party context is detected via either signal:
59
+ # * AGENTVIBES_PARTY_MODE=1 -- the "party marker" bmad-party-speak.sh exports
60
+ # before it invokes this script (it alone knows the roundtable fingerprint).
61
+ # * the routing session id ends in "-bmad-party-mode" (the suffix the party
62
+ # stages under), for any flow that runs under that session directly.
63
+ #
64
+ # Idempotency: a per-session flag ~/.agentvibes/staged-<sessionid>.flag makes the
65
+ # doorbell fire ONCE per party, not once per line.
66
+ #
67
+ # Clearing the flag: there is no per-line clear (that would re-fire every line).
68
+ # `party-set-room.sh --clear` removes it (manual reset / room change), and a
69
+ # party-end / clear hook should remove it too. TODO: wire an automatic clear to
70
+ # a party-teardown hook if/when BMAD exposes one -- until then the flag persists
71
+ # for the life of the session, which is the correct once-per-party behavior.
72
+ if [[ "${AGENTVIBES_STAGE_ROSTER_DISABLED:-}" != "1" ]]; then
73
+ _sr_session=""
74
+ if [[ -f "$SCRIPT_DIR/session-id.sh" ]]; then
75
+ # shellcheck source=./session-id.sh
76
+ source "$SCRIPT_DIR/session-id.sh"
77
+ _sr_session="$(av_session_id "${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}")"
78
+ fi
79
+ if [[ "${AGENTVIBES_PARTY_MODE:-}" == "1" || "$_sr_session" == *-bmad-party-mode ]]; then
80
+ _sr_flag="$HOME/.agentvibes/staged-${_sr_session:-unknown}.flag"
81
+ if [[ -n "$_sr_session" && ! -f "$_sr_flag" ]]; then
82
+ # Claim the flag FIRST (atomic-ish) so parallel party lines don't each fire.
83
+ mkdir -p "$HOME/.agentvibes" 2>/dev/null || true
84
+ if ( set -o noclobber; : > "$_sr_flag" ) 2>/dev/null; then
85
+ _sr_python=""
86
+ if [[ -f "$SCRIPT_DIR/python-resolver.sh" ]]; then
87
+ # shellcheck source=./python-resolver.sh
88
+ source "$SCRIPT_DIR/python-resolver.sh"
89
+ _sr_python="${PYTHON_BIN:-}"
90
+ fi
91
+ if [[ -n "$_sr_python" && -f "$SCRIPT_DIR/party-stage-roster.py" ]]; then
92
+ # Fire-and-forget in the background; NEVER block or fail the line.
93
+ ( "$_sr_python" "$SCRIPT_DIR/party-stage-roster.py" \
94
+ --project-root "${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}" \
95
+ --session-suffix bmad-party-mode >/dev/null 2>&1 || true ) &
96
+ fi
97
+ fi
98
+ fi
99
+ fi
100
+ fi
101
+
52
102
  # ---------------------------------------------------------------------------
53
103
  # Per-agent profile reader — reads from project .agentvibes/bmad-voice-map.json (falls back to global)
54
104
  # Uses node for reliable JSON parsing (jq may not be installed)
@@ -289,10 +339,18 @@ trap 'rmdir "$SPEECH_LOCK" 2>/dev/null' EXIT
289
339
  # has an empty TEMP_PROFILE, so the arg-3 heuristic in play-tts.sh alone would
290
340
  # miss it. AGENT_VOICE is an agent-profile voice, always.
291
341
  export AGENTVIBES_VOICE_SOURCE="agent-profile"
342
+ # Thread the real project dir through to play-tts.sh so any downstream forward
343
+ # (SSH-remote / avatar) derives the correct routing session id from the user's
344
+ # project — NOT the install/HOME basename. Only add the flag when the var is
345
+ # non-empty so the no-project case is unchanged. Mirrors session-start-tts.sh.
346
+ _PT_PROJECT_FLAG=()
347
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
348
+ _PT_PROJECT_FLAG=(--project-dir "$CLAUDE_PROJECT_DIR")
349
+ fi
292
350
  if [[ -n "$AGENT_VOICE" ]]; then
293
- bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "$AGENT_VOICE" "$TEMP_PROFILE"
351
+ bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "$AGENT_VOICE" "$TEMP_PROFILE" "${_PT_PROJECT_FLAG[@]+"${_PT_PROJECT_FLAG[@]}"}"
294
352
  else
295
- bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "" "$TEMP_PROFILE"
353
+ bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "" "$TEMP_PROFILE" "${_PT_PROJECT_FLAG[@]+"${_PT_PROJECT_FLAG[@]}"}"
296
354
  fi
297
355
 
298
356
  # Release lock
@@ -40,6 +40,25 @@ set -e # Exit on error
40
40
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
41
41
  CLAUDE_DIR="$(dirname "$SCRIPT_DIR")"
42
42
 
43
+ # @function _piper_default_voice
44
+ # @intent Return the Provider Catalog's piper default voice (AVI-S9.6 AC2,
45
+ # design row 22), falling back to the legacy hardcoded literal when the
46
+ # generated .claude/hooks/provider-catalog.sh artifact is missing
47
+ # (installed-tree skew — mirrors the fail-safe probe pattern already
48
+ # used by voice-manager.sh / provider-manager.sh).
49
+ # @returns Echoes the default Piper voice id.
50
+ _piper_default_voice() {
51
+ if [[ -f "$SCRIPT_DIR/provider-catalog.sh" ]]; then
52
+ # shellcheck source=/dev/null
53
+ source "$SCRIPT_DIR/provider-catalog.sh" 2>/dev/null || true
54
+ fi
55
+ if type catalog_default_voice >/dev/null 2>&1; then
56
+ catalog_default_voice piper
57
+ else
58
+ echo "en_US-lessac-medium" # legacy fallback if the catalog artifact is missing
59
+ fi
60
+ }
61
+
43
62
  # Colors for output
44
63
  GREEN='\033[0;32m'
45
64
  YELLOW='\033[1;33m'
@@ -193,7 +212,7 @@ map_voice_to_provider() {
193
212
  # Map legacy voice names to Piper equivalents
194
213
  case "$voice" in
195
214
  "Jessica Anne Bogart"|"Aria")
196
- echo "en_US-lessac-medium"
215
+ _piper_default_voice
197
216
  ;;
198
217
  "Matthew Schmitz"|"Archer"|"Michael")
199
218
  echo "en_US-danny-low"
@@ -43,6 +43,27 @@ CONFIG_DIR=".agentvibes/bmad"
43
43
  VOICE_CONFIG_FILE="$CONFIG_DIR/bmad-voices.md"
44
44
  ENABLED_FLAG="$CONFIG_DIR/bmad-voices-enabled.flag"
45
45
 
46
+ # @function _piper_default_voice
47
+ # @intent Return the Provider Catalog's piper default voice (AVI-S9.6 AC2,
48
+ # design row 22), falling back to the legacy hardcoded literal when the
49
+ # generated .claude/hooks/provider-catalog.sh artifact is missing
50
+ # (installed-tree skew — mirrors the fail-safe probe pattern already
51
+ # used by voice-manager.sh / provider-manager.sh).
52
+ # @returns Echoes the default Piper voice id.
53
+ _piper_default_voice() {
54
+ local _script_dir
55
+ _script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
56
+ if [[ -f "$_script_dir/provider-catalog.sh" ]]; then
57
+ # shellcheck source=/dev/null
58
+ source "$_script_dir/provider-catalog.sh" 2>/dev/null || true
59
+ fi
60
+ if type catalog_default_voice >/dev/null 2>&1; then
61
+ catalog_default_voice piper
62
+ else
63
+ echo "en_US-lessac-medium" # legacy fallback if the catalog artifact is missing
64
+ fi
65
+ }
66
+
46
67
  # AI NOTE: Auto-enable pattern - When BMAD is detected via install-manifest.yaml,
47
68
  # automatically enable the voice plugin to provide seamless multi-agent voice support.
48
69
  # This avoids requiring manual plugin activation after BMAD installation.
@@ -160,8 +181,9 @@ get_agent_voice() {
160
181
  # CSV format: agent_id,voice_name
161
182
  local voice=$(grep "^$agent_id," "$bmad_voice_map" | cut -d',' -f2)
162
183
 
163
- # If voice is empty or generic (same for all), use defaults
164
- if [[ -n "$voice" ]] && [[ "$voice" != "en_US-lessac-medium" ]]; then
184
+ # If voice is empty or generic (same for all), use defaults. "Generic"
185
+ # means the CSV still carries the catalog's piper default (AVI-S9.6 AC2).
186
+ if [[ -n "$voice" ]] && [[ "$voice" != "$(_piper_default_voice)" ]]; then
165
187
  echo "$voice"
166
188
  return
167
189
  fi
@@ -172,7 +194,7 @@ get_agent_voice() {
172
194
  # These match the BMAD-METHOD defaults for consistency
173
195
  case "$agent_id" in
174
196
  bmad-master)
175
- echo "en_US-lessac-medium"
197
+ _piper_default_voice
176
198
  return
177
199
  ;;
178
200
  analyst)
@@ -22,11 +22,30 @@
22
22
  set -euo pipefail
23
23
 
24
24
  ENCODED_TEXT="${1:-}"
25
- VOICE="${2:-en_US-lessac-medium}"
25
+
26
+ # Default voice: prefer the Provider Catalog's piper default (AVI-S9.6 AC3,
27
+ # design row 23), falling back to the legacy literal if the generated catalog
28
+ # artifact is missing (installed-tree skew).
29
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ if [[ -f "$SCRIPT_DIR/provider-catalog.sh" ]]; then
31
+ # shellcheck source=/dev/null
32
+ source "$SCRIPT_DIR/provider-catalog.sh" 2>/dev/null || true
33
+ fi
34
+ if type catalog_default_voice >/dev/null 2>&1; then
35
+ _CATALOG_PIPER_DEFAULT="$(catalog_default_voice piper)"
36
+ else
37
+ _CATALOG_PIPER_DEFAULT="en_US-lessac-medium" # legacy fallback
38
+ fi
39
+ VOICE="${2:-$_CATALOG_PIPER_DEFAULT}"
26
40
  ENCODED_AGENT="${3:-}"
27
41
  ENCODED_INTRO="${4:-}"
28
42
 
29
- # SECURITY: Whitelist of allowed voice names
43
+ # SECURITY: Whitelist of allowed voice names.
44
+ # DO NOT UNIFY with the Provider Catalog — this is a SECURITY control, not an
45
+ # inventory list. It is deliberately narrower than (and independent of) the
46
+ # catalog's piper voice set; syncing it to the catalog would silently widen
47
+ # what an untrusted remote SSH sender can request (design §8, provider-catalog.js
48
+ # module header).
30
49
  ALLOWED_VOICES="en_US-amy-medium|en_US-lessac-medium|es_ES-mls_9972-low|es_ES-davefx-medium|en_US-joe-medium"
31
50
 
32
51
  # Validate inputs
@@ -24,7 +24,25 @@
24
24
  set -euo pipefail
25
25
 
26
26
  ENCODED_TEXT="${1:-}"
27
- VOICE="${2:-en_US-lessac-medium}"
27
+
28
+ # Default voice: prefer the Provider Catalog's piper default (AVI-S9.6 AC3,
29
+ # design row 23), falling back to the legacy literal if the generated catalog
30
+ # artifact is missing (installed-tree skew). NOTE: this receiver has no voice
31
+ # allowlist of its own — see clawdbot-receiver-SECURE.sh for the hardened
32
+ # variant, whose ALLOWED_VOICES security allowlist is intentionally NOT
33
+ # migrated to (or synced with) the catalog (a security allowlist must stay
34
+ # independently narrow — see provider-catalog.js module header).
35
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ if [[ -f "$SCRIPT_DIR/provider-catalog.sh" ]]; then
37
+ # shellcheck source=/dev/null
38
+ source "$SCRIPT_DIR/provider-catalog.sh" 2>/dev/null || true
39
+ fi
40
+ if type catalog_default_voice >/dev/null 2>&1; then
41
+ _CATALOG_PIPER_DEFAULT="$(catalog_default_voice piper)"
42
+ else
43
+ _CATALOG_PIPER_DEFAULT="en_US-lessac-medium" # legacy fallback
44
+ fi
45
+ VOICE="${2:-$_CATALOG_PIPER_DEFAULT}"
28
46
  ENCODED_AGENT="${3:-}"
29
47
  ENCODED_INTRO="${4:-}"
30
48
 
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # elevenlabs-voices.sh — GENERATED FILE. DO NOT EDIT.
4
+ # DO NOT EDIT — generated from src/services/provider-catalog.js
5
+ # Regenerate with: node scripts/generate-provider-catalog.mjs
6
+ # content-hash: sha256:5e717024f1250a8f54d07291256e532606dba7b4a15ab9589305d1af6bba7f35
7
+ #
8
+ # SINGLE SOURCE OF TRUTH for the ElevenLabs voice catalog on the shell side.
9
+ # Source it, do not execute it. Consumed by play-tts-elevenlabs.sh and
10
+ # voice-manager.sh. These are the ElevenLabs DEFAULT-LIBRARY voices — available to
11
+ # every API key (community/library voices that must be added to an account first are
12
+ # intentionally excluded so a switch can't fail at synth time).
13
+
14
+ # Guard against double-sourcing.
15
+ [ -n "${_AGENTVIBES_ELEVENLABS_VOICES_LOADED:-}" ] && return 0
16
+ _AGENTVIBES_ELEVENLABS_VOICES_LOADED=1
17
+
18
+ # Default voice used when none is configured.
19
+ ELEVENLABS_DEFAULT_VOICE="Sarah"
20
+
21
+ # Print the catalog voice names, one per line (bash-3.2-safe replacement for the
22
+ # old associative-array key expansion "${!ELEVENLABS_VOICE_IDS[@]}").
23
+ elevenlabs_voice_names() {
24
+ printf '%s\n' 'Sarah' 'Roger' 'Laura' 'Charlie' 'George' 'Callum' 'River' 'Harry' 'Liam' 'Alice' 'Matilda' 'Will' 'Jessica' 'Eric' 'Bella' 'Chris' 'Brian' 'Daniel' 'Lily' 'Adam' 'Bill'
25
+ }
26
+
27
+ # Resolve a user-supplied name (case-insensitive) OR a raw voice_id to a canonical
28
+ # voice_id. Echoes the id and returns 0 on success; returns 1 if the input matches
29
+ # no catalog name and is not a raw-id shape. bash-3.2-safe: case statement + tr fold
30
+ # (no associative arrays, no case-modifying expansions).
31
+ elevenlabs_resolve_voice() {
32
+ local query lc
33
+ query="${1:-}"
34
+ [ -z "$query" ] && return 1
35
+ lc="$(printf '%s' "$query" | tr '[:upper:]' '[:lower:]')"
36
+ case "$lc" in
37
+ 'sarah') printf '%s' 'EXAVITQu4vr4xnSDxMaL'; return 0 ;;
38
+ 'roger') printf '%s' 'CwhRBWXzGAHq8TQ4Fs17'; return 0 ;;
39
+ 'laura') printf '%s' 'FGY2WhTYpPnrIDTdsKH5'; return 0 ;;
40
+ 'charlie') printf '%s' 'IKne3meq5aSn9XLyUdCD'; return 0 ;;
41
+ 'george') printf '%s' 'JBFqnCBsd6RMkjVDRZzb'; return 0 ;;
42
+ 'callum') printf '%s' 'N2lVS1w4EtoT3dr4eOWO'; return 0 ;;
43
+ 'river') printf '%s' 'SAz9YHcvj6GT2YYXdXww'; return 0 ;;
44
+ 'harry') printf '%s' 'SOYHLrjzK2X1ezoPC6cr'; return 0 ;;
45
+ 'liam') printf '%s' 'TX3LPaxmHKxFdv7VOQHJ'; return 0 ;;
46
+ 'alice') printf '%s' 'Xb7hH8MSUJpSbSDYk0k2'; return 0 ;;
47
+ 'matilda') printf '%s' 'XrExE9yKIg1WjnnlVkGX'; return 0 ;;
48
+ 'will') printf '%s' 'bIHbv24MWmeRgasZH58o'; return 0 ;;
49
+ 'jessica') printf '%s' 'cgSgspJ2msm6clMCkdW9'; return 0 ;;
50
+ 'eric') printf '%s' 'cjVigY5qzO86Huf0OWal'; return 0 ;;
51
+ 'bella') printf '%s' 'hpp4J3VqNfWAUOO0d1Us'; return 0 ;;
52
+ 'chris') printf '%s' 'iP95p4xoKVk53GoZ742B'; return 0 ;;
53
+ 'brian') printf '%s' 'nPczCjzI2devNBz1zQrb'; return 0 ;;
54
+ 'daniel') printf '%s' 'onwK4e9ZLuTAKqWW03F9'; return 0 ;;
55
+ 'lily') printf '%s' 'pFZP5JQG7iQjIQuC4Bku'; return 0 ;;
56
+ 'adam') printf '%s' 'pNInz6obpgDQGcFmaJgB'; return 0 ;;
57
+ 'bill') printf '%s' 'pqHfZKP75CvOlQylNhV4'; return 0 ;;
58
+ esac
59
+ # Already a raw ElevenLabs voice_id.
60
+ [[ "$query" =~ ^[A-Za-z0-9]{20}$ ]] && { printf '%s' "$query"; return 0; }
61
+ return 1
62
+ }
@@ -17,6 +17,10 @@
17
17
  set -euo pipefail
18
18
  export LC_ALL=C
19
19
 
20
+ # Resolve a working Python interpreter (Windows git-bash often has none on PATH).
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ source "$SCRIPT_DIR/python-resolver.sh"
23
+
20
24
  NON_INTERACTIVE=false
21
25
  CHECK_ONLY=false
22
26
 
@@ -32,9 +36,10 @@ REQUIRED_PACKAGES=("kokoro" "soundfile" "numpy")
32
36
  # ---------------------------------------------------------------------------
33
37
  # Check current installation state (use find_spec to avoid slow torch import)
34
38
  check_kokoro() {
35
- python3 -c "import importlib.util; exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>/dev/null && \
36
- python3 -c "import importlib.util; exit(0 if importlib.util.find_spec('soundfile') else 1)" 2>/dev/null && \
37
- python3 -c "import importlib.util; exit(0 if importlib.util.find_spec('numpy') else 1)" 2>/dev/null
39
+ [[ -n "$PYTHON_BIN" ]] || return 1
40
+ "$PYTHON_BIN" -c "import importlib.util; exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>/dev/null && \
41
+ "$PYTHON_BIN" -c "import importlib.util; exit(0 if importlib.util.find_spec('soundfile') else 1)" 2>/dev/null && \
42
+ "$PYTHON_BIN" -c "import importlib.util; exit(0 if importlib.util.find_spec('numpy') else 1)" 2>/dev/null
38
43
  }
39
44
 
40
45
  if check_kokoro; then
@@ -70,10 +75,13 @@ if [[ "$NON_INTERACTIVE" != true ]]; then
70
75
  fi
71
76
  fi
72
77
 
73
- # Check Python is available
74
- if ! command -v python3 &>/dev/null; then
75
- echo "❌ Python 3 is required but not found in PATH" >&2
76
- echo " Install with: sudo apt install python3 python3-pip" >&2
78
+ # Check Python is available (resolver handles Windows/no-PATH cases)
79
+ if [[ -z "$PYTHON_BIN" ]]; then
80
+ echo "❌ Python 3 is required but no interpreter was found" >&2
81
+ echo " Linux: sudo apt install python3 python3-pip" >&2
82
+ echo " macOS: brew install python3" >&2
83
+ echo " Windows: install from https://python.org (tick 'Add to PATH')" >&2
84
+ echo " Or set AGENTVIBES_PYTHON=/path/to/python if installed elsewhere." >&2
77
85
  exit 1
78
86
  fi
79
87
 
@@ -84,12 +92,14 @@ echo ""
84
92
  # Try pip install with PEP 668 fallback to pipx/user install
85
93
  _install_pkgs() {
86
94
  local pkgs=("$@")
95
+ # Prefer the resolved interpreter's own pip so packages land where the check
96
+ # (and the synth) will actually import them from.
97
+ if "$PYTHON_BIN" -m pip install "${pkgs[@]}" 2>/dev/null; then return 0; fi
98
+ if "$PYTHON_BIN" -m pip install --user "${pkgs[@]}" 2>/dev/null; then return 0; fi
87
99
  if pip install "${pkgs[@]}" 2>/dev/null; then return 0; fi
88
100
  if pip install --user "${pkgs[@]}" 2>/dev/null; then return 0; fi
89
101
  if pip3 install "${pkgs[@]}" 2>/dev/null; then return 0; fi
90
102
  if pip3 install --user "${pkgs[@]}" 2>/dev/null; then return 0; fi
91
- if python3 -m pip install "${pkgs[@]}" 2>/dev/null; then return 0; fi
92
- if python3 -m pip install --user "${pkgs[@]}" 2>/dev/null; then return 0; fi
93
103
  return 1
94
104
  }
95
105
 
@@ -107,7 +117,7 @@ if _install_pkgs "${REQUIRED_PACKAGES[@]}"; then
107
117
  echo "Switch to Kokoro: /agent-vibes:provider switch kokoro"
108
118
  else
109
119
  echo "⚠️ Packages installed but import check failed." >&2
110
- echo " Try: python3 -c 'import kokoro'" >&2
120
+ echo " Try: \"$PYTHON_BIN\" -c 'import kokoro'" >&2
111
121
  exit 1
112
122
  fi
113
123
  else
@@ -32,7 +32,7 @@
32
32
  # @dependencies provider-manager.sh for active provider detection, .claude/tts-language.txt for state
33
33
  # @entrypoints Called by /agent-vibes:language commands, play-tts-*.sh for voice resolution
34
34
  # @patterns Provider abstraction, language-to-voice mapping, backward compatibility with legacy LANGUAGE_VOICES
35
- # @related play-tts-piper.sh, provider-manager.sh, learn-manager.sh
35
+ # @related play-tts-piper.sh, provider-manager.sh
36
36
 
37
37
  # Determine target .claude directory based on context
38
38
  # Priority:
@@ -215,11 +215,18 @@ get_language_code() {
215
215
  }
216
216
 
217
217
  # Function to check if current voice supports language
218
+ # AVI-S9.6 AC1: this list previously included "Antoni", "Rachel", "Domi",
219
+ # "Charlotte" — none of which exist in the 21-voice ElevenLabs catalog
220
+ # (src/services/provider-catalog.js ELEVENLABS_VOICES), the same class of bug
221
+ # design row 21 flagged in the now-deleted learn-manager.sh. Trimmed to names
222
+ # that ARE in the catalog; test/unit/provider-catalog-conformance.test.js
223
+ # parity-asserts every entry here against the catalog so a future addition of
224
+ # a non-existent name fails the suite instead of shipping silently.
218
225
  is_voice_multilingual() {
219
226
  local voice="$1"
220
227
 
221
- # List of multilingual voices
222
- local multilingual_voices=("Antoni" "Rachel" "Domi" "Bella" "Charlotte" "Matilda")
228
+ # List of multilingual voices (must all exist in the ElevenLabs catalog)
229
+ local multilingual_voices=("Bella" "Matilda")
223
230
 
224
231
  for mv in "${multilingual_voices[@]}"; do
225
232
  if [[ "$voice" == "$mv" ]]; then