agentvibes 5.12.0 → 5.13.1

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 (73) hide show
  1. package/.agentvibes/install-manifest.json +111 -91
  2. package/.claude/commands/agent-vibes/commands.json +0 -20
  3. package/.claude/commands/agent-vibes/unmute.md +6 -2
  4. package/.claude/config/audio-effects.cfg +6 -6
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/agentvibes-session-id.sh +69 -0
  7. package/.claude/hooks/bmad-party-speak.sh +20 -4
  8. package/.claude/hooks/bmad-speak.sh +60 -2
  9. package/.claude/hooks/bmad-tts-injector.sh +20 -1
  10. package/.claude/hooks/bmad-voice-manager.sh +25 -3
  11. package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
  12. package/.claude/hooks/clawdbot-receiver.sh +19 -1
  13. package/.claude/hooks/elevenlabs-voices.sh +62 -0
  14. package/.claude/hooks/kokoro-installer.sh +20 -10
  15. package/.claude/hooks/language-manager.sh +10 -3
  16. package/.claude/hooks/party-set-room.sh +71 -0
  17. package/.claude/hooks/party-stage-roster.py +328 -0
  18. package/.claude/hooks/personality-manager.sh +19 -2
  19. package/.claude/hooks/piper-voice-manager.sh +3 -2
  20. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
  21. package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
  22. package/.claude/hooks/play-tts-kokoro.sh +33 -6
  23. package/.claude/hooks/play-tts-soprano.sh +3 -2
  24. package/.claude/hooks/play-tts-ssh-remote.sh +37 -29
  25. package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
  26. package/.claude/hooks/play-tts.sh +66 -61
  27. package/.claude/hooks/provider-catalog.json +352 -0
  28. package/.claude/hooks/provider-catalog.sh +161 -0
  29. package/.claude/hooks/provider-commands.sh +2 -1
  30. package/.claude/hooks/provider-manager.sh +47 -9
  31. package/.claude/hooks/python-resolver.sh +117 -0
  32. package/.claude/hooks/session-id.sh +56 -0
  33. package/.claude/hooks/session-start-tts.sh +39 -0
  34. package/.claude/hooks/speed-manager.sh +1 -1
  35. package/.claude/hooks/translate-manager.sh +3 -2
  36. package/.claude/hooks/translator.py +1 -1
  37. package/.claude/hooks/voice-manager.sh +242 -10
  38. package/.claude/hooks-windows/language-manager.ps1 +7 -1
  39. package/.claude/hooks-windows/personality-manager.ps1 +16 -1
  40. package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
  41. package/.claude/hooks-windows/play-tts.ps1 +32 -3
  42. package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
  43. package/.claude/hooks-windows/provider-manager.ps1 +63 -8
  44. package/.claude/hooks-windows/tts-watcher.ps1 +33 -12
  45. package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
  46. package/.mcp.json +0 -7
  47. package/README.md +12 -3
  48. package/RELEASE_NOTES.md +61 -0
  49. package/mcp-server/server.py +146 -49
  50. package/mcp-server/test_mcp_correctness.py +20 -2
  51. package/mcp-server/test_windows_script_parity.py +0 -2
  52. package/package.json +2 -2
  53. package/src/cli/list-voices.js +218 -114
  54. package/src/console/bling.js +71 -0
  55. package/src/console/music-preview.js +79 -0
  56. package/src/console/tabs/music-tab.js +16 -39
  57. package/src/console/tabs/settings-tab.js +195 -13
  58. package/src/console/tabs/setup-tab.js +9 -34
  59. package/src/console/tabs/voices-tab.js +83 -14
  60. package/src/console/widgets/track-picker.js +82 -0
  61. package/src/installer.js +136 -18
  62. package/src/services/provider-catalog.js +412 -0
  63. package/src/services/provider-voice-catalog.js +52 -73
  64. package/src/services/tts-engine-service.js +29 -0
  65. package/src/utils/provider-validator.js +62 -12
  66. package/.claude/commands/agent-vibes/language.md +0 -23
  67. package/.claude/commands/agent-vibes/learn.md +0 -67
  68. package/.claude/commands/agent-vibes/replay-target.md +0 -14
  69. package/.claude/commands/agent-vibes/target-voice.md +0 -26
  70. package/.claude/commands/agent-vibes/target.md +0 -30
  71. package/.claude/hooks/learn-manager.sh +0 -492
  72. package/.claude/hooks/replay-target-audio.sh +0 -95
  73. package/.claude/hooks-windows/learn-manager.ps1 +0 -241
