agentvibes 5.6.7 → 5.6.8

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.
@@ -1 +1,6 @@
1
1
  default||agentvibes_soft_flamenco_loop.mp3|0.30
2
+ llm:default|light||0.15|en_US-lessac-high||piper
3
+ llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Claude Code here|piper
4
+ llm:copilot|light|agent_vibes_bossa_nova_v2_loop.mp3|0.15|en_US-libritts-high::Anna-11|Copilot here|piper
5
+ llm:codex|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Codex here|piper
6
+ llm:hermes|light|agent_vibes_bachata_v1_loop.mp3|0.15|en_US-libritts-high::Leo-8|Hermes here|piper
@@ -25,3 +25,4 @@ agent_vibes_chillwave_v2_loop.mp3:.00000000000000000003308191
25
25
  Midnight Charleston Stomp.mp3:.00000000000000000010960000
26
26
  agent_vibes_japanese_city_pop_v1_loop.mp3:11.702675
27
27
  agentvibes_soft_flamenco_loop.mp3:9.589660
28
+ Late Night Hip Hop Groove.mp3:5.601723
@@ -1 +1 @@
1
- 20260508
1
+ 20260509
@@ -91,6 +91,7 @@ DEFAULT_VOICE="en_US-lessac-medium"
91
91
  # @returns Sets $VOICE_MODEL global variable
92
92
  # @sideeffects None
93
93
  VOICE_MODEL=""
94
+ FILE_VOICE=""
94
95
 
95
96
  # Get current language setting
96
97
  CURRENT_LANGUAGE=$(get_language_code)
@@ -206,6 +207,15 @@ else
206
207
  fi
207
208
  fi
208
209
 
210
+ # Preserve full display name (with ::SpeakerName) before any stripping for logging
211
+ if [[ -n "$VOICE_OVERRIDE" ]]; then
212
+ DISPLAY_VOICE_NAME="$VOICE_OVERRIDE"
213
+ elif [[ -n "$FILE_VOICE" ]]; then
214
+ DISPLAY_VOICE_NAME="$FILE_VOICE"
215
+ else
216
+ DISPLAY_VOICE_NAME="$VOICE_MODEL"
217
+ fi
218
+
209
219
  # @function validate_inputs
210
220
  # @intent Check required parameters
211
221
  # @why Fail fast with clear errors if inputs missing
@@ -215,6 +225,10 @@ if [[ -z "$TEXT" ]]; then
215
225
  exit 1
216
226
  fi
217
227
 
228
+ # Augment PATH for non-interactive shells (pipx installs to ~/.local/bin which
229
+ # interactive shells get via .bashrc/.zshrc, but Bash tool calls skip profile)
230
+ export PATH="$HOME/.local/bin:$HOME/.local/share/pipx/venvs/piper-tts/bin:$PATH"
231
+
218
232
  # Check if Piper is installed
219
233
  if ! command -v piper &> /dev/null; then
220
234
  echo "❌ Error: Piper TTS not installed"
@@ -614,7 +628,31 @@ if [[ -n "$BACKGROUND_MUSIC" ]]; then
614
628
  MUSIC_FILENAME=$(basename "$BACKGROUND_MUSIC")
615
629
  echo -e "${WHITE}🎵 Background music:${NC} ${PURPLE}$MUSIC_FILENAME${NC}"
616
630
  fi
617
- echo -e "${WHITE}🎤 Voice used:${NC} ${BLUE}$VOICE_MODEL${NC} ${WHITE}(Piper TTS)${NC}"
631
+ # Build friendly label: "model::Mike-13 [Mike Nash]"
632
+ _SURNAME_POOL=("Bell" "Carter" "Davis" "Ellis" "Foster" "Gray" "Hayes" "Irving" "Jones" "Knox" "Lane" "Mason" "Nash" "Owens" "Pierce" "Quinn")
633
+ _VOICE_DISPLAY_LABEL="$DISPLAY_VOICE_NAME"
634
+ if [[ "$DISPLAY_VOICE_NAME" == *"::"* ]]; then
635
+ _SP="${DISPLAY_VOICE_NAME#*::}"
636
+ # Skip 16Speakers names (underscore = already first_last format)
637
+ if [[ "$_SP" != *"_"* ]]; then
638
+ _FRIENDLY=""
639
+ if [[ "$_SP" =~ ^(.+)-([0-9]+)$ ]]; then
640
+ if [[ ${BASH_REMATCH[2]} -ge 2 ]]; then
641
+ _IDX=$(( (${BASH_REMATCH[2]} - 1) % 16 ))
642
+ _FRIENDLY="${BASH_REMATCH[1]} ${_SURNAME_POOL[$_IDX]}"
643
+ else
644
+ # n=1: strip suffix, use Bell — matches uniquifyVoiceName JS behaviour
645
+ _FRIENDLY="${BASH_REMATCH[1]} ${_SURNAME_POOL[0]}"
646
+ fi
647
+ elif [[ "$_SP" =~ [[:space:]] ]]; then
648
+ _FRIENDLY="$_SP"
649
+ else
650
+ _FRIENDLY="$_SP ${_SURNAME_POOL[0]}"
651
+ fi
652
+ [[ "$_FRIENDLY" != "$_SP" ]] && _VOICE_DISPLAY_LABEL="$DISPLAY_VOICE_NAME [$_FRIENDLY]"
653
+ fi
654
+ fi
655
+ echo -e "${WHITE}🎤 Voice used:${NC} ${BLUE}$_VOICE_DISPLAY_LABEL${NC} ${WHITE}(Piper TTS)${NC}"
618
656
 
