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
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # provider-catalog.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
+ # Bash-3.2-safe catalog accessors (macOS ships bash 3.2 — no associative
9
+ # arrays, no case-modifying parameter expansions). Source this file; do not execute it.
10
+
11
+ AGENTVIBES_CATALOG_VERSION="5e717024f1250a8f"
12
+
13
+ # Guard against double-sourcing.
14
+ [ -n "${_AGENTVIBES_PROVIDER_CATALOG_LOADED:-}" ] && return 0
15
+ _AGENTVIBES_PROVIDER_CATALOG_LOADED=1
16
+
17
+ # Lowercase a string without case-modifying expansions (unavailable in bash 3.2).
18
+ _catalog_lc() { printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]'; }
19
+
20
+ # Resolve an id-or-alias to its canonical provider id (echoes it; 1 if unknown).
21
+ catalog_canonical_provider() {
22
+ case "$(_catalog_lc "${1:-}")" in
23
+ soprano) printf '%s' 'soprano' ;;
24
+ piper) printf '%s' 'piper' ;;
25
+ kokoro) printf '%s' 'kokoro' ;;
26
+ elevenlabs) printf '%s' 'elevenlabs' ;;
27
+ macos|macos-say|say) printf '%s' 'macos' ;;
28
+ windows-sapi|sapi) printf '%s' 'windows-sapi' ;;
29
+ windows-piper) printf '%s' 'windows-piper' ;;
30
+ *) return 1 ;;
31
+ esac
32
+ }
33
+
34
+ # Echo the default voice for a provider (empty string is valid — system default).
35
+ catalog_default_voice() {
36
+ local _p; _p="$(catalog_canonical_provider "${1:-}")" || return 1
37
+ case "$_p" in
38
+ soprano) printf '%s' 'soprano-default' ;;
39
+ piper) printf '%s' 'en_US-lessac-medium' ;;
40
+ kokoro) printf '%s' 'af_heart' ;;
41
+ elevenlabs) printf '%s' 'Sarah' ;;
42
+ macos) printf '%s' 'Samantha' ;;
43
+ windows-sapi) printf '%s' '' ;;
44
+ windows-piper) printf '%s' 'en_US-lessac-medium' ;;
45
+ esac
46
+ }
47
+
48
+ # Echo a human display name for a provider.
49
+ catalog_display_name() {
50
+ local _p; _p="$(catalog_canonical_provider "${1:-}")" || { printf '%s' "${1:-}"; return 0; }
51
+ case "$_p" in
52
+ soprano) printf '%s' 'Soprano TTS' ;;
53
+ piper) printf '%s' 'Piper TTS' ;;
54
+ kokoro) printf '%s' 'Kokoro TTS' ;;
55
+ elevenlabs) printf '%s' 'ElevenLabs' ;;
56
+ macos) printf '%s' 'macOS Say' ;;
57
+ windows-sapi) printf '%s' 'Windows SAPI' ;;
58
+ windows-piper) printf '%s' 'Piper TTS' ;;
59
+ esac
60
+ }
61
+
62
+ # Print the provider ids playable on a platform (unix|darwin|windows), one per line.
63
+ catalog_providers_for_platform() {
64
+ case "$(_catalog_lc "${1:-}")" in
65
+ unix)
66
+ printf '%s\n' 'soprano' 'piper' 'kokoro' 'elevenlabs'
67
+ ;;
68
+ darwin)
69
+ printf '%s\n' 'soprano' 'piper' 'kokoro' 'elevenlabs' 'macos'
70
+ ;;
71
+ windows)
72
+ printf '%s\n' 'soprano' 'kokoro' 'windows-sapi' 'windows-piper'
73
+ ;;
74
+ *) return 1 ;;
75
+ esac
76
+ }
77
+
78
+ # Print a provider's listable voices (ids for kokoro, names for elevenlabs), one per line.
79
+ # Discovered providers (piper/macos/sapi) enumerate at runtime and print nothing here.
80
+ catalog_list_voices() {
81
+ local _p; _p="$(catalog_canonical_provider "${1:-}")" || return 1
82
+ case "$_p" in
83
+ soprano)
84
+ printf '%s\n' 'soprano-default'
85
+ ;;
86
+ piper)
87
+ : # (none — enumerated at runtime)
88
+ ;;
89
+ kokoro)
90
+ printf '%s\n' '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'
91
+ ;;
92
+ elevenlabs)
93
+ printf '%s\n' 'Sarah' 'Roger' 'Laura' 'Charlie' 'George' 'Callum' 'River' 'Harry' 'Liam' 'Alice' 'Matilda' 'Will' 'Jessica' 'Eric' 'Bella' 'Chris' 'Brian' 'Daniel' 'Lily' 'Adam' 'Bill'
94
+ ;;
95
+ macos)
96
+ : # (none — enumerated at runtime)
97
+ ;;
98
+ windows-sapi)
99
+ : # (none — enumerated at runtime)
100
+ ;;
101
+ windows-piper)
102
+ : # (none — enumerated at runtime)
103
+ ;;
104
+ esac
105
+ }
106
+
107
+ # Validate a voice for a provider. Echoes the canonical voice and returns 0 on
108
+ # success; returns 1 (no output) on rejection. Mirrors validateVoice() in the JS SSOT.
109
+ catalog_validate_voice() {
110
+ local _p _v _lc; _p="$(catalog_canonical_provider "${1:-}")" || return 1
111
+ _v="${2:-}"; _lc="$(_catalog_lc "$_v")"
112
+ case "$_p" in
113
+ kokoro)
114
+ case "$_lc" in
115
+ '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')
116
+ printf '%s' "$_lc"; return 0 ;;
117
+ esac
118
+ return 1 ;;
119
+ elevenlabs)
120
+ case "$_lc" in
121
+ 'sarah') printf '%s' 'EXAVITQu4vr4xnSDxMaL'; return 0 ;;
122
+ 'roger') printf '%s' 'CwhRBWXzGAHq8TQ4Fs17'; return 0 ;;
123
+ 'laura') printf '%s' 'FGY2WhTYpPnrIDTdsKH5'; return 0 ;;
124
+ 'charlie') printf '%s' 'IKne3meq5aSn9XLyUdCD'; return 0 ;;
125
+ 'george') printf '%s' 'JBFqnCBsd6RMkjVDRZzb'; return 0 ;;
126
+ 'callum') printf '%s' 'N2lVS1w4EtoT3dr4eOWO'; return 0 ;;
127
+ 'river') printf '%s' 'SAz9YHcvj6GT2YYXdXww'; return 0 ;;
128
+ 'harry') printf '%s' 'SOYHLrjzK2X1ezoPC6cr'; return 0 ;;
129
+ 'liam') printf '%s' 'TX3LPaxmHKxFdv7VOQHJ'; return 0 ;;
130
+ 'alice') printf '%s' 'Xb7hH8MSUJpSbSDYk0k2'; return 0 ;;
131
+ 'matilda') printf '%s' 'XrExE9yKIg1WjnnlVkGX'; return 0 ;;
132
+ 'will') printf '%s' 'bIHbv24MWmeRgasZH58o'; return 0 ;;
133
+ 'jessica') printf '%s' 'cgSgspJ2msm6clMCkdW9'; return 0 ;;
134
+ 'eric') printf '%s' 'cjVigY5qzO86Huf0OWal'; return 0 ;;
135
+ 'bella') printf '%s' 'hpp4J3VqNfWAUOO0d1Us'; return 0 ;;
136
+ 'chris') printf '%s' 'iP95p4xoKVk53GoZ742B'; return 0 ;;
137
+ 'brian') printf '%s' 'nPczCjzI2devNBz1zQrb'; return 0 ;;
138
+ 'daniel') printf '%s' 'onwK4e9ZLuTAKqWW03F9'; return 0 ;;
139
+ 'lily') printf '%s' 'pFZP5JQG7iQjIQuC4Bku'; return 0 ;;
140
+ 'adam') printf '%s' 'pNInz6obpgDQGcFmaJgB'; return 0 ;;
141
+ 'bill') printf '%s' 'pqHfZKP75CvOlQylNhV4'; return 0 ;;
142
+ esac
143
+ [[ "$_v" =~ ^[A-Za-z0-9]{20}$ ]] && { printf '%s' "$_v"; return 0; }
144
+ return 1 ;;
145
+ soprano)
146
+ case "$_lc" in
147
+ ''|soprano|soprano-default) printf '%s' 'soprano-default'; return 0 ;;
148
+ esac
149
+ return 1 ;;
150
+ piper|windows-piper)
151
+ if [[ "$_v" =~ ^[a-z]{2,3}_[A-Z]{2}- ]] || [[ "$_v" == *"::"* ]]; then
152
+ printf '%s' "$_v"; return 0
153
+ fi
154
+ return 1 ;;
155
+ macos|windows-sapi)
156
+ if [ -n "$_v" ]; then printf '%s' "$_v"; return 0; fi
157
+ [ "$_p" = windows-sapi ] && { printf '%s' ''; return 0; }
158
+ return 1 ;;
159
+ esac
160
+ return 1
161
+ }
@@ -44,6 +44,7 @@
44
44
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45
45
  source "$SCRIPT_DIR/provider-manager.sh"
