agentvibes 5.12.0-alpha.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 (129) hide show
  1. package/.agentvibes/config.json +15 -1
  2. package/.agentvibes/install-manifest.json +342 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/commands/agent-vibes/commands.json +0 -20
  5. package/.claude/commands/agent-vibes/unmute.md +6 -2
  6. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  7. package/.claude/commands/agent-vibes-rdp.md +24 -24
  8. package/.claude/config/audio-effects.cfg +6 -6
  9. package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
  10. package/.claude/config/background-music-enabled.txt +1 -1
  11. package/.claude/config/background-music-position.txt +27 -0
  12. package/.claude/config/background-music-volume.txt +1 -0
  13. package/.claude/config/background-music.cfg +1 -0
  14. package/.claude/config/background-music.txt +1 -0
  15. package/.claude/config/language.txt +1 -0
  16. package/.claude/config/personality.txt +1 -0
  17. package/.claude/config/tts-speech-rate.txt +1 -0
  18. package/.claude/config/tts-verbosity.txt +1 -0
  19. package/.claude/docs/TERMUX_SETUP.md +408 -408
  20. package/.claude/github-star-reminder.txt +1 -1
  21. package/.claude/hooks/agentvibes-session-id.sh +69 -0
  22. package/.claude/hooks/bmad-party-speak.sh +20 -4
  23. package/.claude/hooks/bmad-speak.sh +60 -2
  24. package/.claude/hooks/bmad-tts-injector.sh +20 -1
  25. package/.claude/hooks/bmad-voice-manager.sh +25 -3
  26. package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
  27. package/.claude/hooks/clawdbot-receiver.sh +19 -1
  28. package/.claude/hooks/elevenlabs-voices.sh +62 -0
  29. package/.claude/hooks/kokoro-installer.sh +20 -10
  30. package/.claude/hooks/language-manager.sh +10 -3
  31. package/.claude/hooks/party-set-room.sh +71 -0
  32. package/.claude/hooks/party-stage-roster.py +328 -0
  33. package/.claude/hooks/personality-manager.sh +19 -2
  34. package/.claude/hooks/piper-voice-manager.sh +3 -2
  35. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
  36. package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
  37. package/.claude/hooks/play-tts-kokoro.sh +33 -6
  38. package/.claude/hooks/play-tts-soprano.sh +3 -2
  39. package/.claude/hooks/play-tts-ssh-remote.sh +72 -27
  40. package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
  41. package/.claude/hooks/play-tts.sh +86 -69
  42. package/.claude/hooks/provider-catalog.json +352 -0
  43. package/.claude/hooks/provider-catalog.sh +161 -0
  44. package/.claude/hooks/provider-commands.sh +2 -1
  45. package/.claude/hooks/provider-manager.sh +47 -9
  46. package/.claude/hooks/python-resolver.sh +117 -0
  47. package/.claude/hooks/session-id.sh +56 -0
  48. package/.claude/hooks/session-start-tts.sh +39 -0
  49. package/.claude/hooks/speed-manager.sh +1 -1
  50. package/.claude/hooks/translate-manager.sh +3 -2
  51. package/.claude/hooks/translator.py +1 -1
  52. package/.claude/hooks/voice-manager.sh +242 -10
  53. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  54. package/.claude/hooks-windows/language-manager.ps1 +7 -1
  55. package/.claude/hooks-windows/personality-manager.ps1 +16 -1
  56. package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
  57. package/.claude/hooks-windows/play-tts.ps1 +32 -3
  58. package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
  59. package/.claude/hooks-windows/provider-manager.ps1 +63 -8
  60. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  61. package/.claude/hooks-windows/tts-watcher.ps1 +84 -8
  62. package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
  63. package/.claude/piper-voices-dir.txt +1 -0
  64. package/.clawdbot/README.md +105 -105
  65. package/.mcp.json +15 -0
  66. package/LICENSE +190 -190
  67. package/README.md +20 -6
  68. package/RELEASE_NOTES.md +81 -0
  69. package/WINDOWS-SETUP.md +208 -208
  70. package/bin/agent-vibes +39 -39
  71. package/bin/mcp-server.js +121 -121
  72. package/bin/test-bmad-pr +78 -78
  73. package/mcp-server/QUICK_START.md +203 -203
  74. package/mcp-server/README.md +345 -345
  75. package/mcp-server/examples/claude_desktop_config.json +11 -11
  76. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  77. package/mcp-server/examples/custom_instructions.md +169 -169
  78. package/mcp-server/pyproject.toml +52 -52
  79. package/mcp-server/requirements.txt +2 -2
  80. package/mcp-server/server.py +146 -49
  81. package/mcp-server/test_mcp_correctness.py +20 -2
  82. package/mcp-server/test_server.py +395 -395
  83. package/mcp-server/test_windows_script_parity.py +0 -2
  84. package/package.json +1 -1
  85. package/src/cli/list-personalities.js +110 -110
  86. package/src/cli/list-voices.js +115 -11
  87. package/src/commands/bmad-voices.js +394 -394
  88. package/src/console/app.js +1 -9
  89. package/src/console/bling.js +71 -0
  90. package/src/console/brand-colors.js +13 -13
  91. package/src/console/constants/personalities.js +44 -44
  92. package/src/console/footer-config.js +0 -4
  93. package/src/console/music-preview.js +79 -0
  94. package/src/console/navigation.js +0 -1
  95. package/src/console/tabs/agents-tab.js +14 -2
  96. package/src/console/tabs/help-tab.js +314 -314
  97. package/src/console/tabs/music-tab.js +81 -5
  98. package/src/console/tabs/placeholder-tab.js +0 -2
  99. package/src/console/tabs/readme-tab.js +272 -272
  100. package/src/console/tabs/settings-tab.js +207 -14
  101. package/src/console/tabs/setup-tab.js +39 -36
  102. package/src/console/tabs/voices-tab.js +102 -23
  103. package/src/console/widgets/destroy-list.js +25 -25
  104. package/src/console/widgets/notice.js +55 -55
  105. package/src/console/widgets/track-picker.js +82 -0
  106. package/src/installer/language-screen.js +31 -31
  107. package/src/installer/music-file-input.js +304 -304
  108. package/src/installer.js +124 -10
  109. package/src/services/language-service.js +47 -47
  110. package/src/services/navigation-service.js +1 -1
  111. package/src/services/provider-catalog.js +412 -0
  112. package/src/services/provider-voice-catalog.js +52 -73
  113. package/src/services/tts-engine-service.js +29 -0
  114. package/src/utils/audio-format-validator.js +277 -277
  115. package/src/utils/dependency-checker.js +469 -469
  116. package/src/utils/file-ownership-verifier.js +358 -358
  117. package/src/utils/music-file-validator.js +285 -285
  118. package/src/utils/preview-list-prompt.js +144 -144
  119. package/src/utils/provider-validator.js +62 -12
  120. package/src/utils/secure-music-storage.js +412 -412
  121. package/voice-assignments.json +8244 -8244
  122. package/.claude/commands/agent-vibes/language.md +0 -23
  123. package/.claude/commands/agent-vibes/learn.md +0 -67
  124. package/.claude/commands/agent-vibes/replay-target.md +0 -14
  125. package/.claude/commands/agent-vibes/target-voice.md +0 -26
  126. package/.claude/commands/agent-vibes/target.md +0 -30
  127. package/.claude/hooks/learn-manager.sh +0 -492
  128. package/.claude/hooks/replay-target-audio.sh +0 -95
  129. package/.claude/hooks-windows/learn-manager.ps1 +0 -241