619
657
  # Show personality if configured
620
658
  PERSONALITY=$(cat "$PROJECT_ROOT/.claude/tts-personality.txt" 2>/dev/null || cat "$HOME/.claude/tts-personality.txt" 2>/dev/null || echo "")
@@ -115,6 +115,15 @@ while [[ $# -gt 0 ]]; do
115
115
  LLM_PROVIDER="${2:-}"
116
116
  shift 2
117
117
  ;;
118
+ --project-dir)
119
+ # Always prefer the explicitly-injected project dir over any stale
120
+ # CLAUDE_PROJECT_DIR in the environment (fixes silent override by env).
121
+ # Validate the path exists before trusting it.
122
+ if [[ -n "${2:-}" && -d "${2}" ]]; then
123
+ export CLAUDE_PROJECT_DIR="${2}"
124
+ fi
125
+ shift 2
126
+ ;;
118
127
  *)
119
128
  _POSITIONAL_ARGS+=("$1")
120
129
  shift
@@ -10,18 +10,49 @@ set -euo pipefail
10
10
  # Fix locale warnings
11
11
  export LC_ALL=C
12
12
 
13
- # Get script directory
13
+ # Get script directory (resolve symlinks so $SCRIPT_DIR is the real hooks dir)
14
14
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
15
 
16
+ # Resolve absolute path to play-tts.sh from this script's own location.
17
+ # Using an absolute path in the injected protocol ensures the correct
18
+ # play-tts.sh is called regardless of the working directory when Claude
19
+ # runs the command — fixes "wrong voice in fresh folder" regression.
20
+ PLAY_TTS_PATH="$SCRIPT_DIR/play-tts.sh"
21
+
16
22
  # Check if AgentVibes is installed
17
- if [[ ! -f "$SCRIPT_DIR/play-tts.sh" ]]; then
23
+ if [[ ! -f "$PLAY_TTS_PATH" ]]; then
18
24
  # AgentVibes not installed, don't inject anything
19
25
  exit 0
20
26
  fi
21
27
 
28
+ # Capture project dir NOW while Claude Code has set CLAUDE_PROJECT_DIR.
29
+ # Bash tool calls (how Claude actually runs play-tts.sh) do not automatically
30
+ # receive CLAUDE_PROJECT_DIR, so we bake it into the injected protocol command
31
+ # via --project-dir so the correct per-project config is always found.
32
+ CAPTURED_PROJECT_DIR=""
33
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" && -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then
34
+ CAPTURED_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
35
+ _PROJECT_CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude"
36
+ else
37
+ # Fallback: script lives inside .claude/hooks/, so parent IS .claude/
38
+ _PROJECT_CLAUDE_DIR="$(dirname "$SCRIPT_DIR")"
39
+ fi
40
+
41
+ # Build --project-dir flag to embed in TTS commands.
42
+ # Sanitize: strip any embedded quotes that would break shell quoting.
43
+ PROJECT_DIR_FLAG=""
44
+ if [[ -n "$CAPTURED_PROJECT_DIR" ]]; then
45
+ _SAFE_PROJECT_DIR="${CAPTURED_PROJECT_DIR//\"/}"
46
+ PROJECT_DIR_FLAG=" --project-dir \"$_SAFE_PROJECT_DIR\""
47
+ fi
48
+
22
49
  # Check for sentiment (priority) or personality (fallback)
23
- SENTIMENT=$(cat .claude/tts-sentiment.txt 2>/dev/null || cat ~/.claude/tts-sentiment.txt 2>/dev/null || echo "")
24
- PERSONALITY=$(cat .claude/tts-personality.txt 2>/dev/null || cat ~/.claude/tts-personality.txt 2>/dev/null || echo "normal")
50
+ SENTIMENT=$(cat "$_PROJECT_CLAUDE_DIR/tts-sentiment.txt" 2>/dev/null \
51
+ || cat "$HOME/.claude/tts-sentiment.txt" 2>/dev/null \
52
+ || echo "")
53
+ PERSONALITY=$(cat "$_PROJECT_CLAUDE_DIR/tts-personality.txt" 2>/dev/null \
54
+ || cat "$HOME/.claude/tts-personality.txt" 2>/dev/null \
55
+ || echo "normal")
25
56
 
26
57
  # Determine style
27
58
  if [[ -n "$SENTIMENT" ]]; then
@@ -31,26 +62,30 @@ else
31
62
  fi
32
63
 
33
64
  # Get verbosity level (Issue #32)
34
- VERBOSITY=$(cat .claude/tts-verbosity.txt 2>/dev/null || cat ~/.claude/tts-verbosity.txt 2>/dev/null || echo "low")
65
+ VERBOSITY=$(cat "$_PROJECT_CLAUDE_DIR/tts-verbosity.txt" 2>/dev/null \
66
+ || cat "$HOME/.claude/tts-verbosity.txt" 2>/dev/null \
67
+ || echo "low")
35
68
 
