agentvibes 4.5.0 → 4.6.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.
@@ -1,3 +1,4 @@
1
+ bmad-agent-tech-writer||agent_vibes_arabic_v2_loop.mp3|0.70
1
2
  # AgentVibes Audio Effects Configuration|||
2
3
  # Format: AGENT_NAME|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME
3
4
  #|||
@@ -343,7 +343,7 @@ main() {
343
343
  if [[ "$_prof_vol" =~ ^[0-9]+$ ]]; then
344
344
  bg_volume=$(awk "BEGIN{printf \"%.2f\", ${_prof_vol}/100}")
345
345
  else
346
- bg_volume="0.70"
346
+ bg_volume="0.20"
347
347
  fi
348
348
  fi
349
349
  fi
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: ~/.claude/hooks/bmad-party-speak.sh
4
+ #
5
+ # AgentVibes PostToolUse Hook - BMAD Party Mode TTS (Linux / macOS / WSL)
6
+ #
7
+ # Fires after every Agent tool call. Detects BMAD party mode agents by
8
+ # fingerprinting the prompt, extracts the agent display name, maps it to
9
+ # the canonical agent ID via the manifest, then calls bmad-speak.sh.
10
+ # Uses flock for cross-process audio serialization (no overlapping speech).
11
+ #
12
+ # Installed globally so it works in any BMAD project.
13
+ # Uses CLAUDE_PROJECT_DIR env var to locate the project manifest at runtime.
14
+ #
15
+ # Input: JSON on stdin (Claude Code PostToolUse payload)
16
+ #
17
+
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ LOCK_FILE="/tmp/agentvibes-party-queue.lock"
22
+
23
+ # --- Read stdin ---
24
+ raw="$(cat)"
25
+ [[ -z "$raw" ]] && exit 0
26
+
27
+ # --- Parse all needed fields in one python3 call (fixes M5: 3x subprocess, echo safety) ---
28
+ # Outputs: TOOL_NAME|DISPLAY_NAME|RESPONSE_TEXT (newlines in response encoded as \n literals)
29
+ parsed="$(printf '%s' "$raw" | python3 - <<'PYEOF'
30
+ import sys, json, re
31
+
32
+ try:
33
+ d = json.load(sys.stdin)
34
+ except Exception:
35
+ print("|||")
36
+ sys.exit(0)
37
+
38
+ tool_name = d.get('tool_name', '')
39
+ prompt = d.get('tool_input', {}).get('prompt', '')
40
+
41
+ # Extract display name — safe alternative to grep -oP (fixes C2: macOS BSD grep no -P)
42
+ display_name = ''
43
+ m = re.search(r'You are ([A-Za-z]+)\s*\(', prompt)
44
+ if m:
45
+ display_name = m.group(1)
46
+
47
+ # Extract response text
48
+ response_text = ''
49
+ for item in d.get('tool_response', {}).get('content', []):
50
+ if item.get('type') == 'text':
51
+ response_text = item['text']
52
+ break
53
+
54
+ # Strip leading icon + bold name header (e.g. "📊 **Mary:** " or garbled prefix)
55
+ response_text = response_text.strip()
56
+ response_text = re.sub(r'^\S*\s*\*\*[^:]+:\*\*\s*', '', response_text).strip()
57
+
58
+ # Encode newlines so we can pass multi-line text through a single shell variable (fixes m3)
59
+ response_text = response_text.replace('\n', '\\n')
60
+
61
+ print(f"{tool_name}|{display_name}|{response_text}")
62
+ PYEOF
63
+ )" 2>/dev/null || true
64
+
65
+ [[ -z "$parsed" ]] && exit 0
66
+
67
+ tool_name="${parsed%%|*}"
68
+ rest="${parsed#*|}"
69
+ display_name="${rest%%|*}"
70
+ response_text="${rest#*|}"
71
+
72
+ # Decode \n back to newlines for TTS
73
+ response_text="${response_text//\\n/ }"
74
+
75
+ # --- Only handle Agent tool ---
76
+ [[ "$tool_name" != "Agent" ]] && exit 0
77
+
78
+ # --- Fingerprint: only fire for BMAD party mode agents (safe string match, no pipe) ---
79
+ [[ "$raw" == *"BMAD agent in a collaborative roundtable"* ]] || exit 0
80
+
81
+ [[ -z "$display_name" ]] && exit 0
82
+ [[ -z "$response_text" ]] && exit 0
83
+
84
+ # --- Resolve project root ---
85
+ project_root="${CLAUDE_PROJECT_DIR:-}"
86
+
87
+ # --- Find bmad-speak.sh (prefer project-local, fall back to global) ---
88
+ bmad_speak=""
89
+ if [[ -n "$project_root" && -f "$project_root/.claude/hooks/bmad-speak.sh" ]]; then
90
+ bmad_speak="$project_root/.claude/hooks/bmad-speak.sh"
91
+ elif [[ -f "$SCRIPT_DIR/bmad-speak.sh" ]]; then
92
+ bmad_speak="$SCRIPT_DIR/bmad-speak.sh"
93
+ fi
94
+ [[ -z "$bmad_speak" ]] && exit 0
95
+
96
+ # --- Look up canonical agent ID from project manifest via python3 (fixes M4: awk CSV comma) ---
97
+ agent_id="$display_name" # fallback
98
+ if [[ -n "$project_root" && -f "$project_root/_bmad/_config/agent-manifest.csv" ]]; then
99
+ manifest="$project_root/_bmad/_config/agent-manifest.csv"
100
+ matched="$(python3 - "$manifest" "$display_name" <<'PYEOF'
101
+ import sys, csv
102
+ manifest_path, target = sys.argv[1], sys.argv[2].lower()
103
+ try:
104
+ with open(manifest_path, newline='', encoding='utf-8') as f:
105
+ for row in csv.DictReader(f):
106
+ if row.get('displayName', '').lower() == target:
107
+ print(row.get('name', ''))
108
+ break
109
+ except Exception:
110
+ pass
111
+ PYEOF
112
+ )" 2>/dev/null || true
113
+ [[ -n "$matched" ]] && agent_id="$matched"
114
+ fi
115
+
116
+ # --- Apply verbosity truncation ---
117
+ verbosity="medium"
118
+ # Guard project_root empty to avoid /.claude/... path (fixes M1)
119
+ if [[ -n "$project_root" && -f "$project_root/.claude/tts-verbosity.txt" ]]; then
120
+ v="$(tr -d '[:space:]' < "$project_root/.claude/tts-verbosity.txt")"
121
+ [[ -n "$v" ]] && verbosity="$v"
122
+ elif [[ -f "$HOME/.claude/tts-verbosity.txt" ]]; then
123
+ v="$(tr -d '[:space:]' < "$HOME/.claude/tts-verbosity.txt")"
124
+ [[ -n "$v" ]] && verbosity="$v"
125
+ fi
126
+
127
+ case "$verbosity" in
128
+ low)
129
+ # First sentence — fall back to full text if no punctuation (fixes m1)
130
+ first="$(printf '%s' "$response_text" | python3 -c "
131
+ import sys, re
132
+ t = sys.stdin.read()
133
+ m = re.match(r'^.*?[.!?]', t)
134
+ print(m.group(0) if m else t)
135
+ " 2>/dev/null || printf '%s' "$response_text")"
136
+ [[ -n "$first" ]] && response_text="$first"
137
+ ;;
138
+ medium)
139
+ # First 2 sentences — fall back to full text if no punctuation (fixes m1)
140
+ two="$(printf '%s' "$response_text" | python3 -c "
141
+ import sys, re
142
+ t = sys.stdin.read()
143
+ parts = re.findall(r'.*?[.!?]', t)
144
+ print(' '.join(parts[:2]) if parts else t)
145
+ " 2>/dev/null || printf '%s' "$response_text")"
146
+ [[ -n "$two" ]] && response_text="$two"
147
+ ;;
148
+ # high = full text
149
+ esac
150
+
151
+ [[ -z "$response_text" ]] && exit 0
152
+
153
+ # --- Acquire queue lock (flock: cross-process, auto-releases on crash) ---
154
+ exec 9>"$LOCK_FILE"
155
+ if command -v flock &>/dev/null; then
156
+ flock -w 60 9
157
+ "$bmad_speak" "$agent_id" "$response_text" || true
158
+ flock -u 9
159
+ else
160
+ # macOS fallback: atomic mkdir polling lock
161
+ LOCK_DIR="/tmp/agentvibes-party-queue.lock.d"
162
+ # Register trap BEFORE acquiring lock so SIGTERM can't orphan it (fixes M3)
163
+ trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
164
+ WAITED=0
165
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
166
+ sleep 0.5
167
+ WAITED=$((WAITED + 1))
168
+ if [[ $WAITED -ge 120 ]]; then
169
+ echo "[AgentVibes] Party mode TTS queue timeout for agent: $agent_id" >&2
170
+ exit 0
171
+ fi
172
+ done
173
+ "$bmad_speak" "$agent_id" "$response_text" || true
174
+ rmdir "$LOCK_DIR" 2>/dev/null || true
175
+ fi
@@ -177,6 +177,20 @@ if [[ -n "$AGENT_ID" ]] && [[ -f "$VOICE_MAP_FILE" ]]; then
177
177
  IFS='|' read -r PROFILE_VOICE PROFILE_PRETEXT PROFILE_REVERB PROFILE_PERSONALITY PROFILE_MUSIC_TRACK PROFILE_MUSIC_VOLUME PROFILE_MUSIC_ENABLED <<< "$_ALL_FIELDS"