46
46
  source "$SCRIPT_DIR/language-manager.sh"
47
+ source "$SCRIPT_DIR/python-resolver.sh"
47
48
 
48
49
  COMMAND="${1:-help}"
49
50
 
@@ -101,7 +102,7 @@ provider_list() {
101
102
 
102
103
  # Check installation status of optional providers
103
104
  local kokoro_installed=false
104
- python3 -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>/dev/null && kokoro_installed=true
105
+ [[ -n "$PYTHON_BIN" ]] && "$PYTHON_BIN" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>/dev/null && kokoro_installed=true
105
106
 
106
107
  local elevenlabs_key_set=false
107
108
  if [[ -n "${ELEVENLABS_API_KEY:-}" ]]; then
@@ -73,6 +73,23 @@ get_provider_config_path() {
73
73
  echo "$provider_file"
74
74
  }
75
75
 
76
+ # @function _load_provider_catalog
77
+ # @intent Source the generated Provider Catalog (bash-3.2-safe SSOT accessors)
78
+ # @why Single source of truth for per-provider defaults (AVI-S9.4) — mirrors the
79
+ # identical pattern in voice-manager.sh (AVI-S9.3). FAIL-SAFE: callers probe
80
+ # `type catalog_* >/dev/null 2>&1` and fall back to legacy hardcoded literals
81
+ # when the artifact is missing (installed-tree skew). Defaults never break.
82
+ # @returns None (sources catalog_* functions into the current shell when present)
83
+ # @sideeffects None (double-sourcing is guarded inside provider-catalog.sh)
84
+ _load_provider_catalog() {
85
+ local _script_dir
86
+ _script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
87
+ if [[ -f "$_script_dir/provider-catalog.sh" ]]; then
88
+ # shellcheck source=/dev/null
89
+ source "$_script_dir/provider-catalog.sh" 2>/dev/null || true
90
+ fi
91
+ }
92
+
76
93
  # @function get_active_provider
77
94
  # @intent Read currently active TTS provider from config file
78
95
  # @why Central function for determining which provider to use
@@ -187,12 +204,24 @@ migrate_voice_to_provider() {
187
204
  "en_US-danny-low:Alex"
188
205
  )
189
206
 
190
- # Default voices by provider
191
- local piper_default="en_US-lessac-medium"
192
- local macos_default="Samantha"
193
- local soprano_default="soprano-default" # Single voice — no selection needed
194
- local elevenlabs_default="Rachel"
195
- local kokoro_default="af_heart"
207
+ # Default voices by provider — sourced from the generated Provider Catalog SSOT
208
+ # (AVI-S9.4, catalog_default_voice). FAIL-SAFE: legacy literals if the catalog
209
+ # artifact is missing (installed-tree skew) — mirrors AVI-S9.3's pattern.
210
+ _load_provider_catalog
211
+ local piper_default macos_default soprano_default elevenlabs_default kokoro_default
212
+ if type catalog_default_voice >/dev/null 2>&1; then
213
+ piper_default="$(catalog_default_voice piper)"
214
+ macos_default="$(catalog_default_voice macos)"
215
+ soprano_default="$(catalog_default_voice soprano)" # Single voice — no selection needed
216
+ elevenlabs_default="$(catalog_default_voice elevenlabs)"
217
+ kokoro_default="$(catalog_default_voice kokoro)"
218
+ else
219
+ piper_default="en_US-lessac-medium"
220
+ macos_default="Samantha"
221
+ soprano_default="soprano-default" # Single voice — no selection needed
222
+ elevenlabs_default="Sarah"
223
+ kokoro_default="af_heart"
224
+ fi
196
225
 
197
226
  # Single-voice providers: migration is straightforward
198
227
  case "$target_provider" in
@@ -214,7 +243,15 @@ migrate_voice_to_provider() {
214
243
  return 0
215
244
  fi
216
245
 
217
- # If migrating FROM a single-voice provider, return default for target provider
246
+ # If migrating FROM a single-voice/fixed-shape provider, return default for target.
247
+ # NOTE ("Rachel" sentinel, AVI-S9.4): "Rachel" was ElevenLabs' hardcoded default
248
+ # before the 21-voice catalog trim (AVI-S8.1) — it is NOT a name-to-id match in
249
+ # today's catalog. This is READ-SIDE TOLERANCE ONLY (CLAUDE.md non-destructive
250
+ # rule): a user config that still says "Rachel" degrades gracefully to the
251
+ # target provider's default instead of erroring; we never rewrite the user's file.
252
+ if [[ "$current_voice" == "Rachel" ]]; then
253
+ echo "⚠️ '$current_voice' is a legacy ElevenLabs default no longer in the voice catalog; migrating to the $target_provider default." >&2
254
+ fi
218
255
  if [[ "$current_voice" == "soprano-default" || "$current_voice" == "Rachel" || "$current_voice" =~ ^[a-z]{2}_[a-z0-9_]+$ ]]; then
219
256
  case "$target_provider" in
220
257
  piper) echo "$piper_default" ;;
@@ -372,6 +409,7 @@ get_provider_script_path() {
372
409
  # @returns Echoes LLM name (e.g. "claude-code") or empty string if no remote routing configured
373
410
  # @exitcode 0=always
374
411
  detect_routing_llm() {
412
+ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/python-resolver.sh"
375
413
  # Priority 1: AGENTVIBES_LLM_KEY env var (set during active TTS sessions)
376
414
  if [[ -n "${AGENTVIBES_LLM_KEY:-}" ]] && [[ "$AGENTVIBES_LLM_KEY" =~ ^llm:([a-zA-Z0-9][a-zA-Z0-9_-]*)$ ]]; then
377
415
  echo "${BASH_REMATCH[1]}"
@@ -380,9 +418,9 @@ detect_routing_llm() {
380
418
 
381
419
  # Priority 2: First mode=remote entry in transport-config.json
382
420
  local _transport_cfg="$HOME/.agentvibes/transport-config.json"
383
- if [[ -f "$_transport_cfg" ]] && command -v python3 &>/dev/null; then
421
+ if [[ -f "$_transport_cfg" ]] && [[ -n "$PYTHON_BIN" ]]; then
384
422
  local _remote_llm
385
- _remote_llm=$(AGENTVIBES_CFG="$_transport_cfg" python3 - <<'PYEOF'
423
+ _remote_llm=$(AGENTVIBES_CFG="$_transport_cfg" "$PYTHON_BIN" - <<'PYEOF'
386
424
  import json, os, sys
387
425
  try:
388
426
  d = json.load(open(os.environ['AGENTVIBES_CFG'], encoding='utf-8'))
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/python-resolver.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 Locate a WORKING Python 3 interpreter across platforms.
12
+ # @context Windows git-bash frequently has NO `python3`/`python` on PATH even when
13
+ # Python is installed (the on-PATH WindowsApps entries are Microsoft Store
14
+ # execution-alias stubs that do not actually run). The old `command -v
15
+ # python3` guard therefore made hooks falsely report "not installed".
16
+ # @architecture Sourced (not executed) by hooks; sets and exports $PYTHON_BIN.
17
+ # $PYTHON_BIN is empty when no usable interpreter exists. Callers should
18
+ # test `[[ -n "$PYTHON_BIN" ]]` instead of `command -v python3`.
19
+ # @related play-tts.sh, play-tts-kokoro.sh, kokoro-installer.sh, provider-commands.sh
20
+ #
21
+ # Usage:
22
+ # source "$SCRIPT_DIR/python-resolver.sh"
23
+ # [[ -n "$PYTHON_BIN" ]] || { echo "No Python 3 found" >&2; exit 1; }
24
+ # "$PYTHON_BIN" my-script.py ...
25
+ #
26
+ # Override: set AGENTVIBES_PYTHON=/path/to/python to force a specific interpreter.
27
+
28
+ # Fast path: resolve once per process. Several hooks source this file
29
+ # transitively (e.g. play-tts.sh -> provider-manager.sh), so if PYTHON_BIN is
30
+ # already exported and the file still exists, TRUST it without re-launching
31
+ # Python — a fresh interpreter startup per source adds ~100ms of latency per
32
+ # utterance for nothing. We only pay the exec-validation cost during genuine
33
+ # discovery below, where the Windows Store stub actually needs rejecting.
34
+ if [[ -n "${PYTHON_BIN:-}" && -f "$PYTHON_BIN" ]]; then
35
+ export PYTHON_BIN
36
+ else
37
+ _AV_PY_CACHE="${HOME}/.claude/config/python-bin.txt"
38
+
39
+ # A candidate is valid only if it runs AND is Python 3. Running it filters out
40
+ # the Windows Store alias stub (which exits non-zero when given args); the
41
+ # version check rejects a lone python2 whose py3 syntax would crash at runtime.
42
+ _av_python_valid() {
43
+ [[ -n "${1:-}" ]] || return 1
44
+ "$1" -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)' >/dev/null 2>&1
45
+ }
46
+
47
+ _av_resolve_python() {
48
+ local cand resolved
49
+
50
+ # 1. Explicit override wins.
51
+ if [[ -n "${AGENTVIBES_PYTHON:-}" ]] && _av_python_valid "$AGENTVIBES_PYTHON"; then
52
+ printf '%s' "$AGENTVIBES_PYTHON"; return 0
53
+ fi
54
+
55
+ # 2. Previously-cached hit (strip a stray CR from a CRLF-edited cache file).
56
+ if [[ -f "$_AV_PY_CACHE" ]]; then
57
+ cand="$(head -1 "$_AV_PY_CACHE" 2>/dev/null | tr -d '\r' || true)"
58
+ if _av_python_valid "$cand"; then printf '%s' "$cand"; return 0; fi
59
+ fi
60
+
61
+ # 3. On PATH — but VALIDATE each (rejects the WindowsApps store stub).
62
+ for cand in python3 python python3.13 python3.12 python3.11 python3.10; do
63
+ resolved="$(command -v "$cand" 2>/dev/null || true)"
64
+ if [[ -n "$resolved" ]] && _av_python_valid "$resolved"; then
65
+ printf '%s' "$resolved"; return 0
66
+ fi
67
+ done
68
+
69
+ # 4. Windows `py` launcher — ask it for the real interpreter path. Python on
70
+ # Windows emits CRLF even to a pipe, so strip the trailing CR or the exec
71
+ # of "…python.exe\r" fails.
72
+ if command -v py >/dev/null 2>&1; then
73
+ cand="$(py -3 -c 'import sys; print(sys.executable)' 2>/dev/null | tr -d '\r' || true)"
74
+ if _av_python_valid "$cand"; then printf '%s' "$cand"; return 0; fi
75
+ fi
76
+
77
+ # 5. Known Windows install locations, newest version first. Built with
78
+ # nullglob into an array so paths containing spaces ("C:\Program Files",
79
+ # "C:\Users\John Smith\…") survive — an unquoted *variable* glob would
80
+ # word-split them, which is exactly the case this resolver exists to fix.
81
+ # This file is SOURCED, so nullglob is saved and restored to avoid leaking
82
+ # the option into the calling hook.
83
+ local _av_restore_glob; _av_restore_glob="$(shopt -p nullglob 2>/dev/null || true)"
84
+ shopt -s nullglob 2>/dev/null || true
85
+ local -a _av_pys=(
86
+ "${LOCALAPPDATA:-$HOME/AppData/Local}/Programs/Python"/Python3*/python.exe
87
+ "$HOME/AppData/Local/Programs/Python"/Python3*/python.exe
88
+ /c/Python3*/python.exe
89
+ "/c/Program Files/Python3"*/python.exe
90
+ "/c/Program Files (x86)/Python3"*/python.exe
91
+ )
92
+ eval "${_av_restore_glob:-shopt -u nullglob}" 2>/dev/null || true
93
+ # Version-sort descending (Python312 before Python39 — a plain sort -r is
94
+ # lexical and would rank "39" above "312"); NUL-delimited so spacey paths
95
+ # stay intact. `sort -zrV` is GNU coreutils, which git-bash ships (this glob
96
+ # only ever matches under git-bash anyway).
97
+ if [[ ${#_av_pys[@]} -gt 0 ]]; then
98
+ while IFS= read -r -d '' cand; do
99
+ _av_python_valid "$cand" && { printf '%s' "$cand"; return 0; }
100
+ done < <(printf '%s\0' "${_av_pys[@]}" | sort -zrV 2>/dev/null)
101
+ fi
102
+
103
+ return 1
104
+ }
105
+
106
+ PYTHON_BIN="$(_av_resolve_python || true)"
107
+ export PYTHON_BIN
108
+
109
+ # Cache a fresh discovery (best-effort; never fatal). Only rewrite when it
110
+ # actually changed, to avoid needless disk writes on every hook invocation.
111
+ if [[ -n "$PYTHON_BIN" ]]; then
112
+ if [[ ! -f "$_AV_PY_CACHE" ]] || [[ "$(head -1 "$_AV_PY_CACHE" 2>/dev/null | tr -d '\r' || true)" != "$PYTHON_BIN" ]]; then
113
+ mkdir -p "$(dirname "$_AV_PY_CACHE")" 2>/dev/null || true
114
+ printf '%s\n' "$PYTHON_BIN" > "$_AV_PY_CACHE" 2>/dev/null || true
115
+ fi
116
+ fi
117
+ fi
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/session-id.sh
4
+ #
5
+ # AgentVibes - Canonical routing session-id helper.
6
+ #
7
+ # The receiver MULTIPLEXES forwarded TTS from many sources (multiple local
8
+ # projects + remote machines) keyed by a per-message identity. That identity
9
+ # MUST be stable and correctly derived, or the receiver merges unrelated
10
+ # sessions into one.
11
+ #
12
+ # This helper is the SINGLE source of truth for that routing id so every
13
+ # forward path (e.g. play-tts-ssh-remote.sh SSH) derives it identically:
14
+ #
15
+ # session id = basename(CLAUDE_PROJECT_DIR) when CLAUDE_PROJECT_DIR is set
16
+ # = basename(<fallback dir>) otherwise (passed dir, else PWD)
17
+ #
18
+ # It is intentionally NOT basename(install root): for a global install
19
+ # (~/.claude/hooks) that basename collapses to the HOME dir name (e.g.
20
+ # "administrator"), so every project on the machine would report the same id.
21
+ #
22
+ # NOTE: This is a PLAIN SLUG for routing — do NOT confuse it with
23
+ # agentvibes-session-id.sh, which produces a human SPOKEN identity string.
24
+ #
25
+ # Usage:
26
+ # source "$(dirname "${BASH_SOURCE[0]}")/session-id.sh"
27
+ # SESSION_ID="$(av_session_id "$FALLBACK_PROJECT_DIR")"
28
+ #
29
+ # bash-3.2 safe (macOS default bash). Sourced by scripts already in strict mode;
30
+ # defines a function only and does not alter shell options.
31
+ #
32
+
33
+ # av_session_id [fallback_dir]
34
+ # Prints the canonical routing session id (plain slug) to stdout.
35
+ # Prefers CLAUDE_PROJECT_DIR (the real user project injected via
36
+ # --project-dir); else the passed fallback dir; else PWD. Callers MUST pass a
37
+ # real project dir as the fallback — never the package/install root.
38
+ av_session_id() {
39
+ local _dir="${CLAUDE_PROJECT_DIR:-}"
40
+ if [[ -z "$_dir" ]]; then
41
+ _dir="${1:-$PWD}"
42
+ fi
43
+ # Strip trailing slashes so basename of "/a/b/" is "b", not "".
44
+ while [[ "$_dir" == */ && "${#_dir}" -gt 1 ]]; do
45
+ _dir="${_dir%/}"
46
+ done
47
+ local _base
48
+ _base="$(basename "$_dir" 2>/dev/null || printf '%s' "$_dir")"
49
+ # Slugify: collapse anything outside [A-Za-z0-9._-] to '-' so the id is safe
50
+ # as a JSON string and a stable map key on the receiver.
51
+ _base="$(printf '%s' "$_base" | tr -c 'A-Za-z0-9._-' '-')"
52
+ # Trim leading/trailing dashes introduced by slugification.
53
+ _base="${_base#-}"; _base="${_base%-}"
54
+ [[ -z "$_base" ]] && _base="unknown"
55
+ printf '%s' "$_base"
56
+ }
@@ -38,6 +38,45 @@ else
38
38
  _PROJECT_CLAUDE_DIR="$(dirname "$SCRIPT_DIR")"
39
39
  fi
40
40
 
41
+ # ---------------------------------------------------------------------------
42
+ # OPT-IN INJECTION GATE (Issue: global-install cacophony).
43
+ #
44
+ # Previously this hook injected the ~250-token TTS protocol into EVERY session
45
+ # where AgentVibes was found on disk. A GLOBAL install therefore made every open
46
+ # Claude session start speaking at once ("a cacophony of agents"). It also spent
47
+ # tokens on idle sessions that never wanted audio. Injection is now OPT-IN.
48
+ #
49
+ # Two distinct markers, kept SEPARATE on purpose:
50
+ # * agentvibes-unmuted / agentvibes-muted — the user's runtime mute choice,
51
+ # also read by play-tts.sh, where a project `unmuted` OVERRIDES a global mute.
52
+ # * agentvibes-enabled — "this project opted into injection" (written by a
53
+ # project install). It does NOT override a global mute; it only turns the
54
+ # protocol injection on. This separation is why a project install can no
55
+ # longer silently defeat a user's `~/.agentvibes-muted` global kill-switch.
56
+ #
57
+ # Precedence (mirrors play-tts.sh so injection and audio agree):
58
+ # 1. project agentvibes-unmuted → inject (explicit per-project ON)
59
+ # 2. project agentvibes-muted → silent
60
+ # 3. global ~/.agentvibes-muted → silent (the global kill-switch)
61
+ # 4. project agentvibes-enabled, OR global opt-in
62
+ # (~/.claude/agentvibes-enabled or ~/.claude/agentvibes-unmuted) → inject
63
+ # 5. otherwise → silent (opt-in default; zero tokens)
64
+ if [[ -f "$_PROJECT_CLAUDE_DIR/agentvibes-unmuted" ]]; then
65
+ : # explicit per-project enable — wins over a global mute, as in play-tts.sh
66
+ elif [[ -f "$_PROJECT_CLAUDE_DIR/agentvibes-muted" ]]; then
67
+ exit 0
68
+ elif [[ -f "$HOME/.agentvibes-muted" ]]; then
69
+ exit 0 # global kill-switch, no per-project override
70
+ elif [[ -f "$_PROJECT_CLAUDE_DIR/agentvibes-enabled" \
71
+ || -f "$HOME/.claude/agentvibes-enabled" \
72
+ || -f "$HOME/.claude/agentvibes-unmuted" ]]; then
73
+ : # opted in (project install marker, or a deliberate global opt-in)
74
+ else
75
+ # Not enabled for this project → inject nothing (zero tokens).
76
+ # Enable for this project with: /agent-vibes:unmute
77
+ exit 0
78
+ fi
79
+
41
80
  # Build --project-dir flag to embed in TTS commands.
42
81
  # Sanitize: strip any embedded quotes that would break shell quoting.
43
82
  PROJECT_DIR_FLAG=""
@@ -37,7 +37,7 @@
37
37
  # @dependencies .claude/config/tts-speech-rate.txt, .claude/config/tts-target-speech-rate.txt
38
38
  # @entrypoints Called by /agent-vibes:set-speed slash command
39
39
  # @patterns Provider-agnostic speed config, legacy file migration, random tongue twisters for testing
40
- # @related play-tts.sh, play-tts-piper.sh, play-tts-macos.sh, learn-manager.sh
40
+ # @related play-tts.sh, play-tts-piper.sh, play-tts-macos.sh
41
41
  #
42
42
 
43
43
  # Get script directory
@@ -32,7 +32,7 @@
32
32
  # @dependencies translator.py, language-manager.sh, .bmad/core/config.yaml (optional)
33
33
  # @entrypoints Called by /agent-vibes:translate commands and play-tts.sh
34
34
  # @patterns Config cascade - manual override > BMAD config > default (no translation)
35
- # @related translator.py, play-tts.sh, language-manager.sh, learn-manager.sh
35
+ # @related translator.py, play-tts.sh, language-manager.sh
36
36
 
37
37
  # Only set strict mode when executed directly, not when sourced
38
38
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
@@ -40,6 +40,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
40
40
  fi
41
41
 
42
42
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
43
+ source "$SCRIPT_DIR/python-resolver.sh"
43
44
 
44
45
  # Use PWD for project dir when called from project context, fall back to script-relative
45
46
  if [[ -d "$PWD/.claude" ]]; then
@@ -173,7 +174,7 @@ translate_text() {
173
174
 
174
175
  # Call translator.py
175
176
  local translated
176
- translated=$(python3 "$SCRIPT_DIR/translator.py" "$text" "$target" 2>/dev/null) || translated="$text"
177
+ translated=$("$PYTHON_BIN" "$SCRIPT_DIR/translator.py" "$text" "$target" 2>/dev/null) || translated="$text"
177
178
 
178
179
  echo "$translated"
179
180
  }
@@ -32,7 +32,7 @@
32
32
  # @dependencies deep-translator, langdetect (pip install deep-translator langdetect)
33
33
  # @entrypoints CLI: python3 translator.py <text> <target_lang>, Library: from translator import translate
34
34
  # @patterns Command pattern - supports translate, detect, and batch operations
35
- # @related play-tts.sh, learn-manager.sh, language-manager.sh
35
+ # @related play-tts.sh, language-manager.sh
36
36
  #
37
37
 
38
38
  """