agentvibes 5.2.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.claude/config/audio-effects.cfg +1 -1
  2. package/.claude/hooks/audio-cache-utils.sh +246 -246
  3. package/.claude/hooks/background-music-manager.sh +404 -404
  4. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  5. package/.claude/hooks/bmad-speak.sh +290 -290
  6. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  7. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  8. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  9. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  10. package/.claude/hooks/clean-audio-cache.sh +22 -22
  11. package/.claude/hooks/cleanup-cache.sh +106 -106
  12. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  13. package/.claude/hooks/download-extra-voices.sh +244 -244
  14. package/.claude/hooks/effects-manager.sh +268 -268
  15. package/.claude/hooks/github-star-reminder.sh +154 -154
  16. package/.claude/hooks/language-manager.sh +362 -362
  17. package/.claude/hooks/learn-manager.sh +492 -492
  18. package/.claude/hooks/macos-voice-manager.sh +205 -205
  19. package/.claude/hooks/migrate-background-music.sh +125 -125
  20. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  21. package/.claude/hooks/optimize-background-music.sh +87 -87
  22. package/.claude/hooks/path-resolver.sh +60 -60
  23. package/.claude/hooks/personality-manager.sh +448 -448
  24. package/.claude/hooks/piper-installer.sh +292 -292
  25. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  26. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  27. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  28. package/.claude/hooks/play-tts.sh +14 -5
  29. package/.claude/hooks/prepare-release.sh +54 -54
  30. package/.claude/hooks/provider-commands.sh +617 -617
  31. package/.claude/hooks/provider-manager.sh +399 -399
  32. package/.claude/hooks/replay-target-audio.sh +95 -95
  33. package/.claude/hooks/sentiment-manager.sh +201 -201
  34. package/.claude/hooks/speed-manager.sh +291 -291
  35. package/.claude/hooks/stop-tts.sh +84 -84
  36. package/.claude/hooks/termux-installer.sh +261 -261
  37. package/.claude/hooks/translate-manager.sh +341 -341
  38. package/.claude/hooks/tts-queue-worker.sh +145 -145
  39. package/.claude/hooks/tts-queue.sh +165 -165
  40. package/.claude/hooks/voice-manager.sh +552 -548
  41. package/.claude/hooks-windows/play-tts.ps1 +2 -2
  42. package/README.md +11 -2
  43. package/RELEASE_NOTES.md +38 -0
  44. package/bin/mcp-server.sh +206 -206
  45. package/mcp-server/server.py +35 -6
  46. package/package.json +1 -1
  47. package/src/console/tabs/setup-tab.js +59 -23
  48. package/src/installer.js +79 -213
  49. package/src/services/llm-provider-service.js +126 -75