@@ -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
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/party-set-room.sh
4
+ #
5
+ # AgentVibes - record the chosen (non-default) party room for this session.
6
+ #
7
+ # party-stage-roster.py broadcasts the DEFAULT installed-agent room unless it
8
+ # knows a specific room was chosen. This tiny helper is the AgentVibes-side
9
+ # mechanism for recording that choice: it writes the active-room state file
10
+ #
11
+ # ~/.agentvibes/party-active-<sessionid>.json = {"group":"<group-id>"}
12
+ #
13
+ # keyed by the canonical routing session id (av_session_id = basename of
14
+ # CLAUDE_PROJECT_DIR). party-stage-roster.py reads this file and passes the
15
+ # group to resolve_party.py as --party, so a chosen non-default room gets
16
+ # broadcast instead of the default.
17
+ #
18
+ # EXTENSION POINT (we do NOT edit the BMAD skill): a party skill can call this
19
+ # from its user-config `activation_steps_append` when the user picks a room --
20
+ # that is the sanctioned way to wire this in without touching BMAD-owned files.
21
+ #
22
+ # Non-destructive: only creates ~/.agentvibes (mkdir -p) and writes/overwrites
23
+ # the one per-session state file it owns. Touches no user config.
24
+ #
25
+ # Usage: party-set-room.sh <group-id>
26
+ # party-set-room.sh --clear # remove the state file (back to default)
27
+ #
28
+ # bash-3.2 safe (macOS default bash).
29
+
30
+ set -euo pipefail
31
+
32
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ # shellcheck source=./session-id.sh
34
+ source "$SCRIPT_DIR/session-id.sh"
35
+
36
+ GROUP="${1:-}"
37
+ if [[ -z "$GROUP" ]]; then
38
+ echo "usage: party-set-room.sh <group-id> | --clear" >&2
39
+ exit 2
40
+ fi
41
+
42
+ SESSION_ID="$(av_session_id "${CLAUDE_PROJECT_DIR:-$PWD}")"
43
+ STATE_DIR="$HOME/.agentvibes"
44
+ STATE_FILE="$STATE_DIR/party-active-${SESSION_ID}.json"
45
+
46
+ mkdir -p "$STATE_DIR"
47
+
48
+ if [[ "$GROUP" == "--clear" ]]; then
49
+ rm -f "$STATE_FILE"
50
+ # Also clear the stage-on-first-speak idempotency flag so the next party
51
+ # re-broadcasts its cast (this is the manual party-reset path).
52
+ rm -f "$STATE_DIR/staged-${SESSION_ID}.flag"
53
+ echo "party-set-room: cleared active room for session '$SESSION_ID' (back to default)."
54
+ exit 0
55
+ fi
56
+
57
+ # Validate the group id: a group id is a slug (letters/digits/._-). Reject
58
+ # anything else so we never write attacker-controlled JSON.
59
+ if [[ ! "$GROUP" =~ ^[A-Za-z0-9._-]+$ ]]; then
60
+ echo "party-set-room: invalid group id '$GROUP' (allowed: A-Za-z0-9._-)" >&2
61
+ exit 2
62
+ fi
63
+
64
+ # Write atomically so a concurrent reader never sees a half-written file.
65
+ _tmp="$(mktemp "$STATE_DIR/party-active-${SESSION_ID}.XXXXXX")" || exit 1
66
+ trap 'rm -f "$_tmp"' EXIT
67
+ printf '{"group":"%s"}\n' "$GROUP" > "$_tmp"
68
+ mv "$_tmp" "$STATE_FILE"
69
+ trap - EXIT
70
+
71
+ echo "party-set-room: session '$SESSION_ID' room set to '$GROUP'."