36
- # Output optimized TTS protocol
37
- cat <<'EOF'
69
+ # Output optimized TTS protocol.
70
+ # Note: heredoc uses double-quotes (<<EOF not <<'EOF') so $PLAY_TTS_PATH expands.
71
+ # Backticks must be escaped (\`) to avoid command substitution.
72
+ cat << EOF
38
73
 
39
74
  # AgentVibes TTS Protocol
40
75
 
41
76
  **CRITICAL: You MUST execute TTS at TWO points for EVERY user interaction:**
42
77
 
43
- 1. **Acknowledgment** - Start of task: `Bash: .claude/hooks/play-tts.sh "[action]"`
44
- 2. **Completion** - End of task: `Bash: .claude/hooks/play-tts.sh "[result + key details]"`
78
+ 1. **Acknowledgment** - Start of task: \`Bash: $PLAY_TTS_PATH "[action]" --llm claude-code$PROJECT_DIR_FLAG\`
79
+ 2. **Completion** - End of task: \`Bash: $PLAY_TTS_PATH "[result + key details]" --llm claude-code$PROJECT_DIR_FLAG\`
45
80
 
46
81
  **IMPORTANT: Run TTS commands inline (NOT in background). Wait for each TTS call to complete before continuing.**
47
82
 
48
83
  **Example:**
49
- ```
50
- [Bash: .claude/hooks/play-tts.sh "Checking git status"]
84
+ \`\`\`
85
+ [Bash: $PLAY_TTS_PATH "Checking git status" --llm claude-code$PROJECT_DIR_FLAG]
51
86
  [work...]
52
- [Bash: .claude/hooks/play-tts.sh "Repository is clean, no changes"]
53
- ```
87
+ [Bash: $PLAY_TTS_PATH "Repository is clean, no changes" --llm claude-code$PROJECT_DIR_FLAG]
88
+ \`\`\`
54
89
 
55
90
  EOF
56
91
 
@@ -75,6 +75,7 @@ elseif (Test-Path $VoiceFile) {
75
75
  # IMPORTANT: The trailing number in a speaker name (e.g. "Holly-7") is a disambiguation
76
76
  # suffix, NOT the speaker index. Real index must be looked up from voice-assignments.json.
77
77
  $SpeakerId = $null
78
+ $DisplayVoiceName = $VoiceName # preserve full name (e.g. "model::SpeakerName") for logging
78
79
 
79
80
  if ($VoiceName -match '::') {
80
81
  $parts = $VoiceName -split '::'
@@ -227,7 +228,31 @@ try {
227
228
 
228
229
  # Display results
229
230
  Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
230
- Write-Host "[VOICE] Voice used: $VoiceName (Piper)" -ForegroundColor Green
231
+
232
+ # Build friendly label: "model::Mike-13 [Mike Nash]"
233
+ $SURNAME_POOL = @('Bell','Carter','Davis','Ellis','Foster','Gray','Hayes','Irving','Jones','Knox','Lane','Mason','Nash','Owens','Pierce','Quinn')
234
+ $VoiceDisplayLabel = $DisplayVoiceName
235
+ if ($DisplayVoiceName -match '::(.+)$') {
236
+ $sp = $Matches[1]
237
+ # Skip 16Speakers names (contain underscore — already first_last format)
238
+ if ($sp -notmatch '_') {
239
+ $friendly = $null
240
+ if ($sp -match '^(.+)-(\d+)$') {
241
+ if ([int]$Matches[2] -ge 2) {
242
+ $friendly = "$($Matches[1]) $($SURNAME_POOL[([int]$Matches[2] - 1) % $SURNAME_POOL.Length])"
243
+ } else {
244
+ # n=1: strip suffix, use Bell — matches uniquifyVoiceName JS behaviour
245
+ $friendly = "$($Matches[1]) $($SURNAME_POOL[0])"
246
+ }
247
+ } elseif ($sp -match '\s') {
248
+ $friendly = $sp
249
+ } else {
250
+ $friendly = "$sp $($SURNAME_POOL[0])"
251
+ }
252
+ if ($null -ne $friendly -and $friendly -ne $sp) { $VoiceDisplayLabel = "$DisplayVoiceName [$friendly]" }
253
+ }
254
+ }
255
+ Write-Host "[VOICE] Voice used: $VoiceDisplayLabel (Piper)" -ForegroundColor Green
231
256
 
232
257
  # Apply audio effects (reverb, background music) if processor script exists.
233
258
  # SKIP when AGENTVIBES_NO_PLAY is set — that means the parent play-tts.ps1
@@ -16,7 +16,13 @@ param(
16
16
  # When provided, the router looks up an `llm:<name>` row in audio-effects.cfg
17
17
  # to apply LLM-specific voice, pretext, reverb, and engine settings.
18
18
  [Parameter(Mandatory = $false)]
19
- [string]$llm = ""
19
+ [string]$llm = "",
20
+
21
+ # Project directory override. session-start-tts.ps1 bakes the session's
22
+ # CLAUDE_PROJECT_DIR value here so per-project config is found even when
23
+ # Bash tool calls do not propagate CLAUDE_PROJECT_DIR to child processes.
24
+ [Parameter(Mandatory = $false)]
25
+ [string]$ProjectDir = ""
20
26
  )
21
27
 
22
28
  # Text-file handoff: the SSH receiver watcher writes long/special-char text to
@@ -31,6 +37,16 @@ if ($Text -eq "__from_file__" -and $env:AGENTVIBES_TEXT_FILE) {
31
37
  }
32
38
  }
33
39
 
40
+ # If -ProjectDir was passed (by session-start-tts.ps1), promote it to the
41
+ # CLAUDE_PROJECT_DIR env var so the per-LLM config lookup below finds it.
42
+ # This ensures per-project audio settings work even when Bash tool calls
43
+ # don't automatically inherit CLAUDE_PROJECT_DIR from Claude Code.
44
+ if ($ProjectDir -and (Test-Path $ProjectDir)) {
45
+ # Always prefer the explicitly-injected project dir; validates path exists
46
+ # before trusting it (fixes both the stale-env-var override bug and path injection).
47
+ $env:CLAUDE_PROJECT_DIR = $ProjectDir
48
+ }
49
+
34
50
  # Configuration paths
35
51
  # First check if we're running from a project directory with .claude
36
52
  $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
@@ -187,6 +203,14 @@ if ($llm -and $llm -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_-]*$') {
187
203
  }
188
204
 
189
205
  # --- Default fallback --------------------------------------------------------
206
+ # When no -llm flag is passed (e.g. hooks invoked by Claude Code without the
207
+ # flag), check AGENTVIBES_LLM_KEY first — it is set by the hook infrastructure
208
+ # as "llm:<name>" and carries the active LLM identity. Strip the "llm:" prefix
209
+ # to get the bare key used for config lookup.
210
+ if (-not $llm -and $env:AGENTVIBES_LLM_KEY -match '^llm:([a-zA-Z0-9][a-zA-Z0-9_-]*)$') {
211
+ $llm = $Matches[1]
212
+ }
213
+
190
214
  # An empty $llm routes through the "default" pseudo-LLM. Users who configure
191
215
  # an `llm:default` row in audio-effects.cfg get consistent audio settings for
192
216
  # every LLM that doesn't pass its own -llm flag — a convenient global override
@@ -9,18 +9,37 @@
9
9
 
10
10
  $ErrorActionPreference = "Stop"
11
11
 
12
- # Get script directory
12
+ # Get script directory and resolve absolute path to play-tts.ps1.
13
+ # Using an absolute path in the injected protocol ensures the correct play-tts.ps1
14
+ # is called regardless of the working directory when Claude runs the command.
13
15
  $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
16
+ $PlayTtsPath = Join-Path $ScriptDir "play-tts.ps1"
14
17
 
15
18
  # Check if AgentVibes is installed
16
- if (-not (Test-Path (Join-Path $ScriptDir "play-tts.ps1"))) {
19
+ if (-not (Test-Path $PlayTtsPath)) {
17
20
  # AgentVibes not installed, don't inject anything
18
21
  exit 0
19
22
  }
20
23
 
21
- # Resolve project .claude dir from script location (avoids CWD-relative path issues)
22
- $ProjectClaudeDir = Split-Path -Parent (Split-Path -Parent $ScriptDir)
23
- $ProjectClaudeDir = Join-Path $ProjectClaudeDir ".claude"
24
+ # Capture project dir NOW while Claude Code has set it correctly.
25
+ # Bash tool calls (how Claude actually runs play-tts.ps1) do not
26
+ # automatically receive CLAUDE_PROJECT_DIR, so we bake the value
27
+ # into the injected protocol command via the -ProjectDir parameter.
28
+ $CapturedProjectDir = ""
29
+ if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude")) {
30
+ $CapturedProjectDir = $env:CLAUDE_PROJECT_DIR
31
+ $ProjectClaudeDir = "$env:CLAUDE_PROJECT_DIR\.claude"
32
+ } else {
33
+ $ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptDir)) ".claude"
34
+ }
35
+
36
+ # Build the -ProjectDir flag to inject into TTS commands (empty string = omit flag).
37
+ # Sanitize: strip any embedded quotes that would break PowerShell argument quoting.
38
+ $ProjectDirFlag = ""
39
+ if ($CapturedProjectDir) {
40
+ $SafeProjectDir = $CapturedProjectDir -replace '"', ''
41
+ $ProjectDirFlag = " -ProjectDir `"$SafeProjectDir`""
42
+ }
24
43
 