@@ -1,290 +1,290 @@
1
- #!/usr/bin/env bash
2
- #
3
- # File: .claude/hooks/bmad-speak.sh
4
- #
5
- # AgentVibes BMAD Voice Integration
6
- # Maps agent display names OR agent IDs to voices and triggers TTS
7
- #
8
- # Usage: bmad-speak.sh "Agent Name" "dialogue text"
9
- # bmad-speak.sh "agent-id" "dialogue text"
10
- #
11
- # Supports both:
12
- # - Display names (e.g., "Winston", "John") for party mode
13
- # - Agent IDs (e.g., "architect", "pm") for individual agents
14
- #
15
-
16
- set -euo pipefail
17
-
18
- # Get script directory and project root
19
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
21
-
22
- # Arguments
23
- AGENT_NAME_OR_ID="$1"
24
- DIALOGUE="$2"
25
-
26
- # Remove backslash escaping that Claude might add for special chars like ! and $
27
- # In single quotes these don't need escaping, but Claude sometimes adds \! anyway
28
- DIALOGUE="${DIALOGUE//\\!/!}"
29
- DIALOGUE="${DIALOGUE//\\\$/\$}"
30
-
31
- # Strip markdown formatting — prevent Piper from speaking "asterisk asterisk" literally.
32
- # play-tts-piper.sh also strips via perl, but do it here early as defense-in-depth.
33
- DIALOGUE=$(printf '%s' "$DIALOGUE" | sed \
34
- -e 's/\*\{1,3\}//g' \
35
- -e 's/`\{1,3\}[^`]*`\{1,3\}//g' \
36
- -e 's/^[[:space:]]*#\{1,6\}[[:space:]]*//g' \
37
- -e 's/__//g' -e 's/_//g' \
38
- -e 's/\[([^]]*)\]([^)]*)//g' \
39
- -e 's/^[[:space:]]*[-*+] //g' \
40
- -e 's/^[[:space:]]*[0-9]\+\. //g')
41
-
42
- # Check if party mode is enabled
43
- if [[ -f "$PROJECT_ROOT/.agentvibes/bmad/bmad-party-mode-disabled.flag" ]]; then
44
- exit 0
45
- fi
46
-
47
- # Check if BMAD is installed
48
- if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
49
- exit 0
50
- fi
51
-
52
- # ---------------------------------------------------------------------------
53
- # Per-agent profile reader — reads from ~/.agentvibes/bmad-voice-map.json
54
- # Uses node for reliable JSON parsing (jq may not be installed)
55
- # Returns empty string if field not found or file missing
56
-
57
- VOICE_MAP_FILE="$HOME/.agentvibes/bmad-voice-map.json"
58
-
59
- # Read a field from the per-agent profile in bmad-voice-map.json
60
- # Usage: read_agent_profile <agent_id> <field>
61
- # Fields: voice, pretext, reverbPreset, personality, backgroundMusic.track, backgroundMusic.volume
62
- read_agent_profile() {
63
- local agent_id="$1"
64
- local field="$2"
65
-
66
- if [[ ! -f "$VOICE_MAP_FILE" ]]; then
67
- echo ""
68
- return
69
- fi
70
-
71
- # Validate agent_id format (prevent injection)
72
- if [[ ! "$agent_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
73
- echo ""
74
- return
75
- fi
76
-
77
- # Use node for JSON parsing (always available in AgentVibes projects)
78
- # SECURITY: Pass values via env vars to prevent shell injection
79
- _VOICE_MAP="$VOICE_MAP_FILE" _AGENT_ID="$agent_id" _FIELD="$field" node -e "
80
- try {
81
- const d = JSON.parse(require('fs').readFileSync(process.env._VOICE_MAP,'utf8'));
82
- const a = d.agents?.[process.env._AGENT_ID] ?? {};
83
- const f = process.env._FIELD;
84
- if (f.includes('.')) {
85
- const [k1, k2] = f.split('.');
86
- process.stdout.write(String(a[k1]?.[k2] ?? ''));
87
- } else {
88
- process.stdout.write(String(a[f] ?? ''));
89
- }
90
- } catch { process.stdout.write(''); }
91
- " 2>/dev/null || echo ""
92
- }
93
-
94
- # Read all profile fields in a single Node.js invocation to avoid ~900ms of overhead.
95
- # Returns: voice|pretext|reverbPreset|personality|backgroundMusic.track|backgroundMusic.volume
96
- # Outputs `|||||` if the file is missing or the agent is not found.
97
- # SECURITY: Pass values via env vars to prevent shell injection
98
- read_agent_profile_all() {
99
- local agent_id="$1"
100
-
101
- # Validate agent_id format (prevent injection)
102
- if [[ ! "$agent_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
103
- echo "|||||"
104
- return
105
- fi
106
-
107
- if [[ ! -f "$VOICE_MAP_FILE" ]]; then
108
- echo "||||||"
109
- return
110
- fi
111
-
112
- _VOICE_MAP="$VOICE_MAP_FILE" _AGENT_ID="$agent_id" node -e "
113
- try {
114
- const d = JSON.parse(require('fs').readFileSync(process.env._VOICE_MAP,'utf8'));
115
- const a = d.agents?.[process.env._AGENT_ID] ?? {};
116
- const fields = [
117
- String(a.voice ?? ''),
118
- String(a.pretext ?? ''),
119
- String(a.reverbPreset ?? ''),
120
- String(a.personality ?? ''),
121
- String(a.backgroundMusic?.track ?? ''),
122
- String(a.backgroundMusic?.volume ?? ''),
123
- String(a.backgroundMusic?.enabled ?? ''),
124
- ];
125
- process.stdout.write(fields.join('|'));
126
- } catch { process.stdout.write('||||||'); }
127
- " 2>/dev/null || echo "||||||"
128
- }
129
-
130
- # ---------------------------------------------------------------------------
131
- # Map display name to agent ID
132
-
133
- map_to_agent_id() {
134
- local name_or_id="$1"
135
-
136
- # If it looks like a file path (.bmad/*/agents/*.md), extract the agent ID
137
- if [[ "$name_or_id" =~ _?\.?bmad/.*/agents/([^/]+)\.md$ ]]; then
138
- echo "${BASH_REMATCH[1]}"
139
- return
140
- fi
141
-
142
- # First check if it's already an agent ID (column 1 of manifest)
143
- local direct_match=$(grep -i "^\"*${name_or_id}\"*," "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" | head -1)
144
- if [[ -n "$direct_match" ]]; then
145
- echo "$name_or_id"
146
- return
147
- fi
148
-
149
- # Otherwise map display name to agent ID (for party mode)
150
- local agent_id=$(awk -F',' -v name="$name_or_id" '
151
- BEGIN { IGNORECASE=1 }
152
- NR > 1 {
153
- display = $2
154
- gsub(/^"|"$/, "", display)
155
- if (tolower(display) ~ "^" tolower(name) "($| |\\()") {
156
- agent = $1
157
- gsub(/^"|"$/, "", agent)
158
- print agent
159
- exit
160
- }
161
- }
162
- ' "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv")
163
-
164
- echo "$agent_id"
165
- }
166
-
167
- # ---------------------------------------------------------------------------
168
- # Resolve agent profile
169
-
170
- AGENT_ID=$(map_to_agent_id "$AGENT_NAME_OR_ID")
171
-
172
- # Read per-agent profile from bmad-voice-map.json (takes priority)
173
- PROFILE_VOICE=""
174
- PROFILE_PRETEXT=""
175
- PROFILE_REVERB=""
176
- PROFILE_PERSONALITY=""
177
- PROFILE_MUSIC_TRACK=""
178
- PROFILE_MUSIC_VOLUME=""
179
- PROFILE_MUSIC_ENABLED=""
180
-
181
- if [[ -n "$AGENT_ID" ]] && [[ -f "$VOICE_MAP_FILE" ]]; then
182
- # Single Node.js call for all fields — avoids ~900ms of per-call startup overhead
183
- _ALL_FIELDS=$(read_agent_profile_all "$AGENT_ID")
184
- IFS='|' read -r PROFILE_VOICE PROFILE_PRETEXT PROFILE_REVERB PROFILE_PERSONALITY PROFILE_MUSIC_TRACK PROFILE_MUSIC_VOLUME PROFILE_MUSIC_ENABLED <<< "$_ALL_FIELDS"
185
- fi
186
-
187
- # Read global background music volume as fallback (stored as 0.0-1.0, convert to 0-100 integer)
188
- _BG_VOL_FILE="${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}/.claude/config/background-music-volume.txt"
189
- if [[ ! -f "$_BG_VOL_FILE" ]]; then
190
- _BG_VOL_FILE="$HOME/.claude/config/background-music-volume.txt"
191
- fi
192
- if [[ -f "$_BG_VOL_FILE" ]]; then
193
- GLOBAL_BG_VOLUME=$(_BG_VOL_RAW=$(cat "$_BG_VOL_FILE") node -e "
194
- const v = parseFloat(process.env._BG_VOL_RAW);
195
- process.stdout.write(isNaN(v) ? '20' : String(Math.round(v * 100)));
196
- " 2>/dev/null || echo "20")
197
- else
198
- GLOBAL_BG_VOLUME=20
199
- fi
200
-
201
- # Fallback to bmad-voice-manager.sh if no profile voice found
202
- AGENT_VOICE="$PROFILE_VOICE"
203
- AGENT_INTRO="$PROFILE_PRETEXT"
204
-
205
- if [[ -z "$AGENT_VOICE" ]] && [[ -n "$AGENT_ID" ]] && [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then
206
- AGENT_VOICE=$(cd "$PROJECT_ROOT" && bash "$SCRIPT_DIR/bmad-voice-manager.sh" get-voice "$AGENT_ID" 2>/dev/null || true)
207
- fi
208
-
209
- if [[ -z "$AGENT_INTRO" ]] && [[ -n "$AGENT_ID" ]] && [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then
210
- AGENT_INTRO=$(cd "$PROJECT_ROOT" && bash "$SCRIPT_DIR/bmad-voice-manager.sh" get-intro "$AGENT_ID" 2>/dev/null || true)
211
- fi
212
-
213
- # ---------------------------------------------------------------------------
214
- # Write PID-scoped temp profile file for per-agent overrides
215
- # play-tts-enhanced.sh and queue worker read this for reverb/personality/music
216
-
217
- TEMP_PROFILE=""
218
- if [[ -n "$PROFILE_REVERB" ]] || [[ -n "$PROFILE_PERSONALITY" ]] || [[ -n "$PROFILE_MUSIC_TRACK" ]]; then
219
- PROFILE_DIR="${XDG_RUNTIME_DIR:-/tmp}/agentvibes-$(id -u)"
220
- mkdir -p "$PROFILE_DIR"
221
- chmod 700 "$PROFILE_DIR"
222
- TEMP_PROFILE="$PROFILE_DIR/agent-profile-$$.json"
223
-
224
- # Write profile as JSON for reliable parsing downstream
225
- # SECURITY: Pass values via env vars to prevent shell injection
226
- _P_REVERB="$PROFILE_REVERB" _P_PERSONALITY="$PROFILE_PERSONALITY" \
227
- _P_MUSIC_TRACK="$PROFILE_MUSIC_TRACK" _P_MUSIC_VOL="${PROFILE_MUSIC_VOLUME:-$GLOBAL_BG_VOLUME}" \
228
- _P_MUSIC_ENABLED="$PROFILE_MUSIC_ENABLED" \
229
- _P_OUTFILE="$TEMP_PROFILE" node -e "
230
- const p = {};
231
- if (process.env._P_REVERB) p.reverbPreset = process.env._P_REVERB;
232
- if (process.env._P_PERSONALITY) p.personality = process.env._P_PERSONALITY;
233
- if (process.env._P_MUSIC_TRACK) p.backgroundMusic = {
234
- track: process.env._P_MUSIC_TRACK,
235
- volume: parseInt(process.env._P_MUSIC_VOL) || 20,
236
- enabled: process.env._P_MUSIC_ENABLED === 'true'
237
- };
238
- require('fs').writeFileSync(process.env._P_OUTFILE, JSON.stringify(p), { mode: 0o600 });
239
- " 2>/dev/null || true
240
-
241
- # NOTE: Do NOT clean up temp profile here — the queue worker processes it
242
- # asynchronously and cleans it up after use (see tts-queue-worker.sh).
243
- # Removing it here would race with the background queue consumer.
244
- fi
245
-
246
- # ---------------------------------------------------------------------------
247
- # Build full text with intro/pretext
248
-
249
- FULL_TEXT="$DIALOGUE"
250
- if [[ -n "$AGENT_INTRO" ]]; then
251
- FULL_TEXT="${AGENT_INTRO}. ${DIALOGUE}"
252
- fi
253
-
254
-
255
- # Serialize speech — prevents overlap when Claude fires parallel calls
256
- # Uses mkdir as a portable atomic lock (works on Linux, macOS, WSL)
257
- SPEECH_LOCK="${XDG_RUNTIME_DIR:-/tmp}/agentvibes-speech.lock"
258
-
259
- # Acquire lock (wait up to 120s, retry every 0.5s)
260
- # Clean up stale file locks from older flock-based version
261
- [[ -f "$SPEECH_LOCK" ]] && rm -f "$SPEECH_LOCK"
262
- _WAIT=0
263
- while ! mkdir "$SPEECH_LOCK" 2>/dev/null; do
264
- if [[ -e "$SPEECH_LOCK" ]]; then
265
- _LOCK_AGE=$(( $(date +%s) - $(stat -c '%Y' "$SPEECH_LOCK" 2>/dev/null || stat -f '%m' "$SPEECH_LOCK" 2>/dev/null || echo 0) ))
266
- [[ $_LOCK_AGE -gt 60 ]] && { rm -rf "$SPEECH_LOCK" 2>/dev/null || true; continue; }
267
- fi
268
- sleep 0.5
269
- _WAIT=$((_WAIT + 1))
270
- [[ $_WAIT -gt 240 ]] && break
271
- done
272
- trap 'rmdir "$SPEECH_LOCK" 2>/dev/null' EXIT
273
-
274
- # Speak with agent's voice, passing the temp profile path as arg 3 so
275
- # play-tts-piper.sh → audio-processor.sh can read per-agent music settings
276
- # without any env vars (safe for concurrent multi-project use).
277
- if [[ -n "$AGENT_VOICE" ]]; then
278
- bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "$AGENT_VOICE" "$TEMP_PROFILE"
279
- else
280
- bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "" "$TEMP_PROFILE"
281
- fi
282
-
283
- # Release lock
284
- rmdir "$SPEECH_LOCK" 2>/dev/null || true
285
- trap - EXIT
286
-
287
- # Clean up temp profile after use
288
- if [[ -n "$TEMP_PROFILE" ]] && [[ -f "$TEMP_PROFILE" ]]; then
289
- rm -f "$TEMP_PROFILE"
290
- fi
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/bmad-speak.sh
4
+ #
5
+ # AgentVibes BMAD Voice Integration
6
+ # Maps agent display names OR agent IDs to voices and triggers TTS
7
+ #
8
+ # Usage: bmad-speak.sh "Agent Name" "dialogue text"
9
+ # bmad-speak.sh "agent-id" "dialogue text"
10
+ #
11
+ # Supports both:
12
+ # - Display names (e.g., "Winston", "John") for party mode
13
+ # - Agent IDs (e.g., "architect", "pm") for individual agents
14
+ #
15
+
16
+ set -euo pipefail
17
+
18
+ # Get script directory and project root
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
21
+
22
+ # Arguments
23
+ AGENT_NAME_OR_ID="$1"
24
+ DIALOGUE="$2"
25
+
26
+ # Remove backslash escaping that Claude might add for special chars like ! and $
27
+ # In single quotes these don't need escaping, but Claude sometimes adds \! anyway
28
+ DIALOGUE="${DIALOGUE//\\!/!}"
29
+ DIALOGUE="${DIALOGUE//\\\$/\$}"
30
+
31
+ # Strip markdown formatting — prevent Piper from speaking "asterisk asterisk" literally.
32
+ # play-tts-piper.sh also strips via perl, but do it here early as defense-in-depth.
33
+ DIALOGUE=$(printf '%s' "$DIALOGUE" | sed \
34
+ -e 's/\*\{1,3\}//g' \
35
+ -e 's/`\{1,3\}[^`]*`\{1,3\}//g' \
36
+ -e 's/^[[:space:]]*#\{1,6\}[[:space:]]*//g' \
37
+ -e 's/__//g' -e 's/_//g' \
38
+ -e 's/\[([^]]*)\]([^)]*)//g' \
39
+ -e 's/^[[:space:]]*[-*+] //g' \
40
+ -e 's/^[[:space:]]*[0-9]\+\. //g')
41
+
42
+ # Check if party mode is enabled
43
+ if [[ -f "$PROJECT_ROOT/.agentvibes/bmad/bmad-party-mode-disabled.flag" ]]; then
44
+ exit 0
45
+ fi
46
+
47
+ # Check if BMAD is installed
48
+ if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
49
+ exit 0
50
+ fi
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Per-agent profile reader — reads from ~/.agentvibes/bmad-voice-map.json
54
+ # Uses node for reliable JSON parsing (jq may not be installed)
55
+ # Returns empty string if field not found or file missing
56
+
57
+ VOICE_MAP_FILE="$HOME/.agentvibes/bmad-voice-map.json"
58
+
59
+ # Read a field from the per-agent profile in bmad-voice-map.json
60
+ # Usage: read_agent_profile <agent_id> <field>
61
+ # Fields: voice, pretext, reverbPreset, personality, backgroundMusic.track, backgroundMusic.volume
62
+ read_agent_profile() {
63
+ local agent_id="$1"
64
+ local field="$2"
65
+
66
+ if [[ ! -f "$VOICE_MAP_FILE" ]]; then
67
+ echo ""
68
+ return
69
+ fi
70
+
71
+ # Validate agent_id format (prevent injection)
72
+ if [[ ! "$agent_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
73
+ echo ""
74
+ return
75
+ fi
76
+
77
+ # Use node for JSON parsing (always available in AgentVibes projects)
78
+ # SECURITY: Pass values via env vars to prevent shell injection
79
+ _VOICE_MAP="$VOICE_MAP_FILE" _AGENT_ID="$agent_id" _FIELD="$field" node -e "
80
+ try {
81
+ const d = JSON.parse(require('fs').readFileSync(process.env._VOICE_MAP,'utf8'));
82
+ const a = d.agents?.[process.env._AGENT_ID] ?? {};
83
+ const f = process.env._FIELD;
84
+ if (f.includes('.')) {
85
+ const [k1, k2] = f.split('.');
86
+ process.stdout.write(String(a[k1]?.[k2] ?? ''));
87
+ } else {
88
+ process.stdout.write(String(a[f] ?? ''));
89
+ }
90
+ } catch { process.stdout.write(''); }
91
+ " 2>/dev/null || echo ""
92
+ }
93
+
94
+ # Read all profile fields in a single Node.js invocation to avoid ~900ms of overhead.
95
+ # Returns: voice|pretext|reverbPreset|personality|backgroundMusic.track|backgroundMusic.volume
96
+ # Outputs `|||||` if the file is missing or the agent is not found.
97
+ # SECURITY: Pass values via env vars to prevent shell injection
98
+ read_agent_profile_all() {
99
+ local agent_id="$1"
100
+
101
+ # Validate agent_id format (prevent injection)
102
+ if [[ ! "$agent_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
103
+ echo "|||||"
104
+ return
105
+ fi
106
+
107
+ if [[ ! -f "$VOICE_MAP_FILE" ]]; then
108
+ echo "||||||"
109
+ return
110
+ fi
111
+
112
+ _VOICE_MAP="$VOICE_MAP_FILE" _AGENT_ID="$agent_id" node -e "
113
+ try {
114
+ const d = JSON.parse(require('fs').readFileSync(process.env._VOICE_MAP,'utf8'));
115
+ const a = d.agents?.[process.env._AGENT_ID] ?? {};
116
+ const fields = [
117
+ String(a.voice ?? ''),
118
+ String(a.pretext ?? ''),
119
+ String(a.reverbPreset ?? ''),
120
+ String(a.personality ?? ''),
121
+ String(a.backgroundMusic?.track ?? ''),
122
+ String(a.backgroundMusic?.volume ?? ''),
123
+ String(a.backgroundMusic?.enabled ?? ''),
124
+ ];
125
+ process.stdout.write(fields.join('|'));
126
+ } catch { process.stdout.write('||||||'); }
127
+ " 2>/dev/null || echo "||||||"
128
+ }
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Map display name to agent ID
132
+
133
+ map_to_agent_id() {
134
+ local name_or_id="$1"
135
+
136
+ # If it looks like a file path (.bmad/*/agents/*.md), extract the agent ID
137
+ if [[ "$name_or_id" =~ _?\.?bmad/.*/agents/([^/]+)\.md$ ]]; then
138
+ echo "${BASH_REMATCH[1]}"
139
+ return
140
+ fi
141
+
142
+ # First check if it's already an agent ID (column 1 of manifest)
143
+ local direct_match=$(grep -i "^\"*${name_or_id}\"*," "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" | head -1)
144
+ if [[ -n "$direct_match" ]]; then
145
+ echo "$name_or_id"
146
+ return
147
+ fi
148
+
149
+ # Otherwise map display name to agent ID (for party mode)
150
+ local agent_id=$(awk -F',' -v name="$name_or_id" '
151
+ BEGIN { IGNORECASE=1 }
152
+ NR > 1 {
153
+ display = $2
154
+ gsub(/^"|"$/, "", display)
155
+ if (tolower(display) ~ "^" tolower(name) "($| |\\()") {
156
+ agent = $1
157
+ gsub(/^"|"$/, "", agent)
158
+ print agent
159
+ exit
160
+ }
161
+ }
162
+ ' "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv")
163
+
164
+ echo "$agent_id"
165
+ }
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Resolve agent profile
169
+
170
+ AGENT_ID=$(map_to_agent_id "$AGENT_NAME_OR_ID")
171
+
172
+ # Read per-agent profile from bmad-voice-map.json (takes priority)
173
+ PROFILE_VOICE=""
174
+ PROFILE_PRETEXT=""
175
+ PROFILE_REVERB=""
176
+ PROFILE_PERSONALITY=""
177
+ PROFILE_MUSIC_TRACK=""
178
+ PROFILE_MUSIC_VOLUME=""
179
+ PROFILE_MUSIC_ENABLED=""
180
+
181
+ if [[ -n "$AGENT_ID" ]] && [[ -f "$VOICE_MAP_FILE" ]]; then
182
+ # Single Node.js call for all fields — avoids ~900ms of per-call startup overhead
183
+ _ALL_FIELDS=$(read_agent_profile_all "$AGENT_ID")
184
+ IFS='|' read -r PROFILE_VOICE PROFILE_PRETEXT PROFILE_REVERB PROFILE_PERSONALITY PROFILE_MUSIC_TRACK PROFILE_MUSIC_VOLUME PROFILE_MUSIC_ENABLED <<< "$_ALL_FIELDS"
185
+ fi
186
+
187
+ # Read global background music volume as fallback (stored as 0.0-1.0, convert to 0-100 integer)
188
+ _BG_VOL_FILE="${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}/.claude/config/background-music-volume.txt"
189
+ if [[ ! -f "$_BG_VOL_FILE" ]]; then
190
+ _BG_VOL_FILE="$HOME/.claude/config/background-music-volume.txt"
191
+ fi
192
+ if [[ -f "$_BG_VOL_FILE" ]]; then
193
+ GLOBAL_BG_VOLUME=$(_BG_VOL_RAW=$(cat "$_BG_VOL_FILE") node -e "
194
+ const v = parseFloat(process.env._BG_VOL_RAW);
195
+ process.stdout.write(isNaN(v) ? '20' : String(Math.round(v * 100)));
196
+ " 2>/dev/null || echo "20")
197
+ else
198
+ GLOBAL_BG_VOLUME=20
199
+ fi
200
+
201
+ # Fallback to bmad-voice-manager.sh if no profile voice found
202
+ AGENT_VOICE="$PROFILE_VOICE"
203
+ AGENT_INTRO="$PROFILE_PRETEXT"
204
+
205
+ if [[ -z "$AGENT_VOICE" ]] && [[ -n "$AGENT_ID" ]] && [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then
206
+ AGENT_VOICE=$(cd "$PROJECT_ROOT" && bash "$SCRIPT_DIR/bmad-voice-manager.sh" get-voice "$AGENT_ID" 2>/dev/null || true)
207
+ fi
208
+
209
+ if [[ -z "$AGENT_INTRO" ]] && [[ -n "$AGENT_ID" ]] && [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then
210
+ AGENT_INTRO=$(cd "$PROJECT_ROOT" && bash "$SCRIPT_DIR/bmad-voice-manager.sh" get-intro "$AGENT_ID" 2>/dev/null || true)
211
+ fi
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # Write PID-scoped temp profile file for per-agent overrides
215
+ # play-tts-enhanced.sh and queue worker read this for reverb/personality/music
216
+
217
+ TEMP_PROFILE=""
218
+ if [[ -n "$PROFILE_REVERB" ]] || [[ -n "$PROFILE_PERSONALITY" ]] || [[ -n "$PROFILE_MUSIC_TRACK" ]]; then
219
+ PROFILE_DIR="${XDG_RUNTIME_DIR:-/tmp}/agentvibes-$(id -u)"
220
+ mkdir -p "$PROFILE_DIR"
221
+ chmod 700 "$PROFILE_DIR"
222
+ TEMP_PROFILE="$PROFILE_DIR/agent-profile-$$.json"
223
+
224
+ # Write profile as JSON for reliable parsing downstream
225
+ # SECURITY: Pass values via env vars to prevent shell injection
226
+ _P_REVERB="$PROFILE_REVERB" _P_PERSONALITY="$PROFILE_PERSONALITY" \
227
+ _P_MUSIC_TRACK="$PROFILE_MUSIC_TRACK" _P_MUSIC_VOL="${PROFILE_MUSIC_VOLUME:-$GLOBAL_BG_VOLUME}" \
228
+ _P_MUSIC_ENABLED="$PROFILE_MUSIC_ENABLED" \
229
+ _P_OUTFILE="$TEMP_PROFILE" node -e "
230
+ const p = {};
231
+ if (process.env._P_REVERB) p.reverbPreset = process.env._P_REVERB;
232
+ if (process.env._P_PERSONALITY) p.personality = process.env._P_PERSONALITY;
233
+ if (process.env._P_MUSIC_TRACK) p.backgroundMusic = {
234
+ track: process.env._P_MUSIC_TRACK,
235
+ volume: parseInt(process.env._P_MUSIC_VOL) || 20,
236
+ enabled: process.env._P_MUSIC_ENABLED === 'true'
237
+ };
238
+ require('fs').writeFileSync(process.env._P_OUTFILE, JSON.stringify(p), { mode: 0o600 });
239
+ " 2>/dev/null || true
240
+
241
+ # NOTE: Do NOT clean up temp profile here — the queue worker processes it
242
+ # asynchronously and cleans it up after use (see tts-queue-worker.sh).
243
+ # Removing it here would race with the background queue consumer.
244
+ fi
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Build full text with intro/pretext
248
+
249
+ FULL_TEXT="$DIALOGUE"
250
+ if [[ -n "$AGENT_INTRO" ]]; then
251
+ FULL_TEXT="${AGENT_INTRO}. ${DIALOGUE}"
252
+ fi
253
+
254
+
255
+ # Serialize speech — prevents overlap when Claude fires parallel calls
256
+ # Uses mkdir as a portable atomic lock (works on Linux, macOS, WSL)
257
+ SPEECH_LOCK="${XDG_RUNTIME_DIR:-/tmp}/agentvibes-speech.lock"
258
+
259
+ # Acquire lock (wait up to 120s, retry every 0.5s)
260
+ # Clean up stale file locks from older flock-based version
261
+ [[ -f "$SPEECH_LOCK" ]] && rm -f "$SPEECH_LOCK"
262
+ _WAIT=0
263
+ while ! mkdir "$SPEECH_LOCK" 2>/dev/null; do
264
+ if [[ -e "$SPEECH_LOCK" ]]; then
265
+ _LOCK_AGE=$(( $(date +%s) - $(stat -c '%Y' "$SPEECH_LOCK" 2>/dev/null || stat -f '%m' "$SPEECH_LOCK" 2>/dev/null || echo 0) ))
266
+ [[ $_LOCK_AGE -gt 60 ]] && { rm -rf "$SPEECH_LOCK" 2>/dev/null || true; continue; }
267
+ fi
268
+ sleep 0.5
269
+ _WAIT=$((_WAIT + 1))
270
+ [[ $_WAIT -gt 240 ]] && break
271
+ done
272
+ trap 'rmdir "$SPEECH_LOCK" 2>/dev/null' EXIT
273
+
274
+ # Speak with agent's voice, passing the temp profile path as arg 3 so
275
+ # play-tts-piper.sh → audio-processor.sh can read per-agent music settings
276
+ # without any env vars (safe for concurrent multi-project use).
277
+ if [[ -n "$AGENT_VOICE" ]]; then
278
+ bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "$AGENT_VOICE" "$TEMP_PROFILE"
279
+ else
280
+ bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "" "$TEMP_PROFILE"
281
+ fi
282
+
283
+ # Release lock
284
+ rmdir "$SPEECH_LOCK" 2>/dev/null || true
285
+ trap - EXIT
286
+
287
+ # Clean up temp profile after use
288
+ if [[ -n "$TEMP_PROFILE" ]] && [[ -f "$TEMP_PROFILE" ]]; then
289
+ rm -f "$TEMP_PROFILE"
290
+ fi