@@ -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
  """
@@ -43,6 +43,18 @@ to_lower() {
43
43
  echo "$1" | tr '[:upper:]' '[:lower:]'
44
44
  }
45
45
 
46
+ # Source the generated Provider Catalog (bash-3.2-safe SSOT accessors) when present.
47
+ # FAIL-SAFE: consumers probe `type catalog_* >/dev/null 2>&1` and fall back to the
48
+ # legacy regex/hardcoded behavior when the artifact is missing (installed-tree skew)
49
+ # — mirrors play-tts.sh's PLAN_OK legacy fallback. Switch/list never break on a
50
+ # missing catalog. Double-sourcing is guarded inside provider-catalog.sh.
51
+ _load_provider_catalog() {
52
+ if [[ -f "$SCRIPT_DIR/provider-catalog.sh" ]]; then
53
+ # shellcheck source=/dev/null
54
+ source "$SCRIPT_DIR/provider-catalog.sh" 2>/dev/null || true
55
+ fi
56
+ }
57
+
46
58
  # Determine target .claude directory based on context
47
59
  # Priority:
48
60
  # 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings)
@@ -82,13 +94,26 @@ get_default_voice() {
82
94
 
83
95
  case "$active_provider" in
84
96
  piper)
85
- echo "en_US-lessac-medium" # Piper default
97
+ # Piper default sourced from the generated Provider Catalog (SSOT, AVI-S9.4).
98
+ # FAIL-SAFE: legacy literal if the catalog artifact is missing (installed-tree skew).
99
+ _load_provider_catalog
100
+ if type catalog_default_voice >/dev/null 2>&1; then
101
+ catalog_default_voice piper
102
+ else
103
+ echo "en_US-lessac-medium" # Piper default (legacy fallback)
104
+ fi
86
105
  ;;
87
106
  macos)
88
107
  echo "Samantha" # macOS default
89
108
  ;;
90
109
  *)
91
- echo "en_US-lessac-medium" # Default to Piper
110
+ # Default to Piper — same catalog-sourced value, same fail-safe.
111
+ _load_provider_catalog
112
+ if type catalog_default_voice >/dev/null 2>&1; then
113
+ catalog_default_voice piper
114
+ else
115
+ echo "en_US-lessac-medium" # Default to Piper (legacy fallback)
116
+ fi
92
117
  ;;
93
118
  esac
94
119
  }
@@ -103,7 +128,7 @@ case "$1" in
103
128
 
104
129
  ACTIVE_PROVIDER="piper" # default
105
130
  if [ -f "$PROVIDER_FILE" ]; then
106
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
131
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
107
132
  fi
108
133
 
109
134
  CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || get_default_voice)
@@ -170,12 +195,76 @@ case "$1" in
170
195
  fi
171
196
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
172
197
  fi
198
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
199
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
200
+ node "$FORMATTER" "kokoro" "$CURRENT_VOICE"
201
+ else
202
+ echo "🎤 Available Kokoro TTS Voices:"
203
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
204
+ _load_provider_catalog
205
+ if type catalog_list_voices >/dev/null 2>&1; then
206
+ # All ~54 ids from the SSOT, one per line (catalog order = grouped by lang prefix).
207
+ catalog_list_voices kokoro | sed 's/^/ /'
208
+ else
209
+ # FAIL-SAFE legacy path (catalog artifact missing) — degraded, never dead.
210
+ echo " af_heart af_bella af_nicole af_sarah af_sky (American, female)"
211
+ echo " am_adam am_michael am_onyx am_puck (American, male)"
212
+ echo " bf_emma bf_lily bm_george bm_daniel (British)"
213
+ echo " jf_alpha (Japanese) zf_xiaoxiao (Mandarin) ef_dora (Spanish)"
214
+ fi
215
+ echo " (current: $CURRENT_VOICE)"
216
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
217
+ fi
218
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
219
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
220
+ node "$FORMATTER" "elevenlabs" "$CURRENT_VOICE"
221
+ else
222
+ echo "🎤 Available ElevenLabs Voices:"
223
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
224
+ if [[ -f "$SCRIPT_DIR/elevenlabs-voices.sh" ]]; then
225
+ # shellcheck source=/dev/null
226
+ source "$SCRIPT_DIR/elevenlabs-voices.sh"
227
+ elevenlabs_voice_names | sort | sed 's/^/ /'
228
+ fi
229
+ echo " (current: $CURRENT_VOICE)"
230
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
231
+ fi
232
+ elif [[ "$ACTIVE_PROVIDER" == "soprano" ]]; then
233
+ # Soprano is a single-voice provider (voiceModel: single) — list its one
234
+ # canonical voice from the generated Provider Catalog (SSOT, AVI-S9.4).
235
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
236
+ node "$FORMATTER" "soprano" "$CURRENT_VOICE"
237
+ else
238
+ echo "🎤 Available Soprano TTS Voices:"
239
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
240
+ _load_provider_catalog
241
+ if type catalog_list_voices >/dev/null 2>&1; then
242
+ catalog_list_voices soprano | sed 's/^/ /'
243
+ else
244
+ # FAIL-SAFE legacy path (catalog artifact missing): single canonical voice.
245
+ echo " soprano-default"
246
+ fi
247
+ echo " (current: $CURRENT_VOICE)"
248
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
249
+ fi
173
250
  else
174
251
  echo "❌ Unknown provider: $ACTIVE_PROVIDER"
175
252
  echo ""
176
253
  echo "Available providers:"
177
- echo " - piper (Free, Offline)"
178
- echo " - macos (Built-in, macOS only)"
254
+ # Derived from the generated Provider Catalog (SSOT, AVI-S9.4) so a new
255
+ # provider never silently goes missing from this help text again.
256
+ _load_provider_catalog
257
+ if type catalog_providers_for_platform >/dev/null 2>&1; then
258
+ while IFS= read -r _pid; do
259
+ echo " - $_pid ($(catalog_display_name "$_pid"))"
260
+ done < <(catalog_providers_for_platform unix)
261
+ else
262
+ # FAIL-SAFE legacy path (catalog artifact missing).
263
+ echo " - piper (Free, Offline)"
264
+ echo " - macos (Built-in, macOS only)"
265
+ echo " - kokoro (Neural, local)"
266
+ echo " - elevenlabs (Cloud, API key)"
267
+ fi
179
268
  echo ""
180
269
  echo "Switch provider with: /agent-vibes:provider switch piper"
181
270
  fi
@@ -221,7 +310,7 @@ case "$1" in
221
310
 
222
311
  ACTIVE_PROVIDER="piper" # default
223
312
  if [[ -n "$PROVIDER_FILE" ]]; then
224
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
313
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
225
314
  fi
226
315
 
227
316
  # Voice lookup strategy depends on active provider
@@ -363,12 +452,123 @@ case "$1" in
363
452
  echo "Download extra voices with: /agent-vibes:provider download"
364
453
  exit 1
365
454
  fi
455
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
456
+ # Kokoro switch-time validation via the generated Provider Catalog (SSOT):
457
+ # MEMBERSHIP + canonical case-fold (AF_HEART → af_heart), so a typo like
458
+ # `af_hart` is rejected HERE and never reaches tts-voice.txt to die silently
459
+ # at the model. Escape hatch AGENTVIBES_ALLOW_UNLISTED_VOICE=1 bypasses
460
+ # MEMBERSHIP (not shape) so a voice from a newer kokoro model still switches.
461
+ # FAIL-SAFE: if the catalog artifact is missing (installed-tree skew) we fall
462
+ # back to the legacy shape-only regex — degraded, never dead.
463
+ _load_provider_catalog
464
+ KOKORO_VOICE_LC="$(to_lower "$VOICE_NAME")"
465
+ if type catalog_validate_voice >/dev/null 2>&1; then
466
+ _KOKORO_CANON="$(catalog_validate_voice kokoro "$VOICE_NAME" 2>/dev/null || true)"
467
+ if [[ -n "$_KOKORO_CANON" ]]; then
468
+ FOUND="$_KOKORO_CANON"
469
+ elif [[ "${AGENTVIBES_ALLOW_UNLISTED_VOICE:-}" == "1" ]] && [[ "$KOKORO_VOICE_LC" =~ ^[a-z]{2}_[a-z0-9_]+$ ]]; then
470
+ echo "⚠️ '$VOICE_NAME' is not in the shipped Kokoro catalog; allowing it (AGENTVIBES_ALLOW_UNLISTED_VOICE=1)." >&2
471
+ FOUND="$KOKORO_VOICE_LC"
472
+ else
473
+ echo "❌ Kokoro voice not found: $VOICE_NAME"
474
+ echo ""
475
+ echo "That is not a known Kokoro voice. Examples:"
476
+ echo " - af_heart, af_bella, af_nicole (American English, female)"
477
+ echo " - am_adam, am_michael (American English, male)"
478
+ echo " - bf_emma, bf_lily (British English, female)"
479
+ echo " - bm_george, bm_daniel (British English, male)"
480
+ echo " - jf_alpha (Japanese), zf_xiaoxiao (Mandarin), ef_dora (Spanish)"
481
+ echo ""
482
+ echo "Set AGENTVIBES_ALLOW_UNLISTED_VOICE=1 to allow a newer-model voice."
483
+ echo "List all Kokoro voices with: /agent-vibes:list"
484
+ exit 1
485
+ fi
486
+ else
487
+ # FAIL-SAFE legacy path (catalog artifact missing): shape-only regex.
488
+ if [[ "$KOKORO_VOICE_LC" =~ ^[a-z]{2}_[a-z0-9_]+$ ]]; then
489
+ FOUND="$KOKORO_VOICE_LC"
490
+ else
491
+ echo "❌ Kokoro voice not found: $VOICE_NAME"
492
+ echo ""
493
+ echo "Kokoro voice ids look like <lang><sex>_name. Examples:"
494
+ echo " - af_heart, af_bella, af_nicole (American English, female)"
495
+ echo " - am_adam, am_michael (American English, male)"
496
+ echo " - bf_emma, bf_lily (British English, female)"
497
+ echo " - bm_george, bm_daniel (British English, male)"
498
+ echo " - jf_alpha (Japanese), zf_xiaoxiao (Mandarin), ef_dora (Spanish)"
499
+ echo ""
500
+ echo "List all Kokoro voices with: /agent-vibes:list"
501
+ exit 1
502
+ fi
503
+ fi
504
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
505
+ # ElevenLabs voice lookup — resolve a friendly name (case-insensitive) OR a
506
+ # raw voice_id to the canonical voice_id and save THAT (play-tts-elevenlabs.sh
507
+ # accepts a raw id directly). Resolution is bash-native via the shared catalog
508
+ # (elevenlabs-voices.sh) — the SAME source the synth hook uses — so it works
509
+ # in installed deployments with no Node dependency.
510
+ EL_RESOLVED=""
511
+ if [[ -f "$SCRIPT_DIR/elevenlabs-voices.sh" ]]; then
512
+ # shellcheck source=/dev/null
513
+ source "$SCRIPT_DIR/elevenlabs-voices.sh"
514
+ EL_RESOLVED="$(elevenlabs_resolve_voice "$VOICE_NAME" 2>/dev/null || true)"
515
+ elif [[ "$VOICE_NAME" =~ ^[A-Za-z0-9]{20}$ ]]; then
516
+ # Catalog missing but the input is already a raw voice_id — accept it.
517
+ EL_RESOLVED="$VOICE_NAME"
518
+ fi
519
+ if [[ -n "$EL_RESOLVED" ]]; then
520
+ FOUND="$EL_RESOLVED"
521
+ else
522
+ echo "❌ ElevenLabs voice not found: $VOICE_NAME"
523
+ echo ""
524
+ echo "ElevenLabs voices include: Sarah, Roger, Laura, George, Callum, Alice,"
525
+ echo "Matilda, Will, Jessica, Eric, Brian, Daniel, Lily, Bill (or a raw voice_id)."
526
+ echo ""
527
+ echo "List all ElevenLabs voices with: /agent-vibes:list"
528
+ exit 1
529
+ fi
530
+ elif [[ "$ACTIVE_PROVIDER" == "soprano" ]]; then
531
+ # Soprano switch-time validation via the generated Provider Catalog (SSOT):
532
+ # voiceModel `single` — accepts '', 'soprano', 'soprano-default' (case-insensitive)
533
+ # and canonicalizes to 'soprano-default'; anything else is rejected here so a
534
+ # typo never reaches tts-voice.txt. FAIL-SAFE: if the catalog artifact is
535
+ # missing, the same fixed rule is applied hardcoded (soprano has exactly one
536
+ # voice — this rule never drifts).
537
+ _load_provider_catalog
538
+ SOPRANO_CANON=""
539
+ if type catalog_validate_voice >/dev/null 2>&1; then
540
+ SOPRANO_CANON="$(catalog_validate_voice soprano "$VOICE_NAME" 2>/dev/null || true)"
541
+ else
542
+ case "$(to_lower "$VOICE_NAME")" in
543
+ ''|soprano|soprano-default) SOPRANO_CANON="soprano-default" ;;
544
+ esac
545
+ fi
546
+ if [[ -n "$SOPRANO_CANON" ]]; then
547
+ FOUND="$SOPRANO_CANON"
548
+ else
549
+ echo "❌ Soprano voice not found: $VOICE_NAME"
550
+ echo ""
551
+ echo "Soprano TTS has a single voice: soprano-default"
552
+ echo "(accepts '', 'soprano', or 'soprano-default')."
553
+ exit 1
554
+ fi
366
555
  else
367
556
  echo "❌ Unknown provider: $ACTIVE_PROVIDER"
368
557
  echo ""
369
558
  echo "Available providers:"
370
- echo " - piper (Free, Offline)"
371
- echo " - macos (Built-in, macOS only)"
559
+ # Derived from the generated Provider Catalog (SSOT, AVI-S9.4).
560
+ _load_provider_catalog
561
+ if type catalog_providers_for_platform >/dev/null 2>&1; then
562
+ while IFS= read -r _pid; do
563
+ echo " - $_pid ($(catalog_display_name "$_pid"))"
564
+ done < <(catalog_providers_for_platform unix)
565
+ else
566
+ # FAIL-SAFE legacy path (catalog artifact missing).
567
+ echo " - piper (Free, Offline)"
568
+ echo " - macos (Built-in, macOS only)"
569
+ echo " - kokoro (Neural, local)"
570
+ echo " - elevenlabs (Cloud, API key)"
571
+ fi
372
572
  echo ""
373
573
  echo "Switch provider with: /agent-vibes:provider switch piper"
374
574
  exit 1
@@ -412,7 +612,7 @@ case "$1" in
412
612
  fi
413
613
 
414
614
  if [ -f "$PROVIDER_FILE" ]; then
415
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
615
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
416
616
  if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
417
617
  echo "Provider: Piper TTS (Free, Offline)"
418
618
  elif [[ "$ACTIVE_PROVIDER" == "ssh-remote" ]]; then
@@ -421,6 +621,10 @@ case "$1" in
421
621
  echo "Provider: Piper TTS (via AgentVibes Receiver)"
422
622
  elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
423
623
  echo "Provider: macOS Say (Built-in, Free)"
624
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
625
+ echo "Provider: Kokoro TTS (Neural, Local)"
626
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
627
+ echo "Provider: ElevenLabs (Cloud, API Key)"
424
628
  else
425
629
  echo "Provider: $ACTIVE_PROVIDER"
426
630
  fi
@@ -466,7 +670,7 @@ case "$1" in
466
670
 
467
671
  ACTIVE_PROVIDER="piper" # default
468
672
  if [ -f "$PROVIDER_FILE" ]; then
469
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
673
+ ACTIVE_PROVIDER=$(tr -d '[:space:]' < "$PROVIDER_FILE")
470
674
  fi
471
675
 
472
676
  if [[ "$ACTIVE_PROVIDER" == "piper" || "$ACTIVE_PROVIDER" == "ssh-remote" || "$ACTIVE_PROVIDER" == "agentvibes-receiver" ]]; then
@@ -487,6 +691,34 @@ case "$1" in
487
691
  else
488
692
  echo "(macOS voices only available on macOS)"
489
693
  fi
694
+ elif [[ "$ACTIVE_PROVIDER" == "kokoro" ]]; then
695
+ # Kokoro's catalog is fixed; list ALL ids from the generated Provider Catalog (SSOT).
696
+ _load_provider_catalog
697
+ if type catalog_list_voices >/dev/null 2>&1; then
698
+ catalog_list_voices kokoro | sort
699
+ else
700
+ # FAIL-SAFE legacy path (catalog artifact missing): representative subset.
701
+ printf '%s\n' af_heart af_bella af_nicole af_sarah af_sky \
702
+ am_adam am_michael am_onyx am_puck \
703
+ bf_emma bf_lily bm_george bm_daniel \
704
+ jf_alpha zf_xiaoxiao ef_dora | sort
705
+ fi
706
+ elif [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then
707
+ if [[ -f "$SCRIPT_DIR/elevenlabs-voices.sh" ]]; then
708
+ # shellcheck source=/dev/null
709
+ source "$SCRIPT_DIR/elevenlabs-voices.sh"
710
+ elevenlabs_voice_names | sort
711
+ fi
712
+ elif [[ "$ACTIVE_PROVIDER" == "soprano" ]]; then
713
+ # Soprano's catalog is a single fixed voice; list it from the generated
714
+ # Provider Catalog (SSOT, AVI-S9.4).
715
+ _load_provider_catalog
716
+ if type catalog_list_voices >/dev/null 2>&1; then
717
+ catalog_list_voices soprano
718
+ else
719
+ # FAIL-SAFE legacy path (catalog artifact missing).
720
+ echo "soprano-default"
721
+ fi
490
722
  else
491
723
  echo "(Unknown provider: $ACTIVE_PROVIDER)"
492
724
  fi