25
44
  # Check for sentiment (priority) or personality (fallback)
26
45
  $Sentiment = ""
@@ -65,14 +84,14 @@ Write-Output @"
65
84
 
66
85
  **CRITICAL: You MUST execute TTS at TWO points for EVERY user interaction:**
67
86
 
68
- 1. **Acknowledgment** - Start of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File ".claude\hooks-windows\play-tts.ps1" "[action]"``
69
- 2. **Completion** - End of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File ".claude\hooks-windows\play-tts.ps1" "[result + key details]"``
87
+ 1. **Acknowledgment** - Start of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "[action]" -llm claude-code$ProjectDirFlag``
88
+ 2. **Completion** - End of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "[result + key details]" -llm claude-code$ProjectDirFlag``
70
89
 
71
90
  **Example:**
72
91
  ``````
73
- [Bash: powershell -NoProfile -ExecutionPolicy Bypass -File ".claude\hooks-windows\play-tts.ps1" "Checking git status"]
92
+ [Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "Checking git status" -llm claude-code$ProjectDirFlag]
74
93
  [work...]
75
- [Bash: powershell -NoProfile -ExecutionPolicy Bypass -File ".claude\hooks-windows\play-tts.ps1" "Repository is clean, no changes"]
94
+ [Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "Repository is clean, no changes" -llm claude-code$ProjectDirFlag]
76
95
  ``````