178
178
  fi
179
179
 
180
+ # Read global background music volume as fallback (stored as 0.0-1.0, convert to 0-100 integer)
181
+ _BG_VOL_FILE="${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}/.claude/config/background-music-volume.txt"
182
+ if [[ ! -f "$_BG_VOL_FILE" ]]; then
183
+ _BG_VOL_FILE="$HOME/.claude/config/background-music-volume.txt"
184
+ fi
185
+ if [[ -f "$_BG_VOL_FILE" ]]; then
186
+ GLOBAL_BG_VOLUME=$(_BG_VOL_RAW=$(cat "$_BG_VOL_FILE") node -e "
187
+ const v = parseFloat(process.env._BG_VOL_RAW);
188
+ process.stdout.write(isNaN(v) ? '20' : String(Math.round(v * 100)));
189
+ " 2>/dev/null || echo "20")
190
+ else
191
+ GLOBAL_BG_VOLUME=20
192
+ fi
193
+
180
194
  # Fallback to bmad-voice-manager.sh if no profile voice found
181
195
  AGENT_VOICE="$PROFILE_VOICE"
182
196
  AGENT_INTRO="$PROFILE_PRETEXT"
@@ -203,7 +217,7 @@ if [[ -n "$PROFILE_REVERB" ]] || [[ -n "$PROFILE_PERSONALITY" ]] || [[ -n "$PROF
203
217
  # Write profile as JSON for reliable parsing downstream
204
218
  # SECURITY: Pass values via env vars to prevent shell injection
205
219
  _P_REVERB="$PROFILE_REVERB" _P_PERSONALITY="$PROFILE_PERSONALITY" \
206
- _P_MUSIC_TRACK="$PROFILE_MUSIC_TRACK" _P_MUSIC_VOL="${PROFILE_MUSIC_VOLUME:-70}" \
220
+ _P_MUSIC_TRACK="$PROFILE_MUSIC_TRACK" _P_MUSIC_VOL="${PROFILE_MUSIC_VOLUME:-$GLOBAL_BG_VOLUME}" \
207
221
  _P_MUSIC_ENABLED="$PROFILE_MUSIC_ENABLED" \
208
222
  _P_OUTFILE="$TEMP_PROFILE" node -e "
209
223
  const p = {};
@@ -211,7 +225,7 @@ if [[ -n "$PROFILE_REVERB" ]] || [[ -n "$PROFILE_PERSONALITY" ]] || [[ -n "$PROF
211
225
  if (process.env._P_PERSONALITY) p.personality = process.env._P_PERSONALITY;
212
226
  if (process.env._P_MUSIC_TRACK) p.backgroundMusic = {
213
227
  track: process.env._P_MUSIC_TRACK,
214
- volume: parseInt(process.env._P_MUSIC_VOL) || 70,
228
+ volume: parseInt(process.env._P_MUSIC_VOL) || 20,
215
229
  enabled: process.env._P_MUSIC_ENABLED === 'true'
216
230
  };
217
231
  require('fs').writeFileSync(process.env._P_OUTFILE, JSON.stringify(p), { mode: 0o600 });
@@ -0,0 +1,141 @@
1
+ #
2
+ # File: ~/.claude/hooks-windows/bmad-party-speak.ps1
3
+ #
4
+ # AgentVibes PostToolUse Hook - BMAD Party Mode TTS
5
+ #
6
+ # Fires after every Agent tool call. Detects BMAD party mode agents by
7
+ # fingerprinting the prompt, extracts the agent display name, maps it to
8
+ # the canonical agent ID via the manifest, then calls bmad-speak.ps1.
9
+ #
10
+ # Installed globally so it works in any BMAD project.
11
+ # Uses $env:CLAUDE_PROJECT_DIR to locate the project manifest at runtime.
12
+ #
13
+
14
+ try {
15
+ # --- Read stdin safely ---
16
+ $raw = [Console]::In.ReadToEnd()
17
+ if (-not $raw -or $raw.Trim() -eq "") { exit 0 }
18
+
19
+ $data = $raw | ConvertFrom-Json
20
+ if (-not $data) { exit 0 }
21
+
22
+ # --- Only handle Agent tool ---
23
+ if ($data.tool_name -ne "Agent") { exit 0 }
24
+
25
+
26
+ # --- Extract prompt ---
27
+ $prompt = $data.tool_input.prompt
28
+ if (-not $prompt) { exit 0 }
29
+
30
+ # --- Fingerprint: only fire for BMAD party mode agents ---
31
+ if ($prompt -notmatch "BMAD agent in a collaborative roundtable") { exit 0 }
32
+
33
+ # --- Extract display name from "You are {Name} (" ---
34
+ if ($prompt -notmatch "You are ([A-Za-z]+)\s*\(") { exit 0 }
35
+ $DisplayName = $Matches[1]
36
+
37
+ # --- Extract response text ---
38
+ $content = $data.tool_response.content
39
+ if (-not $content -or $content.Count -eq 0) { exit 0 }
40
+ $ResponseText = ($content | Where-Object { $_.type -eq "text" } | Select-Object -First 1).text
41
+ if (-not $ResponseText) { exit 0 }
42
+
43
+ # Strip leading icon + bold name header e.g. "📊 **Mary:**" or garbled "≡ƒôè **Mary:**"
44
+ # Trim first so leading newlines don't defeat the ^ anchor (fixes M7)
45
+ $ResponseText = $ResponseText.Trim()
46
+ $ResponseText = $ResponseText -replace '^\S*\s*\*\*[^:]+:\*\*\s*', ''
47
+ $ResponseText = $ResponseText.Trim()
48
+ if (-not $ResponseText) { exit 0 }
49
+
50
+ # --- Resolve paths ---
51
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
52
+ $ProjectRoot = $env:CLAUDE_PROJECT_DIR
53
+
54
+ # --- Find bmad-speak.ps1 (prefer project-local, fall back to global) ---
55
+ $BmadSpeak = $null
56
+ if ($ProjectRoot) {
57
+ $local = Join-Path $ProjectRoot ".claude\hooks-windows\bmad-speak.ps1"
58
+ if (Test-Path $local) { $BmadSpeak = $local }
59
+ }
60
+ if (-not $BmadSpeak) {
61
+ $global = Join-Path $ScriptDir "bmad-speak.ps1"
62
+ if (Test-Path $global) { $BmadSpeak = $global }
63
+ }
64
+ if (-not $BmadSpeak) { exit 0 }
65
+
66
+ # --- Look up canonical agent ID from project manifest (fixes m2: UTF8 encoding) ---
67
+ $AgentId = $DisplayName # fallback
68
+ if ($ProjectRoot) {
69
+ $ManifestFile = Join-Path $ProjectRoot "_bmad\_config\agent-manifest.csv"
70
+ if (Test-Path $ManifestFile) {
71
+ $rows = Import-Csv $ManifestFile -Encoding UTF8
72
+ foreach ($row in $rows) {
73
+ if ($row.displayName -ieq $DisplayName) {
74
+ $AgentId = $row.name
75
+ break
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ # --- Apply verbosity truncation ---
82
+ $Verbosity = "medium"
83
+ $verbosityPaths = @(
84
+ (Join-Path $env:USERPROFILE ".claude\tts-verbosity.txt")
85
+ )
86
+ if ($ProjectRoot) {
87
+ $verbosityPaths = @((Join-Path $ProjectRoot ".claude\tts-verbosity.txt")) + $verbosityPaths
88
+ }
89
+ foreach ($p in $verbosityPaths) {
90
+ if (Test-Path $p) {
91
+ $v = (Get-Content $p -Raw -ErrorAction SilentlyContinue).Trim()
92
+ if ($v) { $Verbosity = $v; break }
93
+ }
94
+ }
95
+
96
+ switch ($Verbosity) {
97
+ "low" {
98
+ $sentences = [regex]::Split($ResponseText, '(?<=[.!?])\s+')
99
+ # Fall back to full text if no sentence-ending punctuation (fixes m1)
100
+ if ($sentences.Count -gt 0 -and $sentences[0]) { $ResponseText = $sentences[0] }
101
+ }
102
+ "medium" {
103
+ $sentences = [regex]::Split($ResponseText, '(?<=[.!?])\s+')
104
+ # Fall back to full text if no sentence-ending punctuation (fixes m1)
105
+ $truncated = ($sentences | Select-Object -First 2) -join " "
106
+ if ($truncated) { $ResponseText = $truncated }
107
+ }
108
+ # "high" = full text
109
+ }
110
+
111
+ # --- Speak with queue serialization (named mutex, cross-process) ---
112
+ $mutex = New-Object System.Threading.Mutex($false, "AgentVibesPartyModeTTSQueue")
113
+ try {
114
+ $acquired = $false
115
+ try {
116
+ # WaitOne throws AbandonedMutexException if prior process crashed while holding it.
117
+ # That exception means we DID acquire the mutex — treat it as success (fixes M2).
118
+ $acquired = $mutex.WaitOne(60000)
119
+ } catch [System.Threading.AbandonedMutexException] {
120
+ $acquired = $true # abandoned = we now own it
121
+ }
122
+
123
+ if ($acquired) {
124
+ try {
125
+ # Pass positional args directly after -File (spaces handled by quoting via array)
126
+ & powershell -NoProfile -ExecutionPolicy Bypass -File $BmadSpeak $AgentId $ResponseText
127
+ } finally {
128
+ $mutex.ReleaseMutex()
129
+ }
130
+ } else {
131
+ # Timed out — log to stderr so it's visible in hook error output (fixes M6)
132
+ [Console]::Error.WriteLine("[AgentVibes] Party mode TTS queue timeout for agent: $AgentId")
133
+ }
134
+ } finally {
135
+ $mutex.Close()
136
+ }
137
+
138
+ } catch {
139
+ # Silently exit — never block Claude
140
+ exit 0
141
+ }
@@ -21,6 +21,11 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
21
21
  $ClaudeDir = Split-Path -Parent $ScriptDir
22
22
  $ProjectRoot = Split-Path -Parent $ClaudeDir
23
23
 
24
+ # When running as global script, prefer CLAUDE_PROJECT_DIR for project root
25
+ if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\_bmad")) {
26
+ $ProjectRoot = $env:CLAUDE_PROJECT_DIR
27
+ }
28
+
24
29
  # Strip markdown formatting — prevent SAPI/Piper from speaking asterisks literally
25
30
  $Dialogue = $Dialogue -replace '\*\*', '' -replace '\*', '' -replace '`', ''
26
31
  $Dialogue = $Dialogue -replace '\\!', '!' -replace '\\\$', '$'
@@ -39,22 +44,42 @@ if (-not (Test-Path $ManifestFile)) {
39
44
 
40
45
  # ---------------------------------------------------------------------------
41
46
  # Read bmad-voice-map.json for per-agent profile
42
- $VoiceMapFile = Join-Path $env:USERPROFILE ".agentvibes\bmad-voice-map.json"
47
+ # Prefer project-local voice map, fall back to global
48
+ $VoiceMapLocal = Join-Path $ProjectRoot ".agentvibes\bmad-voice-map.json"
49
+ $VoiceMapGlobal = Join-Path $env:USERPROFILE ".agentvibes\bmad-voice-map.json"
50
+ $VoiceMapFile = if (Test-Path $VoiceMapLocal) { $VoiceMapLocal } else { $VoiceMapGlobal }
43
51
 
44
- $AgentVoice = ""
52
+ $AgentVoice = ""
45
53
  $AgentPersonality = ""
54
+ $AgentBgEnabled = $false
55
+ $AgentBgTrack = ""
56
+ $AgentId = $null
57
+
58
+ # Read global background music volume (stored as 0.0-1.0 float)
59
+ $_BgVolFile = Join-Path $ProjectRoot ".claude\config\background-music-volume.txt"
60
+ if (-not (Test-Path $_BgVolFile)) {
61
+ $_BgVolFile = Join-Path $env:USERPROFILE ".claude\config\background-music-volume.txt"
62
+ }
63
+ if (Test-Path $_BgVolFile) {
64
+ $_BgVolRaw = (Get-Content $_BgVolFile -Raw -ErrorAction SilentlyContinue).Trim()
65
+ $_BgVolParsed = 0.0
66
+ if ([double]::TryParse($_BgVolRaw, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$_BgVolParsed)) {
67
+ $AgentBgVolume = "{0:F2}" -f $_BgVolParsed
68
+ } else {
69
+ $AgentBgVolume = "0.20"
70
+ }
71
+ } else {
72
+ $AgentBgVolume = "0.20"
73
+ }
46
74
 
47
75
  if (Test-Path $VoiceMapFile) {
48
76
  try {
49
77
  $VoiceMap = Get-Content $VoiceMapFile -Raw | ConvertFrom-Json
50
78
 
51
- # Resolve agent ID from display name or direct ID
52
- $AgentId = $null
53
-
54
- # Check direct match against manifest column 1
55
- $ManifestRows = Import-Csv $ManifestFile
79
+ # Resolve agent ID: match canonical ID or display name prefix
80
+ $ManifestRows = Import-Csv $ManifestFile -Encoding UTF8
56
81
  foreach ($row in $ManifestRows) {
57
- $id = ($row.PSObject.Properties | Select-Object -First 1).Value -replace '^"|"$', ''
82
+ $id = ($row.PSObject.Properties | Select-Object -First 1).Value -replace '^"|"$', ''
58
83
  $display = ($row.PSObject.Properties | Select-Object -Skip 1 -First 1).Value -replace '^"|"$', ''
59
84
  if ($id -ieq $AgentNameOrId -or $display -like "$AgentNameOrId*") {
60
85
  $AgentId = $id
@@ -64,8 +89,16 @@ if (Test-Path $VoiceMapFile) {
64
89
 
65
90
  if ($AgentId -and $VoiceMap.agents.$AgentId) {
66
91
  $Profile = $VoiceMap.agents.$AgentId
67
- if ($Profile.voice) { $AgentVoice = $Profile.voice }
92
+ if ($Profile.voice) { $AgentVoice = $Profile.voice }
68
93
  if ($Profile.personality) { $AgentPersonality = $Profile.personality }
94
+ if ($Profile.backgroundMusic) {
95
+ $AgentBgEnabled = [bool]$Profile.backgroundMusic.enabled
96
+ if ($Profile.backgroundMusic.track) { $AgentBgTrack = $Profile.backgroundMusic.track }
97
+ if ($null -ne $Profile.backgroundMusic.volume) {
98
+ # Voice map stores 0-100; audio-effects.cfg uses 0.0-1.0
99
+ $AgentBgVolume = "{0:F2}" -f ([double]$Profile.backgroundMusic.volume / 100.0)
100
+ }
101
+ }
69
102
  }
70
103
  } catch {
71
104
  # Silently degrade — TTS will still play with global settings
@@ -74,7 +107,7 @@ if (Test-Path $VoiceMapFile) {
74
107
 
75
108
  # ---------------------------------------------------------------------------
76
109
  # Locate play-tts.ps1 — prefer project-local, fall back to global
77
- $PlayTtsLocal = Join-Path $ProjectRoot ".claude\hooks-windows\play-tts.ps1"
110
+ $PlayTtsLocal = Join-Path $ProjectRoot ".claude\hooks-windows\play-tts.ps1"
78
111
  $PlayTtsGlobal = Join-Path $env:USERPROFILE ".claude\hooks-windows\play-tts.ps1"
79
112
  $PlayTtsScript = if (Test-Path $PlayTtsLocal) { $PlayTtsLocal } else { $PlayTtsGlobal }
80
113
 
@@ -82,10 +115,20 @@ if (-not (Test-Path $PlayTtsScript)) {
82
115
  exit 0
83
116
  }
84
117
 
118
+ # ---------------------------------------------------------------------------
119
+ # Determine which .claude config dir play-tts.ps1 will read.
120
+ # play-tts.ps1 checks CLAUDE_PROJECT_DIR first — match that logic exactly.
121
+ $TtsClaudeDir = if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude")) {
122
+ "$env:CLAUDE_PROJECT_DIR\.claude"
123
+ } else {
124
+ $ClaudeDir # ~/.claude (this script's own ClaudeDir)
125
+ }
126
+ $TtsConfigDir = Join-Path $TtsClaudeDir "config"
127
+
85
128
  # ---------------------------------------------------------------------------
86
129
  # Apply per-agent personality override if set
87
- $OldPersonality = ""
88
- $PersonalityFile = Join-Path $ClaudeDir "config\personality.txt"
130
+ $OldPersonality = ""
131
+ $PersonalityFile = Join-Path $TtsClaudeDir "config\personality.txt"
89
132
  if ($AgentPersonality -and (Test-Path (Split-Path $PersonalityFile -Parent))) {
90
133
  if (Test-Path $PersonalityFile) {
91
134
  $OldPersonality = (Get-Content $PersonalityFile -Raw).Trim()
@@ -93,6 +136,32 @@ if ($AgentPersonality -and (Test-Path (Split-Path $PersonalityFile -Parent))) {
93
136
  Set-Content $PersonalityFile $AgentPersonality -NoNewline
94
137
  }
95
138
 
139
+ # ---------------------------------------------------------------------------
140
+ # Temporarily patch background music config for this agent.
141
+ # The caller (bmad-party-speak.ps1) holds a named mutex so only one speak
142
+ # call runs at a time — these file patches are safe from concurrent clobber.
143
+ $BgEnabledFile = Join-Path $TtsConfigDir "background-music-enabled.txt"
144
+ $AudioEffectsCfg = Join-Path $TtsConfigDir "audio-effects.cfg"
145
+ $OldBgEnabled = $null
146
+ $TempCfgLine = ""
147
+
148
+ if ($AgentBgEnabled -and $AgentBgTrack) {
149
+ # Save + enable background music
150
+ if (Test-Path $BgEnabledFile) {
151
+ $OldBgEnabled = (Get-Content $BgEnabledFile -Raw -ErrorAction SilentlyContinue).Trim()
152
+ }
153
+ Set-Content $BgEnabledFile "true" -NoNewline
154
+
155
+ # Prepend agent line to audio-effects.cfg so play-tts.ps1 finds it first
156
+ # Format: AGENT_NAME|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME
157
+ $TempCfgLine = "${AgentId}||${AgentBgTrack}|${AgentBgVolume}"
158
+ $env:AGENTVIBES_AGENT_NAME = $AgentId
159
+ $existingCfg = if (Test-Path $AudioEffectsCfg) {
160
+ Get-Content $AudioEffectsCfg -Raw -ErrorAction SilentlyContinue
161
+ } else { "" }
162
+ Set-Content $AudioEffectsCfg "${TempCfgLine}`n${existingCfg}" -NoNewline
163
+ }
164
+
96
165
  try {
97
166
  # Speak with agent's voice (or global voice if none configured)
98
167
  if ($AgentVoice) {
@@ -101,7 +170,7 @@ try {
101
170
  & powershell -NoProfile -ExecutionPolicy Bypass -File $PlayTtsScript $Dialogue
102
171
  }
103
172
  } finally {
104
- # Restore original personality
173
+ # Restore personality
105
174
  if ($AgentPersonality -and $PersonalityFile) {
106
175
  if ($OldPersonality) {
107
176
  Set-Content $PersonalityFile $OldPersonality -NoNewline
@@ -109,4 +178,23 @@ try {
109
178
  Remove-Item $PersonalityFile -Force -ErrorAction SilentlyContinue
110
179
  }
111
180
  }
181
+
182
+ # Restore background music config
183
+ if ($AgentBgEnabled -and $AgentBgTrack) {
184
+ if ($null -ne $OldBgEnabled) {
185
+ Set-Content $BgEnabledFile $OldBgEnabled -NoNewline
186
+ } elseif (Test-Path $BgEnabledFile) {
187
+ Remove-Item $BgEnabledFile -Force -ErrorAction SilentlyContinue
188
+ }
189
+
190
+ # Remove the prepended agent line from audio-effects.cfg
191
+ if (Test-Path $AudioEffectsCfg) {
192
+ $cfgRaw = Get-Content $AudioEffectsCfg -Raw -ErrorAction SilentlyContinue
193
+ $escaped = [regex]::Escape($TempCfgLine)
194
+ $cfgRaw = $cfgRaw -replace "^${escaped}\r?\n?", ""
195
+ Set-Content $AudioEffectsCfg $cfgRaw -NoNewline
196
+ }
197
+
198
+ $env:AGENTVIBES_AGENT_NAME = ""
199
+ }
112
200
  }
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # AgentVibes Release Notes
2
2
 
3
+ ## ✨ v4.6.0 — Minor Release
4
+
5
+ **Release Date:** April 2026
6
+
7
+ ### New Features
8
+
9
+ - **BMAD party mode TTS auto-installs for all platforms** — The installer now automatically copies `bmad-party-speak.sh` (Linux/macOS/WSL) or `bmad-party-speak.ps1` (Windows) to `~/.claude/hooks/` and registers a `PostToolUse` hook in `~/.claude/settings.json`. Party mode agents now speak out of the box in any BMAD project without manual setup. Both scripts are included in critical hooks so `npx agentvibes update` keeps them fresh.
10
+
11
+ ### Bug Fixes
12
+
13
+ - **Background music volume default** — All volume defaults lowered from 70% to 20% across the UI (settings tab, agents tab, music tab, track picker) and scripts (`audio-processor.sh`, `bmad-speak.sh`, `bmad-speak.ps1`). New installs and newly configured agents default to a much more reasonable level.
14
+ - **bmad-speak volume inheritance** — `bmad-speak.sh` and `bmad-speak.ps1` now read the global `background-music-volume.txt` config file as the fallback volume instead of a hardcoded value.
15
+ - **Installer wizard left arrow** — Pressing ← on the completion screen (screen 5) to move from Done-Quit to Done-Customize More no longer jumps back to the installation step.
16
+
17
+ ### Tests
18
+
19
+ - 29 new tests: volume default regression guards across all affected files, `configurePartyModeHook` installer coverage (idempotency, settings.json registration, script copying, hook preservation), and a regression test for the screen 5 navigation fix.
20
+
21
+ ---
22
+
23
+ ## 🐛 v4.5.7 — Patch Release
24
+
25
+ **Release Date:** April 2026
26
+
27
+ ### Bug Fixes
28
+
29
+ - **Background music volume default** — All volume defaults lowered from 70% to 20% across the UI (settings tab, agents tab, music tab, track picker). New installs and newly configured agents will default to a much more reasonable background music level.
30
+ - **bmad-speak volume inheritance** — `bmad-speak.sh` and `bmad-speak.ps1` now read the global `background-music-volume.txt` config file as the fallback volume instead of a hardcoded value. Per-agent background music volume now correctly inherits the global setting when no explicit per-agent override is saved.
31
+
32
+ ---
33
+
34
+ ## 🐛 v4.5.1 — Patch Release
35
+
36
+ **Release Date:** April 2026
37
+
38
+ ### Bug Fix
39
+
40
+ - **Music tab preview** — Pressing Space on a track in the Music tab now plays correctly
41
+ when running `npx agentvibes` from a fresh directory. Previously, if `.claude/audio/tracks/`
42
+ didn't exist in the current working directory, the track list showed built-in tracks but
43
+ Space did nothing (the player was spawned against a non-existent path). Now falls back to
44
+ the package-bundled tracks directory automatically.
45
+
46
+ ---
47
+
3
48
  ## 🌍 v4.5.0 — "Speak Every Language" Release
4
49
 
5
50
  **Release Date:** April 2026
@@ -27,8 +72,22 @@ Every screen, tab, button, and label in the `npx agentvibes` TUI is now fully tr
27
72
 
28
73
  ### 🎙️ Cross-Platform BMAD Speak
29
74
 
30
- BMAD (Build More Architect Dreams) is an AI multi-agent framework where specialized agents — Architect, PM, Developer, QA, and Analyst — collaborate to build software. With this release, every agent in a BMAD party mode session now speaks aloud with their own unique voice, personality, and music on Windows — making each role instantly recognizable.
31
-
75
+ BMAD (Build More Architect Dreams) is an AI multi-agent framework where specialized agents — Architect, PM, Developer, QA, and Analyst — collaborate to build software. With this release, every agent in a BMAD party mode session now speaks aloud with their own unique voice, personality, and music on Windows — making each role instantly recognizable.
76
+
77
+ ## 🐛 v4.5.1 — Patch Release
78
+
79
+ **Release Date:** April 2026
80
+
81
+ ### Bug Fix
82
+
83
+ - **Music tab preview** — Pressing Space on a track in the Music tab now plays correctly
84
+ when running `npx agentvibes` from a fresh directory. Previously, if `.claude/audio/tracks/`
85
+ didn't exist in the current working directory, the track list showed built-in tracks but
86
+ Space did nothing (the player was spawned against a non-existent path). Now falls back to
87
+ the package-bundled tracks directory automatically.
88
+
89
+ ---
90
+
32
91
  - `bin/bmad-speak.js` — cross-platform entry point for BMAD agent speech
33
92
  - `.claude/hooks-windows/bmad-speak.ps1` — native Windows BMAD speak with per-agent personality routing
34
93
 
package/bin/agent-vibes CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * AgentVibes - Beautiful ElevenLabs TTS voice commands for Claude Code
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * AgentVibes Voice Browser
package/bin/mcp-server.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * AgentVibes MCP Server Launcher (Cross-Platform)
package/bin/test-bmad-pr CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bash
1
+ #!/usr/bin/env bash
2
2
  #
3
3
  # AgentVibes BMAD PR Testing Command
4
4
  # Quick command to test BMAD PRs with AgentVibes integration
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": "4.5.0",
4
+ "version": "4.6.0",
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": [
@@ -825,7 +825,11 @@ export class AgentVibesConsole {
825
825
  });
826
826
 
827
827
  // Register global key bindings (S/V/M/A/R/H/I/T/Esc)
828
- setupNavigation(this.screen, this.navigationService);
828
+ setupNavigation(this.screen, this.navigationService, () => {
829
+ const id = this.navigationService.getActiveTab();
830
+ const item = this._tabItems?.[id];
831
+ if (item) item.focus();
832
+ });
829
833
  }
830
834
 
831
835
  // ---------------------------------------------------------------------------
@@ -35,8 +35,9 @@ const KEY_TO_TAB = {
35
35
  *
36
36
  * @param {object} screen - Blessed screen instance (or stub in tests)
37
37
  * @param {import('../services/navigation-service.js').NavigationService} navigationService
38
+ * @param {function} [focusMainTabBar] - Optional callback to return focus to the tab bar
38
39
  */
39
- export function setupNavigation(screen, navigationService) {
40
+ export function setupNavigation(screen, navigationService, focusMainTabBar) {
40
41
  // Tab switching shortcuts — one handler per key (both cases)
41
42
  for (const [key, tabId] of Object.entries(KEY_TO_TAB)) {
42
43
  screen.key([key], () => {
@@ -53,10 +54,12 @@ export function setupNavigation(screen, navigationService) {
53
54
  }
54
55
  });
55
56
 
56
- // Escape — close modal (story 6.4 will expand modal handling)
57
+ // Escape — close modal if open, otherwise return focus to tab bar
57
58
  screen.key(['escape'], () => {
58
59
  if (navigationService.isModalOpen()) {
59
60
  navigationService.closeModal();
61
+ } else if (typeof focusMainTabBar === 'function') {
62
+ focusMainTabBar();
60
63
  }
61
64
  });
62
65
  }
@@ -319,7 +319,7 @@ ${_tl('bmadDesc')}
319
319
  }
320
320
 
321
321
  const resetBtn = _createBtn('[X] Reset', () => {
322
- const agent = _agents[agentList.selected];
322
+ const agent = _agents[agentList.selected ?? 0];
323
323
  if (agent) {
324
324
  voiceStore.resetAgentProfile(agent.id);
325
325
  refreshDisplay();
@@ -374,7 +374,7 @@ ${_tl('bmadDesc')}
374
374
  ? formatTrackName(profile.backgroundMusic.track)
375
375
  : '(global)')).padEnd(COL_MUSIC).slice(0, COL_MUSIC);
376
376
  const vol = profile.backgroundMusic?.enabled
377
- ? ` ${profile.backgroundMusic.volume ?? 70}%`.padEnd(COL_VOL)
377
+ ? ` ${profile.backgroundMusic.volume ?? 20}%`.padEnd(COL_VOL)
378
378
  : ' — ';
379
379
  const pretext = ' ' + (profile.pretext || '(default)').slice(0, COL_PRETEXT - 1);
380
380
  return ` ${icon}${name}${voice}${gender}${provider}${reverb}${music}${vol} ${pretext}`;
@@ -513,7 +513,7 @@ ${_tl('bmadDesc')}
513
513
  personality: profile.personality || globalCfg.personality || 'none',
514
514
  backgroundMusic: {
515
515
  track: profile.backgroundMusic?.track || globalCfg.backgroundMusic?.track || '',
516
- volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 70,
516
+ volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 20,
517
517
  enabled: profile.backgroundMusic?.enabled ?? globalCfg.backgroundMusic?.enabled ?? false,
518
518
  },
519
519
  });
@@ -534,7 +534,7 @@ ${_tl('bmadDesc')}
534
534
  personality: profile.personality || globalCfg.personality || 'none',
535
535
  backgroundMusic: {
536
536
  track: profile.backgroundMusic?.track || globalCfg.backgroundMusic?.track || '',
537
- volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 70,
537
+ volume: profile.backgroundMusic?.volume ?? globalCfg.backgroundMusic?.volume ?? 20,
538
538
  enabled: profile.backgroundMusic?.enabled ?? globalCfg.backgroundMusic?.enabled ?? false,
539
539
  },
540
540
  };
@@ -657,7 +657,7 @@ ${_tl('bmadDesc')}
657
657
  if (draft.reverbPreset !== (globalCfg.effects?.reverbPreset || 'light')) toSave.reverbPreset = draft.reverbPreset;
658
658
  if (draft.personality !== (globalCfg.personality || 'none')) toSave.personality = draft.personality;
659
659
  if (draft.backgroundMusic.track !== (globalCfg.backgroundMusic?.track || '') ||
660
- draft.backgroundMusic.volume !== (globalCfg.backgroundMusic?.volume ?? 70) ||
660
+ draft.backgroundMusic.volume !== (globalCfg.backgroundMusic?.volume ?? 20) ||
661
661
  draft.backgroundMusic.enabled !== (globalCfg.backgroundMusic?.enabled ?? false)) {
662
662
  toSave.backgroundMusic = draft.backgroundMusic;
663
663
  }
@@ -1275,7 +1275,7 @@ ${_tl('bmadDesc')}
1275
1275
  const track = shuffled[i % shuffled.length];
1276
1276
  const existing = voiceStore.getAgentProfile(agent.id);
1277
1277
  voiceStore.setAgentProfile(agent.id, {
1278
- backgroundMusic: { track, volume: existing.backgroundMusic?.volume ?? 70, enabled: true },
1278
+ backgroundMusic: { track, volume: existing.backgroundMusic?.volume ?? 20, enabled: true },
1279
1279
  });
1280
1280
  });
1281
1281
  return true;
@@ -1373,7 +1373,7 @@ ${_tl('bmadDesc')}
1373
1373
 
1374
1374
  case 'setMusic':
1375
1375
  _closeMenu(() => {
1376
- openTrackPicker(screen, '', 70, (track, volume) => {
1376
+ openTrackPicker(screen, '', 20, (track, volume) => {
1377
1377
  _agents.forEach(agent => {
1378
1378
  const p = voiceStore.getAgentProfile(agent.id);
1379
1379
  voiceStore.setAgentProfile(agent.id, {
@@ -1389,7 +1389,7 @@ ${_tl('bmadDesc')}
1389
1389
 
1390
1390
  case 'setVolume':
1391
1391
  _closeMenu(() => {
1392
- const curVol = voiceStore.getAgentProfile(_agents[0]?.id)?.backgroundMusic?.volume ?? 70;
1392
+ const curVol = voiceStore.getAgentProfile(_agents[0]?.id)?.backgroundMusic?.volume ?? 20;
1393
1393
  openVolumeInput(screen, curVol, (volume) => {
1394
1394
  _agents.forEach(agent => {
1395
1395
  const p = voiceStore.getAgentProfile(agent.id);
@@ -1504,7 +1504,7 @@ ${_tl('bmadDesc')}
1504
1504
  // Key bindings
1505
1505
 
1506
1506
  agentList.key(['x', 'X'], () => {
1507
- const agent = _agents[agentList.selected];
1507
+ const agent = _agents[agentList.selected ?? 0];
1508
1508
  if (agent) {
1509
1509
  voiceStore.resetAgentProfile(agent.id);
1510
1510
  refreshDisplay();
@@ -1513,12 +1513,12 @@ ${_tl('bmadDesc')}
1513
1513
 
1514
1514
 
1515
1515
  agentList.key(['enter'], () => {
1516
- const agent = _agents[agentList.selected];
1516
+ const agent = _agents[agentList.selected ?? 0];
1517
1517
  if (agent) _openAgentDetailPanel(agent);
1518
1518
  });
1519
1519
 
1520
1520
  agentList.key(['space'], () => {
1521
- const agent = _agents[agentList.selected];
1521
+ const agent = _agents[agentList.selected ?? 0];
1522
1522
  if (agent) _sampleAgent(agent);
1523
1523
  });
1524
1524
 
@@ -443,9 +443,10 @@ export function createInstallTab(screen, services) {
443
443
 
444
444
  _renderScreen5();
445
445
 
446
- // Show OK button now that install is done (success or error)
447
- _s5OkBtn.show();
448
- _s5OkBtn.focus();
446
+ // Show buttons now that install is done (success or error)
447
+ _s5QuitBtn.show();
448
+ _s5CustomizeBtn.show();
449
+ _s5QuitBtn.focus();
449
450
  screen.render();
450
451
 
451
452
  // Play TTS greeting on success
@@ -579,12 +580,25 @@ export function createInstallTab(screen, services) {
579
580
  // Screen 3: no Continue button — Enter/→ on the list confirms selection and advances
580
581
 
581
582
  // -------------------------------------------------------------------------
582
- // Screen 5 buttonOK (summary page only, config already saved on screen 4)
583
+ // Screen 5 buttonsCustomize More (left) + Done - Quit (right, default focused)
584
+ // Layout: [ Customize More ] [ Done - Quit ]
585
+ // Button width = label + 2 padding chars; gap between buttons = 3 chars
583
586
 
584
- const _s5OkBtn = _createInstallBtn(_tl('okDoneBtn'), '#1565c0', () => {
587
+ const _s5CustomizeBtn = _createInstallBtn(_tl('doneCustomizeBtn'), '#1565c0', () => {
585
588
  _dismissCompletionModal();
586
589
  });
587
- _s5OkBtn.bottom = 3; _s5OkBtn.left = 4; // bottom-anchored: sits above hintLine (bottom:2)
590
+ _s5CustomizeBtn.bottom = 3; _s5CustomizeBtn.left = 4;
591
+
592
+ const _s5QuitBtn = _createInstallBtn(_tl('doneQuitBtn'), '#b71c1c', () => {
593
+ screen.destroy();
594
+ process.exit(0);
595
+ });
596
+ // left = start(4) + customizeLabel + 2 padding + 3 gap
597
+ _s5QuitBtn.bottom = 3; _s5QuitBtn.left = 4 + _tl('doneCustomizeBtn').length + 2 + 3;
598
+
599
+ // Arrow/Tab navigation between the two buttons
600
+ _s5CustomizeBtn.key(['tab', 'right'], () => { _s5QuitBtn.focus(); screen.render(); });
601
+ _s5QuitBtn.key(['tab', 'left', 'S-tab'], () => { _s5CustomizeBtn.focus(); screen.render(); });
588
602
 
589
603
  // -------------------------------------------------------------------------
590
604
  // Screen renderers
@@ -827,11 +841,13 @@ export function createInstallTab(screen, services) {
827
841
  // Screen 2 continue button: hidden on other screens; _renderScreen2 manages show/focus
828
842
  if (_screen !== 2) _s2ContinueBtn.hide();
829
843
 
830
- // Screen 5 OK button: hidden during active install, shown by _runInstall() on completion
844
+ // Screen 5 buttons: hidden during active install, shown by _runInstall() on completion
831
845
  if (_screen === 5 && (_installComplete || _installError)) {
832
- _s5OkBtn.show();
846
+ _s5QuitBtn.show();
847
+ _s5CustomizeBtn.show();
833
848
  } else {
834
- _s5OkBtn.hide();
849
+ _s5QuitBtn.hide();
850
+ _s5CustomizeBtn.hide();
835
851
  }
836
852
 
837
853
  // Show Screen 4 action buttons only on screen 4
@@ -958,6 +974,7 @@ export function createInstallTab(screen, services) {
958
974
  screen.key(['left'], () => {
959
975
  if (box.hidden || _checking) return;
960
976
  if (_screen === 4) return;
977
+ if (_screen === 5) return; // Screen 5: ← handled by button nav
961
978
  if (_screen > 0) {
962
979
  _screen--;
963
980
  _showCurrentScreen();
@@ -1047,7 +1064,7 @@ export function createInstallTab(screen, services) {
1047
1064
  } else if (_screen === 4) {
1048
1065
  _editBtn.focus();
1049
1066
  } else if (_screen === 5 && (_installComplete || _installError)) {
1050
- _s5OkBtn.focus();
1067
+ _s5QuitBtn.focus();
1051
1068
  } else {
1052
1069
  box.focus();
1053
1070
  }
@@ -12,10 +12,17 @@
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import os from 'node:os';
15
+ import { fileURLToPath } from 'node:url';
15
16
  import { spawn } from 'node:child_process';
16
17
  import { buildAudioEnv, detectMp3Player } from '../audio-env.js';
17
18
  import { t } from '../../i18n/strings.js';
18
19
 
20
+ // Package-relative tracks dir — used as fallback when cwd has no .claude/audio/tracks/
21
+ const _PKG_TRACKS_DIR = path.resolve(
22
+ path.dirname(fileURLToPath(import.meta.url)),
23
+ '..', '..', '..', '.claude', 'audio', 'tracks'
24
+ );
25
+
19
26
  const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
20
27
 
21
28
  let blessed;
@@ -157,7 +164,9 @@ function createTestStub() {
157
164
  * @returns {string}
158
165
  */
159
166
  function _getTracksDir() {
160
- return path.join(process.cwd(), '.claude', 'audio', 'tracks');
167
+ const cwdTracks = path.join(process.cwd(), '.claude', 'audio', 'tracks');
168
+ // Fall back to package-bundled tracks if cwd doesn't have any
169
+ return fs.existsSync(cwdTracks) ? cwdTracks : _PKG_TRACKS_DIR;
161
170
  }
162
171
 
163
172
  /**
@@ -191,7 +200,7 @@ function _getMusic(configService) {
191
200
  return {
192
201
  enabled: music.enabled ?? false,
193
202
  track: music.track ?? BUILT_IN_TRACK_CATALOG[0].id,
194
- volume: music.volume ?? 70,
203
+ volume: music.volume ?? 20,
195
204
  };
196
205
  }
197
206
 
@@ -108,7 +108,7 @@ const FOOTER_TEXT =
108
108
  const EFFECTS_DEFAULTS = Object.freeze({ reverbPreset: 'light' });
109
109
 
110
110
  // Default background music config
111
- const MUSIC_DEFAULTS = Object.freeze({ enabled: false, track: 'agentvibes_soft_flamenco_loop.mp3', volume: 70 });
111
+ const MUSIC_DEFAULTS = Object.freeze({ enabled: false, track: 'agentvibes_soft_flamenco_loop.mp3', volume: 20 });
112
112
 
113
113
  // Verbosity display labels
114
114
  const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', minimal: 'Minimal', custom: 'Custom' });
@@ -279,7 +279,7 @@ export function openTrackPicker(screen, currentTrack, currentVolume, onSelect, o
279
279
  _killPreview();
280
280
  if (list._label2) list._label2.destroy();
281
281
  destroyList(list, screen, null);
282
- openVolumeInput(screen, currentVolume ?? 70, (volume) => {
282
+ openVolumeInput(screen, currentVolume ?? 20, (volume) => {
283
283
  onSelect(selected.file, volume);
284
284
  }, onClose);
285
285
  });
package/src/i18n/de.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "Weiter →",
114
114
  acceptInstallBtn: "✓ Akzeptieren & Installieren",
115
115
  okDoneBtn: "✓ OK — Fertig",
116
+ doneCustomizeBtn: "✓ Fertig - Mehr anpassen",
117
+ doneQuitBtn: "✕ Fertig - Beenden",
116
118
  editInstallBtn: "Bearbeiten",
117
119
  musicDisabledMsg: "Musik deaktiviert. Jetzt aktivieren?",
118
120
  settingsFooter: "[↑↓] Gruppe [←→] Geschwister/Sub-Tab [Enter/Space] Aktivieren [Tab] Tab Wechseln [Q] Beenden",
package/src/i18n/en.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "Continue →",
114
114
  acceptInstallBtn: "✓ Accept & Install",
115
115
  okDoneBtn: "✓ OK — Done",
116
+ doneCustomizeBtn: "✓ Done - Customize More",
117
+ doneQuitBtn: "✕ Done - Quit",
116
118
  editInstallBtn: "Edit",
117
119
  musicDisabledMsg: "Music is disabled. Enable it now?",
118
120
  settingsFooter: "[↑↓] Group [←→] Sibling/Sub-tab [Enter/Space] Activate [Tab] Switch Tab [Q] Quit",
package/src/i18n/es.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "Continuar →",
114
114
  acceptInstallBtn: "✓ Aceptar e Instalar",
115
115
  okDoneBtn: "✓ OK — Listo",
116
+ doneCustomizeBtn: "✓ Listo - Personalizar más",
117
+ doneQuitBtn: "✕ Listo - Salir",
116
118
  editInstallBtn: "Editar",
117
119
  musicDisabledMsg: "Música desactivada. ¿Activar ahora?",
118
120
  settingsFooter: "[↑↓] Grupo [←→] Hermano/Sub-tab [Enter/Space] Activar [Tab] Cambiar Tab [Q] Salir",
package/src/i18n/fr.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "Continuer →",
114
114
  acceptInstallBtn: "✓ Accepter et Installer",
115
115
  okDoneBtn: "✓ OK — Terminé",
116
+ doneCustomizeBtn: "✓ Terminé - Personnaliser plus",
117
+ doneQuitBtn: "✕ Terminé - Quitter",
116
118
  editInstallBtn: "Modifier",
117
119
  musicDisabledMsg: "Musique désactivée. L'activer maintenant ?",
118
120
  settingsFooter: "[↑↓] Groupe [←→] Sibling/Sub-tab [Enter/Space] Activer [Tab] Changer Tab [Q] Quitter",
package/src/i18n/hi.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "जारी रखें →",
114
114
  acceptInstallBtn: "✓ स्वीकार करें और स्थापित करें",
115
115
  okDoneBtn: "✓ OK — हो गया",
116
+ doneCustomizeBtn: "✓ हो गया - और अनुकूलित करें",
117
+ doneQuitBtn: "✕ हो गया - बाहर निकलें",
116
118
  editInstallBtn: "संपादित करें",
117
119
  musicDisabledMsg: "संगीत अक्षम है। अभी सक्षम करें?",
118
120
  settingsFooter: "[↑↓] समूह [←→] सहोदर/उप-टैब [Enter/Space] सक्रिय करें [Tab] टैब बदलें [Q] बाहर",
package/src/i18n/ja.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "続ける →",
114
114
  acceptInstallBtn: "✓ 承認してインストール",
115
115
  okDoneBtn: "✓ OK — 終了",
116
+ doneCustomizeBtn: "✓ 終了 - さらにカスタマイズ",
117
+ doneQuitBtn: "✕ 終了 - 終了する",
116
118
  editInstallBtn: "編集",
117
119
  musicDisabledMsg: "音楽が無効です。今すぐ有効にしますか?",
118
120
  settingsFooter: "[↑↓] グループ [←→] 兄弟/サブタブ [Enter/Space] 実行 [Tab] タブ切替 [Q] 終了",
package/src/i18n/ko.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "계속 →",
114
114
  acceptInstallBtn: "✓ 수락 및 설치",
115
115
  okDoneBtn: "✓ OK — 완료",
116
+ doneCustomizeBtn: "✓ 완료 - 더 맞춤 설정",
117
+ doneQuitBtn: "✕ 완료 - 종료",
116
118
  editInstallBtn: "편집",
117
119
  musicDisabledMsg: "음악이 비활성화되어 있습니다. 지금 활성화하겠습니까?",
118
120
  settingsFooter: "[↑↓] 그룹 [←→] 형제/서브탭 [Enter/Space] 활성화 [Tab] 탭 전환 [Q] 나가기",
package/src/i18n/pt.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "Continuar →",
114
114
  acceptInstallBtn: "✓ Aceitar e Instalar",
115
115
  okDoneBtn: "✓ OK — Concluído",
116
+ doneCustomizeBtn: "✓ Concluído - Personalizar mais",
117
+ doneQuitBtn: "✕ Concluído - Sair",
116
118
  editInstallBtn: "Editar",
117
119
  musicDisabledMsg: "Música desativada. Ativar agora?",
118
120
  settingsFooter: "[↑↓] Grupo [←→] Irmão/Sub-tab [Enter/Space] Ativar [Tab] Trocar Tab [Q] Sair",
package/src/i18n/zh-CN.js CHANGED
@@ -113,6 +113,8 @@ export default {
113
113
  continueArrowBtn: "继续 →",
114
114
  acceptInstallBtn: "✓ 接受并安装",
115
115
  okDoneBtn: "✓ OK — 完成",
116
+ doneCustomizeBtn: "✓ 完成 - 更多自定义",
117
+ doneQuitBtn: "✕ 完成 - 退出",
116
118
  editInstallBtn: "编辑",
117
119
  musicDisabledMsg: "音乐已禁用。现在启用?",
118
120
  settingsFooter: "[↑↓] 组 [←→] 兄弟/子标签 [Enter/Space] 激活 [Tab] 切换标签 [Q] 退出",
package/src/installer.js CHANGED
@@ -3822,6 +3822,76 @@ async function configureSessionStartHook(targetDir, spinner) {
3822
3822
  }
3823
3823
  }
3824
3824
 
3825
+ /**
3826
+ * Configure BMAD party mode PostToolUse hook in the global ~/.claude/settings.json.
3827
+ * Copies bmad-party-speak script to ~/.claude/hooks/ (or hooks-windows/ on Windows)
3828
+ * and registers the PostToolUse hook so party mode TTS works in any BMAD project.
3829
+ * @param {string} targetDir - Target installation directory (used to locate source scripts)
3830
+ * @param {Object} spinner - Ora spinner instance
3831
+ */
3832
+ async function configurePartyModeHook(targetDir, spinner, homeDirOverride) {
3833
+ spinner.start('Configuring BMAD party mode TTS hook...');
3834
+ const homeDir = homeDirOverride || os.homedir();
3835
+ const globalClaudeDir = path.join(homeDir, '.claude');
3836
+ const globalSettingsPath = path.join(globalClaudeDir, 'settings.json');
3837
+
3838
+ try {
3839
+ // Determine platform-specific paths
3840
+ const hooksSubdir = isNativeWindows() ? 'hooks-windows' : 'hooks';
3841
+ const scriptName = isNativeWindows() ? 'bmad-party-speak.ps1' : 'bmad-party-speak.sh';
3842
+ const globalHooksDir = path.join(globalClaudeDir, hooksSubdir);
3843
+ const srcScript = path.join(__dirname, '..', '.claude', hooksSubdir, scriptName);
3844
+ const destScript = path.join(globalHooksDir, scriptName);
3845
+
3846
+ // Copy script to global hooks dir (create dir if needed)
3847
+ await fs.mkdir(globalHooksDir, { recursive: true });
3848
+ await fs.copyFile(srcScript, destScript);
3849
+ if (!isNativeWindows()) {
3850
+ await fs.chmod(destScript, 0o750);
3851
+ }
3852
+
3853
+ // Build the PostToolUse hook command
3854
+ const hookCommand = isNativeWindows()
3855
+ ? `powershell -NoProfile -ExecutionPolicy Bypass -File "$HOME\\.claude\\hooks-windows\\bmad-party-speak.ps1"`
3856
+ : `bash "$HOME/.claude/hooks/bmad-party-speak.sh"`;
3857
+
3858
+ // Read/create global settings.json
3859
+ let settings = {};
3860
+ try {
3861
+ const content = await fs.readFile(globalSettingsPath, 'utf8');
3862
+ settings = JSON.parse(content);
3863
+ } catch {
3864
+ // File missing or invalid — start fresh
3865
+ }
3866
+
3867
+ if (!settings.hooks) settings.hooks = {};
3868
+
3869
+ // Check if PostToolUse hook already registered
3870
+ const existing = settings.hooks.PostToolUse;
3871
+ const alreadyRegistered = Array.isArray(existing) &&
3872
+ existing.some(entry =>
3873
+ Array.isArray(entry.hooks) &&
3874
+ entry.hooks.some(h => h.command && h.command.includes('bmad-party-speak'))
3875
+ );
3876
+
3877
+ if (!alreadyRegistered) {
3878
+ if (!Array.isArray(settings.hooks.PostToolUse)) {
3879
+ settings.hooks.PostToolUse = [];
3880
+ }
3881
+ settings.hooks.PostToolUse.push({
3882
+ hooks: [{ type: 'command', command: hookCommand }]
3883
+ });
3884
+ await fs.writeFile(globalSettingsPath, JSON.stringify(settings, null, 2));
3885
+ spinner.succeed(chalk.green('BMAD party mode TTS hook configured!\n'));
3886
+ } else {
3887
+ // Script still updated above — just note settings unchanged
3888
+ spinner.succeed(chalk.green('BMAD party mode TTS hook up to date\n'));
3889
+ }
3890
+ } catch (error) {
3891
+ spinner.warn(chalk.yellow(`BMAD party mode hook setup skipped: ${error.message}\n`));
3892
+ }
3893
+ }
3894
+
3825
3895
  /**
3826
3896
  * Ensure target directory is a git repo (required for Claude Code hook context injection)
3827
3897
  * @param {string} targetDir - Target installation directory
@@ -4781,7 +4851,8 @@ async function updateCommandFiles(targetDir, spinner) {
4781
4851
  * These hooks contain bug fixes (e.g. markdown stripping) that must propagate
4782
4852
  * on every `npx agentvibes update` regardless of target directory.
4783
4853
  */
4784
- const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'session-start-tts.sh'];
4854
+ const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'session-start-tts.sh', 'bmad-party-speak.sh'];
4855
+ const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1'];
4785
4856
 
4786
4857
  /**
4787
4858
  * Update critical hooks in the global ~/.claude/hooks/ directory if it exists.
@@ -4811,6 +4882,27 @@ async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
4811
4882
  // file not in global dir or src missing — skip silently
4812
4883
  }
4813
4884
  }
4885
+
4886
+ // Also update Windows global hooks-windows dir if present
4887
+ const globalHooksWindowsDir = path.join(homeDirOverride || os.homedir(), '.claude', 'hooks-windows');
4888
+ const srcHooksWindowsDir = path.join(path.dirname(srcHooksDir), 'hooks-windows');
4889
+ try {
4890
+ await fs.access(globalHooksWindowsDir);
4891
+ for (const hook of CRITICAL_HOOKS_WINDOWS) {
4892
+ const destPath = path.join(globalHooksWindowsDir, hook);
4893
+ const srcPath = path.join(srcHooksWindowsDir, hook);
4894
+ try {
4895
+ await fs.access(destPath); // only update if already installed
4896
+ await fs.copyFile(srcPath, destPath);
4897
+ updated++;
4898
+ } catch {
4899
+ // file not in global dir or src missing — skip silently
4900
+ }
4901
+ }
4902
+ } catch {
4903
+ // hooks-windows dir not present — nothing to do
4904
+ }
4905
+
4814
4906
  return updated;
4815
4907
  }
4816
4908
 
@@ -4873,6 +4965,7 @@ async function performUpdateOperations(targetDir, spinner) {
4873
4965
  // Update settings.json
4874
4966
  spinner.text = 'Updating AgentVibes hook configuration...';
4875
4967
  await configureSessionStartHook(targetDir, silentSpinner);
4968
+ await configurePartyModeHook(targetDir, silentSpinner);
4876
4969
  await ensureGitRepo(targetDir, silentSpinner);
4877
4970
 
4878
4971
  // Detect and migrate old configuration
@@ -5050,6 +5143,7 @@ async function install(options = {}) {
5050
5143
  await copyBackgroundMusicFiles(targetDir, silentSpinner);
5051
5144
  await copyConfigFiles(targetDir, silentSpinner);
5052
5145
  await configureSessionStartHook(targetDir, silentSpinner);
5146
+ await configurePartyModeHook(targetDir, silentSpinner);
5053
5147
  await installPluginManifest(targetDir, silentSpinner);
5054
5148
  await ensureGitRepo(targetDir, silentSpinner);
5055
5149
 
@@ -5943,7 +6037,7 @@ export {
5943
6037
  isTermux, isNativeWindows, detectAndNotifyTermux,
5944
6038
  copyCommandFiles, copyHookFiles, copyPersonalityFiles,
5945
6039
  copyPluginFiles, copyBmadConfigFiles, copyBackgroundMusicFiles,
5946
- copyConfigFiles, configureSessionStartHook, ensureGitRepo,
6040
+ copyConfigFiles, configureSessionStartHook, configurePartyModeHook, ensureGitRepo,
5947
6041
  installPluginManifest, checkAndInstallPiper,
5948
- updateGlobalHooks, CRITICAL_HOOKS,
6042
+ updateGlobalHooks, CRITICAL_HOOKS, CRITICAL_HOOKS_WINDOWS,
5949
6043
  };