agentvibes 5.1.4 → 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 (69) hide show
  1. package/.agentvibes/config.json +23 -13
  2. package/.claude/commands/agent-vibes/verbosity.md +98 -89
  3. package/.claude/config/audio-effects.cfg +4 -1
  4. package/.claude/hooks/audio-cache-utils.sh +246 -246
  5. package/.claude/hooks/background-music-manager.sh +404 -404
  6. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  7. package/.claude/hooks/bmad-speak.sh +290 -290
  8. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  9. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  10. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  11. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  12. package/.claude/hooks/clean-audio-cache.sh +22 -22
  13. package/.claude/hooks/cleanup-cache.sh +106 -106
  14. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  15. package/.claude/hooks/download-extra-voices.sh +244 -244
  16. package/.claude/hooks/effects-manager.sh +268 -268
  17. package/.claude/hooks/github-star-reminder.sh +154 -154
  18. package/.claude/hooks/language-manager.sh +362 -362
  19. package/.claude/hooks/learn-manager.sh +492 -492
  20. package/.claude/hooks/macos-voice-manager.sh +205 -205
  21. package/.claude/hooks/migrate-background-music.sh +125 -125
  22. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  23. package/.claude/hooks/optimize-background-music.sh +87 -87
  24. package/.claude/hooks/path-resolver.sh +60 -60
  25. package/.claude/hooks/personality-manager.sh +448 -448
  26. package/.claude/hooks/piper-download-voices.sh +233 -225
  27. package/.claude/hooks/piper-installer.sh +292 -292
  28. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  29. package/.claude/hooks/piper-voice-manager.sh +125 -0
  30. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +97 -90
  31. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  32. package/.claude/hooks/play-tts-piper.sh +16 -5
  33. package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
  34. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  35. package/.claude/hooks/play-tts.sh +35 -14
  36. package/.claude/hooks/prepare-release.sh +54 -54
  37. package/.claude/hooks/provider-commands.sh +617 -617
  38. package/.claude/hooks/provider-manager.sh +399 -399
  39. package/.claude/hooks/replay-target-audio.sh +95 -95
  40. package/.claude/hooks/sentiment-manager.sh +201 -201
  41. package/.claude/hooks/session-start-tts.sh +4 -1
  42. package/.claude/hooks/speed-manager.sh +291 -291
  43. package/.claude/hooks/stop-tts.sh +84 -84
  44. package/.claude/hooks/termux-installer.sh +261 -261
  45. package/.claude/hooks/translate-manager.sh +341 -341
  46. package/.claude/hooks/tts-queue-worker.sh +145 -145
  47. package/.claude/hooks/tts-queue.sh +165 -165
  48. package/.claude/hooks/verbosity-manager.sh +185 -178
  49. package/.claude/hooks/voice-manager.sh +552 -548
  50. package/.claude/hooks-windows/download-extra-voices.ps1 +243 -185
  51. package/.claude/hooks-windows/play-tts-piper.ps1 +7 -2
  52. package/.claude/hooks-windows/play-tts.ps1 +9 -3
  53. package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
  54. package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
  55. package/README.md +19 -2
  56. package/RELEASE_NOTES.md +74 -0
  57. package/bin/agentvibes-voice-browser.js +1939 -1840
  58. package/bin/mcp-server.sh +206 -206
  59. package/mcp-server/server.py +87 -15
  60. package/package.json +1 -1
  61. package/src/console/tabs/receiver-tab.js +1527 -1483
  62. package/src/console/tabs/settings-tab.js +2 -2
  63. package/src/console/tabs/setup-tab.js +112 -31
  64. package/src/console/tabs/voices-tab.js +130 -13
  65. package/src/i18n/en.js +202 -202
  66. package/src/installer.js +79 -213
  67. package/src/services/llm-provider-service.js +126 -75
  68. package/src/services/verbosity-service.js +159 -157
  69. package/templates/agentvibes-receiver.sh +3 -2
@@ -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" && "$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" && "$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