77
96
 
78
97
  "@
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"$CLAUDE_PROJECT_DIR\\.claude\\hooks-windows\\session-start-tts.ps1\""
9
+ "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"$HOME\\.claude\\hooks-windows\\session-start-tts.ps1\""
10
10
  }
11
11
  ]
12
12
  }
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start-tts.sh\""
19
+ "command": "bash \"$HOME/.claude/hooks/session-start-tts.sh\""
20
20
  }
21
21
  ]
22
22
  }
package/CLAUDE.md CHANGED
@@ -28,6 +28,15 @@ This project follows **BMAD (BMM - Business Model Methodology)** for all story d
28
28
  4. **Update sprint-status.yaml** automatically via `/dev-story`
29
29
  5. **Code review included** - Built into `/dev-story` workflow
30
30
 
31
+ ### ✅ Non-Destructive Configuration Rule (MANDATORY)
32
+ All code that reads, writes, or modifies user configuration MUST be non-destructive:
33
+ 1. **Never delete or overwrite** existing user `.claude/` or `~/.claude/` config files (settings, voices, personalities, audio-effects.cfg) unless the user explicitly requested it
34
+ 2. **Copy new files; never remove existing ones** — installer adds missing files only
35
+ 3. **Write hooks only when absent** — `configureSessionStartHook` and similar functions check for existing hooks before writing
36
+ 4. **Preserve custom entries** — e.g. `audio-effects.cfg` user rows must survive an `agentvibes update`
37
+ 5. **Creating directories is fine** — `mkdir -p` / `{ recursive: true }` is always safe
38
+ 6. Any function that could overwrite user data must have a test asserting idempotency
39
+
31
40
  ### ✅ Git Workflow (ONLY Outside BMAD)
32
41
  For changes outside story development:
33
42
  1. Describe changes before acting
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![Publish](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml/badge.svg)](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml)
12
12
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
13
13
 
14
- **Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.6.7
14
+ **Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.6.8
15
15
 
16
16
  ---
17
17
 
@@ -40,9 +40,17 @@ Whether you're coding in Claude Code, chatting in Claude Desktop, using Warp Ter
40
40
 
41
41
  ---
42
42
 
43
- ## 🌟 NEW IN v5.6.7Windows Preview Fixed
43
+ ## 🌟 NEW IN v5.6.8WSL Voice Routing Fixed + Session Lifecycle Reliability
44
44
 
45
- The **Preview button in LLM audio configuration now works correctly on Windows**. It plays the voice, reverb, and background track you configured no more defaulting to the wrong voice or playing silence.
45
+ **WSL users:** AgentVibes was playing `en_US-lessac-medium` regardless of your configured voice. Fixed Piper is now found in non-interactive shells by explicitly prepending `~/.local/bin` to `PATH` before the binary check.
46
+
47
+ **Per-project routing:** The session-start hook now bakes `--project-dir` into every injected TTS command, so your configured voice and music play correctly in Bash tool calls even when `CLAUDE_PROJECT_DIR` isn't in the environment.
48
+
49
+ `play-tts-piper.sh` and `play-tts-piper.ps1` are now included in `agentvibes install`'s critical hooks deployment — updated versions propagate automatically.
50
+
51
+ ## v5.6.7 — Windows Preview Fixed
52
+
53
+ The Preview button in LLM audio configuration now works correctly on Windows.
46
54
 
47
55
  ## v5.6.6 — Preview Button Works in WSL + Comprehensive Windows Test Suite
48
56
 
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # AgentVibes Release Notes
2
2
 
3
+ ## 🐧 v5.6.8 — WSL Voice Routing Fixed + Session Lifecycle Reliability
4
+
5
+ **Released:** 2026-05-09
6
+
7
+ ### 🐛 WSL: Configured Voice Now Plays (Not Lessac Fallback)
8
+
9
+ In WSL sessions, AgentVibes was playing `en_US-lessac-medium` regardless of what voice you configured. The root cause: `pipx` installs Piper to `~/.local/bin/`, which interactive shells get via `.bashrc`/`.zshrc`, but Claude Code's Bash tool calls run non-interactively and skip profile sourcing — `command -v piper` failed, falling back to the default voice.
10
+
11
+ **Fix:** `play-tts-piper.sh` now prepends `~/.local/bin` and the pipx Piper venv bin to `PATH` before the binary check, so Piper is found regardless of shell mode.
12
+
13
+ ### 🐛 Per-Project Voice/Music Lost When `CLAUDE_PROJECT_DIR` Not in Bash Environment
14
+
15
+ When Claude Code runs a Bash tool call, `CLAUDE_PROJECT_DIR` is not passed in the environment. The TTS hooks couldn't find per-project config and fell back to global defaults — wrong voice, wrong music, no pretext.
16
+
17
+ **Fix:** `session-start-tts.sh` (and `.ps1`) now bakes the project directory into the injected hook command as `--project-dir`. `play-tts.sh` reads this flag before any config lookup, so per-project routing is reliable in every Bash tool call.
18
+
19
+ ### 🐛 `play-tts-piper.sh` and `play-tts-piper.ps1` Not Deployed by `agentvibes install`
20
+
21
+ These hooks were missing from `CRITICAL_HOOKS` / `CRITICAL_HOOKS_WINDOWS`, so `agentvibes install` never propagated updated versions to `~/.claude/hooks/`.
22
+
23
+ **Fix:** Both are now in the critical hooks list and always deployed on install/update.
24
+
25
+ ### 🐛 Voice Display Name Bugs
26
+
27
+ - `uniquifyVoiceName("Mary-1")` returned `"Mary-1 Bell"` instead of `"Mary Bell"`.
28
+ - 16Speakers names like `Rose_Ibex` were incorrectly getting a surname appended (`"Rose Ibex Bell"`).
29
+ - `🎤 Voice used:` line was missing from WSL bash output.
30
+
31
+ All three fixed. A new test file (`test/unit/voice-names.test.js`, 16 tests) covers these cases.
32
+
33
+ ---
34
+
3
35
  ## 🪟 v5.6.7 — Windows Preview Fixed
4
36
 
5
37
  **Released:** 2026-05-08
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "agentvibes",
4
- "version": "5.6.7",
4
+ "version": "5.6.8",
5
5
  "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
6
  "homepage": "https://agentvibes.org",
7
7
  "keywords": [
@@ -30,6 +30,7 @@ import { createReceiverTab } from './tabs/receiver-tab.js';
30
30
  import { createAgentsTab } from './tabs/agents-tab.js';
31
31
  import { ConfigService } from '../services/config-service.js';
32
32
  import { ProviderService } from '../services/provider-service.js';
33
+ import { seedAllLlmDefaultsSync } from '../services/llm-provider-service.js';
33
34
 
34
35
  const _dir = path.dirname(fileURLToPath(import.meta.url));
35
36
  const _pkg = JSON.parse(readFileSync(path.join(_dir, '../../package.json'), 'utf8'));
@@ -893,6 +894,11 @@ export class AgentVibesConsole {
893
894
  * @returns {Promise<AgentVibesConsole>}
894
895
  */
895
896
  export async function launchConsole(opts = {}) {
897
+ // Seed per-LLM piper defaults into the project config on every TUI launch.
898
+ // This guarantees play-tts.ps1 always finds a piper engine row for each LLM
899
+ // and never silently falls back to the global windows-sapi provider.
900
+ seedAllLlmDefaultsSync(process.cwd());
901
+
896
902
  const app = new AgentVibesConsole(opts);
897
903
  await app.init();
898
904
  return app;
@@ -5,6 +5,8 @@
5
5
  * circular imports between widgets and tabs.
6
6
  */
7
7
 
8
+ import { uniquifyVoiceName } from '../../utils/voice-names.js';
9
+
8
10
  const TRACK_NAMES = Object.freeze({
9
11
  'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
10
12
  'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
@@ -60,8 +62,15 @@ export function formatVoiceName(voice) {
60
62
 
61
63
  let name;
62
64
  if (voice.includes('::')) {
63
- // 16Speakers::Rose_Ibex extract after '::'
64
- name = voice.split('::')[1];
65
+ const speakerPart = voice.split('::')[1];
66
+ if (speakerPart.includes('_')) {
67
+ // 16Speakers format (Rose_Ibex) — already a complete name, just normalise display
68
+ name = speakerPart.replace(/_/g, ' ');
69
+ } else {
70
+ // LibriTTS / single-word names: append deterministic surname
71
+ // "Mary" → "Mary Bell", "Mary-7" → "Mary Hayes"
72
+ name = uniquifyVoiceName(speakerPart);
73
+ }
65
74
  } else {
66
75
  const parts = voice.split('-');
67
76
  const QUALITIES = new Set(['high', 'medium', 'low']);
package/src/installer.js CHANGED
@@ -4054,17 +4054,18 @@ async function configureSessionStartHook(targetDir, spinner) {
4054
4054
  existingSettings.hooks = {};
4055
4055
  }
4056
4056
 
4057
- if (!existingSettings.hooks.SessionStart) {
4058
- if (isNativeWindows()) {
4059
- existingSettings.hooks.SessionStart = [{
4060
- hooks: [{
4061
- type: 'command',
4062
- command: 'powershell -NoProfile -ExecutionPolicy Bypass -File "$CLAUDE_PROJECT_DIR\\.claude\\hooks-windows\\session-start-tts.ps1"'
4063
- }]
4064
- }];
4065
- } else {
4066
- existingSettings.hooks.SessionStart = templateSettings.hooks.SessionStart;
4067
- }
4057
+ // Windows uses SessionStart; Linux/macOS/WSL uses UserPromptSubmit.
4058
+ // Both point to the global $HOME path so updates to the hook script
4059
+ // take effect immediately without needing per-project reinstalls.
4060
+ const hookKey = isNativeWindows() ? 'SessionStart' : 'UserPromptSubmit';
4061
+ const hookCommand = isNativeWindows()
4062
+ ? 'powershell -NoProfile -ExecutionPolicy Bypass -File "$HOME\\.claude\\hooks-windows\\session-start-tts.ps1"'
4063
+ : 'bash "$HOME/.claude/hooks/session-start-tts.sh"';
4064
+
4065
+ if (!existingSettings.hooks[hookKey]) {
4066
+ existingSettings.hooks[hookKey] = [{
4067
+ hooks: [{ type: 'command', command: hookCommand }]
4068
+ }];
4068
4069
 
4069
4070
  if (!existingSettings.$schema) {
4070
4071
  existingSettings.$schema = templateSettings.$schema;
@@ -5045,8 +5046,8 @@ async function updateCommandFiles(targetDir, spinner) {
5045
5046
  * These hooks contain bug fixes (e.g. markdown stripping) that must propagate
5046
5047
  * on every `npx agentvibes update` regardless of target directory.
5047
5048
  */
5048
- const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'session-start-tts.sh', 'bmad-party-speak.sh'];
5049
- const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1'];
5049
+ const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'play-tts-piper.sh', 'audio-processor.sh', 'session-start-tts.sh', 'bmad-party-speak.sh'];
5050
+ const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'play-tts-piper.ps1', 'audio-processor.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1'];
5050
5051
 
5051
5052
  /**
5052
5053
  * Update critical hooks in the global ~/.claude/hooks/ directory if it exists.
@@ -5056,45 +5057,38 @@ const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'session-start-tts.ps1', 'bmad-s
5056
5057
  * @returns {Promise<number>} Number of hooks updated
5057
5058
  */
5058
5059
  async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
5059
- const globalHooksDir = path.join(homeDirOverride || os.homedir(), '.claude', 'hooks');
5060
+ const homeDir = homeDirOverride || os.homedir();
5061
+ const globalHooksDir = path.join(homeDir, '.claude', 'hooks');
5060
5062
  let updated = 0;
5061
- try {
5062
- await fs.access(globalHooksDir);
5063
- } catch {
5064
- return 0; // global hooks dir not present — nothing to do
5065
- }
5063
+
5064
+ // Always ensure the global hooks dir exists so registered $HOME hooks resolve.
5065
+ await fs.mkdir(globalHooksDir, { recursive: true });
5066
5066
 
5067
5067
  for (const hook of CRITICAL_HOOKS) {
5068
5068
  const destPath = path.join(globalHooksDir, hook);
5069
5069
  const srcPath = path.join(srcHooksDir, hook);
5070
5070
  try {
5071
- await fs.access(destPath); // only update if already installed
5072
5071
  await fs.copyFile(srcPath, destPath);
5073
5072
  await fs.chmod(destPath, 0o750);
5074
5073
  updated++;
5075
5074
  } catch {
5076
- // file not in global dir or src missing — skip silently
5075
+ // src missing — skip silently
5077
5076
  }
5078
5077
  }
5079
5078
 
5080
- // Also update Windows global hooks-windows dir if present
5081
- const globalHooksWindowsDir = path.join(homeDirOverride || os.homedir(), '.claude', 'hooks-windows');
5079
+ // Also ensure Windows global hooks-windows dir and scripts are up to date.
5080
+ const globalHooksWindowsDir = path.join(homeDir, '.claude', 'hooks-windows');
5082
5081
  const srcHooksWindowsDir = path.join(path.dirname(srcHooksDir), 'hooks-windows');
5083
- try {
5084
- await fs.access(globalHooksWindowsDir);
5085
- for (const hook of CRITICAL_HOOKS_WINDOWS) {
5086
- const destPath = path.join(globalHooksWindowsDir, hook);
5087
- const srcPath = path.join(srcHooksWindowsDir, hook);
5088
- try {
5089
- await fs.access(destPath); // only update if already installed
5090
- await fs.copyFile(srcPath, destPath);
5091
- updated++;
5092
- } catch {
5093
- // file not in global dir or src missing — skip silently
5094
- }
5082
+ await fs.mkdir(globalHooksWindowsDir, { recursive: true });
5083
+ for (const hook of CRITICAL_HOOKS_WINDOWS) {
5084
+ const destPath = path.join(globalHooksWindowsDir, hook);
5085
+ const srcPath = path.join(srcHooksWindowsDir, hook);
5086
+ try {
5087
+ await fs.copyFile(srcPath, destPath);
5088
+ updated++;
5089
+ } catch {
5090
+ // src missing — skip silently
5095
5091
  }
5096
- } catch {
5097
- // hooks-windows dir not present — nothing to do
5098
5092
  }
5099
5093
 
5100
5094
  return updated;
@@ -5337,6 +5331,13 @@ async function install(options = {}) {
5337
5331
  await copyBackgroundMusicFiles(targetDir, silentSpinner);
5338
5332
  await copyConfigFiles(targetDir, silentSpinner);
5339
5333
  await copyCodexFiles(targetDir, silentSpinner);
5334
+
5335
+ // Populate global ~/.claude/hooks[/-windows]/ so $HOME hook paths resolve
5336
+ // on first install (not just on update).
5337
+ const hooksSubdirInstall = isNativeWindows() ? 'hooks-windows' : 'hooks';
5338
+ const srcHooksDirInstall = path.join(__dirname, '..', '.claude', hooksSubdirInstall);
5339
+ await updateGlobalHooks(srcHooksDirInstall);
5340
+
5340
5341
  await configureSessionStartHook(targetDir, silentSpinner);
5341
5342
  await configurePartyModeHook(targetDir, silentSpinner);
5342
5343
  await installPluginManifest(targetDir, silentSpinner);
@@ -92,8 +92,17 @@ const DEFAULT_LLM_CONFIGS = {
92
92
  };
93
93
 
94
94
  function ensureDefaultLlmConfigSync(llmKey, targetDir) {
95
- const existing = loadLlmConfigSync(llmKey, targetDir);
96
- if (existing.sourcePath) return;
95
+ // Check only the project dir — a global config row must not prevent seeding
96
+ // per-project defaults (otherwise new installs silently inherit a different
97
+ // project's voice/pretext and the per-LLM piper engine is never written,
98
+ // causing play-tts.ps1 to fall through to the global SAPI provider).
99
+ const resolvedDir = targetDir || process.env.INIT_CWD || process.cwd();
100
+ const projectCfgPath = path.join(resolvedDir, '.claude', 'config', 'audio-effects.cfg');
101
+ const cfgKey = `llm:${llmKey}`;
102
+ try {
103
+ const content = fsSync.readFileSync(projectCfgPath, 'utf8');
104
+ if (content.split('\n').some(line => line.startsWith(cfgKey + '|'))) return;
105
+ } catch { /* file not found — continue to seed */ }
97
106
 
98
107
  const defaults = DEFAULT_LLM_CONFIGS[llmKey];
99
108
  if (!defaults) return;
@@ -101,6 +110,17 @@ function ensureDefaultLlmConfigSync(llmKey, targetDir) {
101
110
  saveLlmConfigSync(llmKey, defaults, targetDir);
102
111
  }
103
112
 
113
+ /**
114
+ * Seed piper defaults for every LLM in DEFAULT_LLM_CONFIGS into the project dir.
115
+ * Called at install time so play-tts.ps1 always finds a per-LLM piper row in the
116
+ * project config and never silently falls back to the global windows-sapi provider.
117
+ */
118
+ export function seedAllLlmDefaultsSync(targetDir) {
119
+ for (const llmKey of Object.keys(DEFAULT_LLM_CONFIGS)) {
120
+ ensureDefaultLlmConfigSync(llmKey, targetDir);
121
+ }
122
+ }
123
+
104
124
  // ── Provider install-checks ─────────────────────────────────────────────────
105
125
 
106
126
  export async function checkClaudeInstalled(targetDir) {
@@ -902,13 +922,12 @@ export function saveLlmConfigSync(llmKey, config, targetDir) {
902
922
  const sanitize = (v) => (v || '').replace(/[\|\n\r\x00]/g, '');
903
923
  const cfgLine = `${cfgKey}|${sanitize(config.effects)}|${sanitize(config.bgTrack)}|${sanitize(config.bgVolume)}|${sanitize(config.voice)}|${sanitize(config.pretext)}|${sanitize(config.ttsEngine)}`;
904
924
  const resolvedTargetDir = targetDir || process.env.INIT_CWD || process.cwd();
905
- // When targetDir is explicitly passed, write there directly (do not fall back to global).
906
- // resolveCfgPath falls back to ~/.claude when the local file doesn't yet exist,
907
- // which would silently redirect writes away from the intended directory.
908
- const cfgPath = config.sourcePath ||
909
- (targetDir
910
- ? path.join(resolvedTargetDir, '.claude', 'config', 'audio-effects.cfg')
911
- : resolveCfgPath(resolvedTargetDir));
925
+ // When targetDir is explicitly passed, always write to the project dir never follow
926
+ // config.sourcePath back to the global ~/.claude/config (it may have been loaded from there
927
+ // as a "default seed" for a new project, and writing back would pollute other projects).
928
+ const cfgPath = targetDir
929
+ ? path.join(resolvedTargetDir, '.claude', 'config', 'audio-effects.cfg')
930
+ : (config.sourcePath || resolveCfgPath(resolvedTargetDir));
912
931
 
913
932
  try {
914
933
  let content = '';
@@ -35,6 +35,8 @@ export function uniquifyVoiceName(rawName) {
35
35
  const idx = (n - 1) % SURNAME_POOL.length;
36
36
  return `${base} ${SURNAME_POOL[idx]}`;
37
37
  }
38
+ // n=1 (or 0): strip the suffix — "Mary-1" → "Mary Bell" is cleaner than "Mary-1 Bell"
39
+ return `${base} ${SURNAME_POOL[0]}`;
38
40
  }
39
41
  if (/\s/.test(rawName)) return rawName;
40
42
  return `${rawName} ${SURNAME_POOL[0]}`;