agentvibes 4.0.0 → 4.2.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.
- package/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/hooks/audio-processor.sh +87 -43
- package/.claude/hooks/bmad-speak.sh +184 -27
- package/.claude/hooks/play-tts-enhanced.sh +40 -5
- package/.claude/hooks/play-tts-macos.sh +29 -6
- package/.claude/hooks/play-tts-piper.sh +174 -67
- package/.claude/hooks/play-tts-soprano.sh +42 -6
- package/.claude/hooks/play-tts-ssh-remote.sh +117 -38
- package/.claude/hooks/play-tts.sh +12 -9
- package/.claude/hooks/session-start-tts.sh +10 -0
- package/.claude/hooks/stop-tts.sh +84 -0
- package/.claude/hooks/tts-queue-worker.sh +51 -20
- package/.claude/hooks/tts-queue.sh +37 -8
- package/.claude/hooks/voice-manager.sh +5 -1
- package/CLAUDE.md +0 -11
- package/README.md +176 -78
- package/RELEASE_NOTES.md +1197 -60
- package/bin/agentvibes-voice-browser.js +35 -21
- package/mcp-server/server.py +36 -0
- package/package.json +1 -3
- package/src/console/app.js +23 -5
- package/src/console/constants/personalities.js +44 -0
- package/src/console/footer-config.js +8 -0
- package/src/console/navigation.js +3 -1
- package/src/console/tabs/agents-tab.js +1219 -72
- package/src/console/tabs/install-tab.js +2 -1
- package/src/console/tabs/placeholder-tab.js +9 -1
- package/src/console/tabs/receiver-tab.js +1212 -0
- package/src/console/tabs/settings-tab.js +33 -323
- package/src/console/widgets/destroy-list.js +25 -0
- package/src/console/widgets/format-utils.js +89 -0
- package/src/console/widgets/notice.js +55 -0
- package/src/console/widgets/personality-picker.js +185 -0
- package/src/console/widgets/reverb-picker.js +94 -0
- package/src/console/widgets/track-picker.js +285 -0
- package/src/installer.js +54 -2
- package/src/services/agent-voice-store.js +282 -22
- package/src/services/config-service.js +24 -0
- package/src/services/navigation-service.js +1 -1
- package/src/utils/music-file-validator.js +41 -31
- package/templates/agentvibes-receiver.sh +431 -111
|
@@ -25,7 +25,7 @@ Winston|reverb 40 50 90 gain -2|agentvibes_soft_flamenco_loop.mp3|0.25
|
|
|
25
25
|
Amelia|compand 0.3,1 6:-70,-60,-20|agentvibes_soft_flamenco_loop.mp3|0.25
|
|
26
26
|
|||
|
|
27
27
|
# Business Analyst Mary - warm, analytical|||
|
|
28
|
-
Mary|equalizer 200 1q +3 equalizer 2500 1q -2|agentvibes_soft_flamenco_loop.mp3|0.25
|
|
28
|
+
Mary|reverb 70 50 100 equalizer 200 1q +3 equalizer 2500 1q -2|agentvibes_soft_flamenco_loop.mp3|0.25
|
|
29
29
|
|||
|
|
30
30
|
# Scrum Master Bob - refined, organized|||
|
|
31
31
|
Bob|reverb 30 40 70 compand 0.2,0.5 6:-70,-60,-20|agentvibes_soft_flamenco_loop.mp3|0.25
|
|
@@ -49,4 +49,5 @@ BMad Master|reverb 50 60 100 pitch -100|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
|
49
49
|
_party_mode|compand 0.3,1 6:-70,-60,-20|agent_vibes_dark_chill_step_loop.mp3|0.40
|
|
50
50
|
|||
|
|
51
51
|
# Default (no agent specified) - clean with Bachata background|||
|
|
52
|
-
default|reverb 40 50 70|
|
|
52
|
+
default|reverb 40 50 70|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
53
|
+
analyst|reverb 70 50 100|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
@@ -1 +1 @@
|
|
|
1
|
-
agent_vibes_celtic_harp_v1_loop.mp3
|
|
1
|
+
agent_vibes_celtic_harp_v1_loop.mp3:.00000000000000000012190647
|
|
@@ -34,6 +34,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
34
34
|
INPUT_FILE="${1:-}"
|
|
35
35
|
AGENT_NAME="${2:-default}"
|
|
36
36
|
OUTPUT_FILE="${3:-}"
|
|
37
|
+
AGENT_PROFILE_FILE="${4:-}" # Optional: path to per-agent profile JSON (from bmad-speak.sh)
|
|
37
38
|
|
|
38
39
|
# Config and directories (resolve to absolute paths)
|
|
39
40
|
CONFIG_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/config/audio-effects.cfg"
|
|
@@ -140,9 +141,11 @@ apply_sox_effects() {
|
|
|
140
141
|
|
|
141
142
|
# Position tracking file for continuous playback
|
|
142
143
|
POSITION_FILE="$SCRIPT_DIR/../config/background-music-position.txt"
|
|
144
|
+
# Lock file for position file — prevents race conditions in party mode with concurrent agents
|
|
145
|
+
POSITION_LOCK="/tmp/agentvibes-bgpos-$(id -u).lock"
|
|
143
146
|
|
|
144
147
|
# @function get_background_position
|
|
145
|
-
# @intent Get saved position for a background track
|
|
148
|
+
# @intent Get saved position for a background track (caller must hold POSITION_LOCK)
|
|
146
149
|
# @param $1 Background file path
|
|
147
150
|
# @returns Position in seconds (or 0 if not found)
|
|
148
151
|
get_background_position() {
|
|
@@ -158,7 +161,7 @@ get_background_position() {
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
# @function save_background_position
|
|
161
|
-
# @intent Save position for a background track
|
|
164
|
+
# @intent Save position for a background track (caller must hold POSITION_LOCK)
|
|
162
165
|
# @param $1 Background file path
|
|
163
166
|
# @param $2 New position in seconds
|
|
164
167
|
save_background_position() {
|
|
@@ -173,7 +176,8 @@ save_background_position() {
|
|
|
173
176
|
local tmp_pos
|
|
174
177
|
tmp_pos=$(mktemp "${POSITION_FILE}.XXXXXX")
|
|
175
178
|
if [[ -f "$POSITION_FILE" ]]; then
|
|
176
|
-
grep -
|
|
179
|
+
# SECURITY: Use grep -F for fixed string matching (#134)
|
|
180
|
+
grep -vF "${bg_name}:" "$POSITION_FILE" > "$tmp_pos" 2>/dev/null || true
|
|
177
181
|
fi
|
|
178
182
|
echo "${bg_name}:${position}" >> "$tmp_pos"
|
|
179
183
|
mv "$tmp_pos" "$POSITION_FILE"
|
|
@@ -216,48 +220,58 @@ mix_background() {
|
|
|
216
220
|
bg_duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$background" 2>/dev/null)
|
|
217
221
|
bg_duration=${bg_duration:-0}
|
|
218
222
|
|
|
219
|
-
#
|
|
223
|
+
# Read the start position and pre-compute the new position atomically under flock.
|
|
224
|
+
# This prevents party-mode race conditions where concurrent agents both read the
|
|
225
|
+
# same position, compute independently, and overwrite each other's updates.
|
|
220
226
|
local start_pos
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
#
|
|
230
|
-
if
|
|
231
|
-
start_pos
|
|
227
|
+
local new_pos
|
|
228
|
+
local total_duration
|
|
229
|
+
{
|
|
230
|
+
flock -x 200
|
|
231
|
+
|
|
232
|
+
# Get saved position for this track (continuous playback)
|
|
233
|
+
start_pos=$(get_background_position "$background")
|
|
234
|
+
|
|
235
|
+
# Validate start_pos: if too small (floating point error) or invalid, reset to 0
|
|
236
|
+
if command -v bc &> /dev/null; then
|
|
237
|
+
if ! [[ "$start_pos" =~ ^[0-9]+\.?[0-9]*$ ]] || (( $(echo "$start_pos < 0.001" | bc -l) )); then
|
|
238
|
+
start_pos="0"
|
|
239
|
+
fi
|
|
240
|
+
else
|
|
241
|
+
# Without bc, just check if it's a valid number
|
|
242
|
+
if ! [[ "$start_pos" =~ ^[0-9]+\.?[0-9]*$ ]]; then
|
|
243
|
+
start_pos="0"
|
|
244
|
+
fi
|
|
232
245
|
fi
|
|
233
|
-
fi
|
|
234
246
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
247
|
+
# If position exceeds track length, wrap around
|
|
248
|
+
if command -v bc &> /dev/null && [[ -n "$bg_duration" ]]; then
|
|
249
|
+
if (( $(echo "$start_pos >= $bg_duration" | bc -l) )); then
|
|
250
|
+
start_pos=$(echo "$start_pos % $bg_duration" | bc -l)
|
|
251
|
+
fi
|
|
239
252
|
fi
|
|
240
|
-
fi
|
|
241
253
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
fi
|
|
254
|
+
# Extend total duration by 2 seconds for background music fade out
|
|
255
|
+
if command -v bc &> /dev/null; then
|
|
256
|
+
total_duration=$(echo "$duration + 2" | bc -l)
|
|
257
|
+
else
|
|
258
|
+
total_duration=$(awk "BEGIN {print $duration + 2}")
|
|
259
|
+
fi
|
|
249
260
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
# Calculate new position after this clip (including fade out time)
|
|
262
|
+
if command -v bc &> /dev/null; then
|
|
263
|
+
new_pos=$(echo "$start_pos + $total_duration" | bc -l)
|
|
264
|
+
# Wrap around if needed
|
|
265
|
+
if [[ -n "$bg_duration" ]] && (( $(echo "$new_pos >= $bg_duration" | bc -l) )); then
|
|
266
|
+
new_pos=$(echo "$new_pos % $bg_duration" | bc -l)
|
|
267
|
+
fi
|
|
268
|
+
else
|
|
269
|
+
new_pos="0"
|
|
257
270
|
fi
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
271
|
+
|
|
272
|
+
# Claim the new position immediately so concurrent agents advance past it
|
|
273
|
+
save_background_position "$background" "$new_pos"
|
|
274
|
+
} 200>"$POSITION_LOCK"
|
|
261
275
|
|
|
262
276
|
# Mix: Seek to position in background, apply volume and fades
|
|
263
277
|
# Background fades in at start (0.3s), continues under speech, then fades out over 2s after speech ends
|
|
@@ -302,9 +316,6 @@ mix_background() {
|
|
|
302
316
|
cp "$voice" "$output"
|
|
303
317
|
return
|
|
304
318
|
}
|
|
305
|
-
|
|
306
|
-
# Save new position for next time
|
|
307
|
-
save_background_position "$background" "$new_pos"
|
|
308
319
|
}
|
|
309
320
|
|
|
310
321
|
# Main processing
|
|
@@ -318,6 +329,25 @@ main() {
|
|
|
318
329
|
# Parse config (format: NAME|EFFECTS|BACKGROUND|VOLUME)
|
|
319
330
|
IFS='|' read -r _ sox_effects background_file bg_volume <<< "$config"
|
|
320
331
|
|
|
332
|
+
# Per-agent background music override from bmad-speak.sh profile JSON (takes priority over cfg).
|
|
333
|
+
# The profile file is a PID-scoped temp file written by bmad-speak.sh; no env var leakage.
|
|
334
|
+
if [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
|
|
335
|
+
# SECURITY: Pass profile path via env var to avoid shell injection in node -e string
|
|
336
|
+
local _prof_track _prof_vol _prof_enabled
|
|
337
|
+
_prof_track=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(p.backgroundMusic?.track??'')}catch{process.stdout.write('')}" 2>/dev/null || true)
|
|
338
|
+
_prof_vol=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.volume??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
|
|
339
|
+
_prof_enabled=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.enabled??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
|
|
340
|
+
if [[ "$_prof_enabled" == "true" ]] && [[ -n "$_prof_track" ]]; then
|
|
341
|
+
background_file="$_prof_track"
|
|
342
|
+
# Convert percentage volume (0-100) to decimal (0.0-1.0) for ffmpeg
|
|
343
|
+
if [[ "$_prof_vol" =~ ^[0-9]+$ ]]; then
|
|
344
|
+
bg_volume=$(awk "BEGIN{printf \"%.2f\", ${_prof_vol}/100}")
|
|
345
|
+
else
|
|
346
|
+
bg_volume="0.70"
|
|
347
|
+
fi
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
|
|
321
351
|
# SECURITY: Use secure temp directory per CLAUDE.md guidelines
|
|
322
352
|
# Prefer XDG_RUNTIME_DIR (user-owned, restricted permissions)
|
|
323
353
|
# Fall back to user-specific directory in /tmp
|
|
@@ -338,7 +368,10 @@ main() {
|
|
|
338
368
|
chmod 700 "$TEMP_DIR"
|
|
339
369
|
|
|
340
370
|
# SECURITY: Verify ownership of temp directory
|
|
341
|
-
|
|
371
|
+
# SECURITY: Handle stat failure explicitly (#134)
|
|
372
|
+
local _dir_uid
|
|
373
|
+
_dir_uid=$(stat -c '%u' "$TEMP_DIR" 2>/dev/null || stat -f '%u' "$TEMP_DIR" 2>/dev/null)
|
|
374
|
+
if [[ -z "$_dir_uid" ]] || [[ "$_dir_uid" != "$(id -u)" ]]; then
|
|
342
375
|
echo "Error: Temp directory not owned by current user: $TEMP_DIR" >&2
|
|
343
376
|
exit 1
|
|
344
377
|
fi
|
|
@@ -369,8 +402,19 @@ main() {
|
|
|
369
402
|
background_path="$BACKGROUNDS_DIR/$background_file"
|
|
370
403
|
fi
|
|
371
404
|
|
|
405
|
+
# Per-agent profile enables music independently of the global flag.
|
|
406
|
+
local _bg_allowed=false
|
|
407
|
+
if is_background_music_enabled; then
|
|
408
|
+
_bg_allowed=true
|
|
409
|
+
elif [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
|
|
410
|
+
# A valid agent profile with enabled=true overrides the global off switch.
|
|
411
|
+
local _check_enabled
|
|
412
|
+
_check_enabled=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.enabled??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
|
|
413
|
+
[[ "$_check_enabled" == "true" ]] && _bg_allowed=true
|
|
414
|
+
fi
|
|
415
|
+
|
|
372
416
|
local used_background=""
|
|
373
|
-
if
|
|
417
|
+
if [[ "$_bg_allowed" == "true" ]] && [[ -n "$background_path" ]] && [[ -f "$background_path" ]] && [[ "${bg_volume:-0}" != "0" ]] && [[ "${bg_volume:-0}" != "0.0" ]]; then
|
|
374
418
|
echo " → Mixing background: $background_file at ${bg_volume} volume" >&2
|
|
375
419
|
mix_background "$temp_effects" "$background_path" "$bg_volume" "$temp_final"
|
|
376
420
|
used_background="$background_path" # Return full path instead of just filename
|
|
@@ -28,6 +28,10 @@ DIALOGUE="$2"
|
|
|
28
28
|
DIALOGUE="${DIALOGUE//\\!/!}"
|
|
29
29
|
DIALOGUE="${DIALOGUE//\\\$/\$}"
|
|
30
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 's/\*\*//g; s/\*//g; s/`//g; s/^[[:space:]]*#\+[[:space:]]*//')
|
|
34
|
+
|
|
31
35
|
# Check if party mode is enabled
|
|
32
36
|
if [[ -f "$PROJECT_ROOT/.agentvibes/bmad/bmad-party-mode-disabled.flag" ]]; then
|
|
33
37
|
exit 0
|
|
@@ -38,40 +42,110 @@ if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
|
|
|
38
42
|
exit 0
|
|
39
43
|
fi
|
|
40
44
|
|
|
41
|
-
#
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Per-agent profile reader — reads from ~/.agentvibes/bmad-voice-map.json
|
|
47
|
+
# Uses node for reliable JSON parsing (jq may not be installed)
|
|
48
|
+
# Returns empty string if field not found or file missing
|
|
49
|
+
|
|
50
|
+
VOICE_MAP_FILE="$HOME/.agentvibes/bmad-voice-map.json"
|
|
51
|
+
|
|
52
|
+
# Read a field from the per-agent profile in bmad-voice-map.json
|
|
53
|
+
# Usage: read_agent_profile <agent_id> <field>
|
|
54
|
+
# Fields: voice, pretext, reverbPreset, personality, backgroundMusic.track, backgroundMusic.volume
|
|
55
|
+
read_agent_profile() {
|
|
56
|
+
local agent_id="$1"
|
|
57
|
+
local field="$2"
|
|
58
|
+
|
|
59
|
+
if [[ ! -f "$VOICE_MAP_FILE" ]]; then
|
|
60
|
+
echo ""
|
|
61
|
+
return
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Validate agent_id format (prevent injection)
|
|
65
|
+
if [[ ! "$agent_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
66
|
+
echo ""
|
|
67
|
+
return
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Use node for JSON parsing (always available in AgentVibes projects)
|
|
71
|
+
# SECURITY: Pass values via env vars to prevent shell injection
|
|
72
|
+
_VOICE_MAP="$VOICE_MAP_FILE" _AGENT_ID="$agent_id" _FIELD="$field" node -e "
|
|
73
|
+
try {
|
|
74
|
+
const d = JSON.parse(require('fs').readFileSync(process.env._VOICE_MAP,'utf8'));
|
|
75
|
+
const a = d.agents?.[process.env._AGENT_ID] ?? {};
|
|
76
|
+
const f = process.env._FIELD;
|
|
77
|
+
if (f.includes('.')) {
|
|
78
|
+
const [k1, k2] = f.split('.');
|
|
79
|
+
process.stdout.write(String(a[k1]?.[k2] ?? ''));
|
|
80
|
+
} else {
|
|
81
|
+
process.stdout.write(String(a[f] ?? ''));
|
|
82
|
+
}
|
|
83
|
+
} catch { process.stdout.write(''); }
|
|
84
|
+
" 2>/dev/null || echo ""
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Read all profile fields in a single Node.js invocation to avoid ~900ms of overhead.
|
|
88
|
+
# Returns: voice|pretext|reverbPreset|personality|backgroundMusic.track|backgroundMusic.volume
|
|
89
|
+
# Outputs `|||||` if the file is missing or the agent is not found.
|
|
90
|
+
# SECURITY: Pass values via env vars to prevent shell injection
|
|
91
|
+
read_agent_profile_all() {
|
|
92
|
+
local agent_id="$1"
|
|
93
|
+
|
|
94
|
+
# Validate agent_id format (prevent injection)
|
|
95
|
+
if [[ ! "$agent_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
96
|
+
echo "|||||"
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [[ ! -f "$VOICE_MAP_FILE" ]]; then
|
|
101
|
+
echo "||||||"
|
|
102
|
+
return
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
_VOICE_MAP="$VOICE_MAP_FILE" _AGENT_ID="$agent_id" node -e "
|
|
106
|
+
try {
|
|
107
|
+
const d = JSON.parse(require('fs').readFileSync(process.env._VOICE_MAP,'utf8'));
|
|
108
|
+
const a = d.agents?.[process.env._AGENT_ID] ?? {};
|
|
109
|
+
const fields = [
|
|
110
|
+
String(a.voice ?? ''),
|
|
111
|
+
String(a.pretext ?? ''),
|
|
112
|
+
String(a.reverbPreset ?? ''),
|
|
113
|
+
String(a.personality ?? ''),
|
|
114
|
+
String(a.backgroundMusic?.track ?? ''),
|
|
115
|
+
String(a.backgroundMusic?.volume ?? ''),
|
|
116
|
+
String(a.backgroundMusic?.enabled ?? ''),
|
|
117
|
+
];
|
|
118
|
+
process.stdout.write(fields.join('|'));
|
|
119
|
+
} catch { process.stdout.write('||||||'); }
|
|
120
|
+
" 2>/dev/null || echo "||||||"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Map display name to agent ID
|
|
125
|
+
|
|
42
126
|
map_to_agent_id() {
|
|
43
127
|
local name_or_id="$1"
|
|
44
128
|
|
|
45
129
|
# If it looks like a file path (.bmad/*/agents/*.md), extract the agent ID
|
|
46
|
-
# Example: .bmad/bmm/agents/pm.md -> pm
|
|
47
130
|
if [[ "$name_or_id" =~ _?\.?bmad/.*/agents/([^/]+)\.md$ ]]; then
|
|
48
131
|
echo "${BASH_REMATCH[1]}"
|
|
49
132
|
return
|
|
50
133
|
fi
|
|
51
134
|
|
|
52
135
|
# First check if it's already an agent ID (column 1 of manifest)
|
|
53
|
-
# CSV format: name,displayName,title,icon,role,...
|
|
54
136
|
local direct_match=$(grep -i "^\"*${name_or_id}\"*," "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" | head -1)
|
|
55
137
|
if [[ -n "$direct_match" ]]; then
|
|
56
|
-
# Already an agent ID, pass through
|
|
57
138
|
echo "$name_or_id"
|
|
58
139
|
return
|
|
59
140
|
fi
|
|
60
141
|
|
|
61
142
|
# Otherwise map display name to agent ID (for party mode)
|
|
62
|
-
# Extract 'name' (column 1) where displayName (column 2) contains the name
|
|
63
|
-
# displayName format in CSV: "John", "Mary", "Winston", etc. (first word before any parentheses)
|
|
64
143
|
local agent_id=$(awk -F',' -v name="$name_or_id" '
|
|
65
144
|
BEGIN { IGNORECASE=1 }
|
|
66
145
|
NR > 1 {
|
|
67
|
-
# Extract displayName (column 2)
|
|
68
146
|
display = $2
|
|
69
|
-
gsub(/^"|"$/, "", display)
|
|
70
|
-
|
|
71
|
-
# Check if display name starts with the search name (case-insensitive)
|
|
72
|
-
# This handles both "John" and "John (Product Manager)"
|
|
147
|
+
gsub(/^"|"$/, "", display)
|
|
73
148
|
if (tolower(display) ~ "^" tolower(name) "($| |\\()") {
|
|
74
|
-
# Extract agent ID (column 1)
|
|
75
149
|
agent = $1
|
|
76
150
|
gsub(/^"|"$/, "", agent)
|
|
77
151
|
print agent
|
|
@@ -83,30 +157,113 @@ map_to_agent_id() {
|
|
|
83
157
|
echo "$agent_id"
|
|
84
158
|
}
|
|
85
159
|
|
|
86
|
-
#
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Resolve agent profile
|
|
162
|
+
|
|
87
163
|
AGENT_ID=$(map_to_agent_id "$AGENT_NAME_OR_ID")
|
|
88
164
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
165
|
+
# Read per-agent profile from bmad-voice-map.json (takes priority)
|
|
166
|
+
PROFILE_VOICE=""
|
|
167
|
+
PROFILE_PRETEXT=""
|
|
168
|
+
PROFILE_REVERB=""
|
|
169
|
+
PROFILE_PERSONALITY=""
|
|
170
|
+
PROFILE_MUSIC_TRACK=""
|
|
171
|
+
PROFILE_MUSIC_VOLUME=""
|
|
172
|
+
PROFILE_MUSIC_ENABLED=""
|
|
173
|
+
|
|
174
|
+
if [[ -n "$AGENT_ID" ]] && [[ -f "$VOICE_MAP_FILE" ]]; then
|
|
175
|
+
# Single Node.js call for all fields — avoids ~900ms of per-call startup overhead
|
|
176
|
+
_ALL_FIELDS=$(read_agent_profile_all "$AGENT_ID")
|
|
177
|
+
IFS='|' read -r PROFILE_VOICE PROFILE_PRETEXT PROFILE_REVERB PROFILE_PERSONALITY PROFILE_MUSIC_TRACK PROFILE_MUSIC_VOLUME PROFILE_MUSIC_ENABLED <<< "$_ALL_FIELDS"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Fallback to bmad-voice-manager.sh if no profile voice found
|
|
181
|
+
AGENT_VOICE="$PROFILE_VOICE"
|
|
182
|
+
AGENT_INTRO="$PROFILE_PRETEXT"
|
|
183
|
+
|
|
184
|
+
if [[ -z "$AGENT_VOICE" ]] && [[ -n "$AGENT_ID" ]] && [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then
|
|
185
|
+
AGENT_VOICE=$(cd "$PROJECT_ROOT" && "$SCRIPT_DIR/bmad-voice-manager.sh" get-voice "$AGENT_ID" 2>/dev/null || true)
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
if [[ -z "$AGENT_INTRO" ]] && [[ -n "$AGENT_ID" ]] && [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then
|
|
189
|
+
AGENT_INTRO=$(cd "$PROJECT_ROOT" && "$SCRIPT_DIR/bmad-voice-manager.sh" get-intro "$AGENT_ID" 2>/dev/null || true)
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Write PID-scoped temp profile file for per-agent overrides
|
|
194
|
+
# play-tts-enhanced.sh and queue worker read this for reverb/personality/music
|
|
195
|
+
|
|
196
|
+
TEMP_PROFILE=""
|
|
197
|
+
if [[ -n "$PROFILE_REVERB" ]] || [[ -n "$PROFILE_PERSONALITY" ]] || [[ -n "$PROFILE_MUSIC_TRACK" ]]; then
|
|
198
|
+
PROFILE_DIR="${XDG_RUNTIME_DIR:-/tmp}/agentvibes-$(id -u)"
|
|
199
|
+
mkdir -p "$PROFILE_DIR"
|
|
200
|
+
chmod 700 "$PROFILE_DIR"
|
|
201
|
+
TEMP_PROFILE="$PROFILE_DIR/agent-profile-$$.json"
|
|
202
|
+
|
|
203
|
+
# Write profile as JSON for reliable parsing downstream
|
|
204
|
+
# SECURITY: Pass values via env vars to prevent shell injection
|
|
205
|
+
_P_REVERB="$PROFILE_REVERB" _P_PERSONALITY="$PROFILE_PERSONALITY" \
|
|
206
|
+
_P_MUSIC_TRACK="$PROFILE_MUSIC_TRACK" _P_MUSIC_VOL="${PROFILE_MUSIC_VOLUME:-70}" \
|
|
207
|
+
_P_MUSIC_ENABLED="$PROFILE_MUSIC_ENABLED" \
|
|
208
|
+
_P_OUTFILE="$TEMP_PROFILE" node -e "
|
|
209
|
+
const p = {};
|
|
210
|
+
if (process.env._P_REVERB) p.reverbPreset = process.env._P_REVERB;
|
|
211
|
+
if (process.env._P_PERSONALITY) p.personality = process.env._P_PERSONALITY;
|
|
212
|
+
if (process.env._P_MUSIC_TRACK) p.backgroundMusic = {
|
|
213
|
+
track: process.env._P_MUSIC_TRACK,
|
|
214
|
+
volume: parseInt(process.env._P_MUSIC_VOL) || 70,
|
|
215
|
+
enabled: process.env._P_MUSIC_ENABLED === 'true'
|
|
216
|
+
};
|
|
217
|
+
require('fs').writeFileSync(process.env._P_OUTFILE, JSON.stringify(p), { mode: 0o600 });
|
|
218
|
+
" 2>/dev/null || true
|
|
219
|
+
|
|
220
|
+
# NOTE: Do NOT clean up temp profile here — the queue worker processes it
|
|
221
|
+
# asynchronously and cleans it up after use (see tts-queue-worker.sh).
|
|
222
|
+
# Removing it here would race with the background queue consumer.
|
|
95
223
|
fi
|
|
96
224
|
|
|
97
|
-
#
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Build full text with intro/pretext
|
|
227
|
+
|
|
98
228
|
FULL_TEXT="$DIALOGUE"
|
|
99
229
|
if [[ -n "$AGENT_INTRO" ]]; then
|
|
100
230
|
FULL_TEXT="${AGENT_INTRO}. ${DIALOGUE}"
|
|
101
231
|
fi
|
|
102
232
|
|
|
103
|
-
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
|
|
233
|
+
|
|
234
|
+
# Serialize speech — prevents overlap when Claude fires parallel calls
|
|
235
|
+
# Uses mkdir as a portable atomic lock (works on Linux, macOS, WSL)
|
|
236
|
+
SPEECH_LOCK="${XDG_RUNTIME_DIR:-/tmp}/agentvibes-speech.lock"
|
|
237
|
+
|
|
238
|
+
# Acquire lock (wait up to 120s, retry every 0.5s)
|
|
239
|
+
# Clean up stale file locks from older flock-based version
|
|
240
|
+
[[ -f "$SPEECH_LOCK" ]] && rm -f "$SPEECH_LOCK"
|
|
241
|
+
_WAIT=0
|
|
242
|
+
while ! mkdir "$SPEECH_LOCK" 2>/dev/null; do
|
|
243
|
+
if [[ -e "$SPEECH_LOCK" ]]; then
|
|
244
|
+
_LOCK_AGE=$(( $(date +%s) - $(stat -c '%Y' "$SPEECH_LOCK" 2>/dev/null || stat -f '%m' "$SPEECH_LOCK" 2>/dev/null || echo 0) ))
|
|
245
|
+
[[ $_LOCK_AGE -gt 60 ]] && { rm -rf "$SPEECH_LOCK" 2>/dev/null || true; continue; }
|
|
246
|
+
fi
|
|
247
|
+
sleep 0.5
|
|
248
|
+
_WAIT=$((_WAIT + 1))
|
|
249
|
+
[[ $_WAIT -gt 240 ]] && break
|
|
250
|
+
done
|
|
251
|
+
trap 'rmdir "$SPEECH_LOCK" 2>/dev/null' EXIT
|
|
252
|
+
|
|
253
|
+
# Speak with agent's voice, passing the temp profile path as arg 3 so
|
|
254
|
+
# play-tts-piper.sh → audio-processor.sh can read per-agent music settings
|
|
255
|
+
# without any env vars (safe for concurrent multi-project use).
|
|
107
256
|
if [[ -n "$AGENT_VOICE" ]]; then
|
|
108
|
-
bash "$SCRIPT_DIR/tts
|
|
257
|
+
bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "$AGENT_VOICE" "$TEMP_PROFILE"
|
|
109
258
|
else
|
|
110
|
-
|
|
111
|
-
|
|
259
|
+
bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "" "$TEMP_PROFILE"
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# Release lock
|
|
263
|
+
rmdir "$SPEECH_LOCK" 2>/dev/null || true
|
|
264
|
+
trap - EXIT
|
|
265
|
+
|
|
266
|
+
# Clean up temp profile after use
|
|
267
|
+
if [[ -n "$TEMP_PROFILE" ]] && [[ -f "$TEMP_PROFILE" ]]; then
|
|
268
|
+
rm -f "$TEMP_PROFILE"
|
|
112
269
|
fi
|
|
@@ -31,16 +31,51 @@ if [[ "${AGENTVIBES_PARTY_MODE:-false}" == "true" ]]; then
|
|
|
31
31
|
CONFIG_KEY="_party_mode"
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Per-agent profile overrides (from bmad-voice-map.json via bmad-speak.sh)
|
|
36
|
+
# If AGENTVIBES_AGENT_PROFILE is set and the file exists, apply reverb/personality/music
|
|
37
|
+
# overrides by temporarily setting effects-manager config for this agent
|
|
38
|
+
AGENT_PROFILE="${AGENTVIBES_AGENT_PROFILE:-}"
|
|
39
|
+
|
|
40
|
+
if [[ -n "$AGENT_PROFILE" ]] && [[ -f "$AGENT_PROFILE" ]]; then
|
|
41
|
+
# Read profile fields using node (reliable JSON parsing)
|
|
42
|
+
# SECURITY: Pass values via env vars to prevent shell injection
|
|
43
|
+
_PROFILE_REVERB=$(_APFILE="$AGENT_PROFILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._APFILE,'utf8'));process.stdout.write(p.reverbPreset||'')}catch{}" 2>/dev/null || true)
|
|
44
|
+
_PROFILE_MUSIC_TRACK=$(_APFILE="$AGENT_PROFILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._APFILE,'utf8'));process.stdout.write(p.backgroundMusic?.track||'')}catch{}" 2>/dev/null || true)
|
|
45
|
+
_PROFILE_MUSIC_VOL=$(_APFILE="$AGENT_PROFILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._APFILE,'utf8'));process.stdout.write(String(p.backgroundMusic?.volume||''))}catch{}" 2>/dev/null || true)
|
|
46
|
+
|
|
47
|
+
# Apply per-agent reverb via effects-manager (scoped to this agent's config key)
|
|
48
|
+
if [[ -n "$_PROFILE_REVERB" ]] && [[ -f "$SCRIPT_DIR/effects-manager.sh" ]]; then
|
|
49
|
+
bash "$SCRIPT_DIR/effects-manager.sh" set-reverb "$_PROFILE_REVERB" "$CONFIG_KEY" 2>/dev/null || true
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Override background music track/volume for this invocation via env vars
|
|
53
|
+
if [[ -n "$_PROFILE_MUSIC_TRACK" ]]; then
|
|
54
|
+
export AGENTVIBES_BG_TRACK="$_PROFILE_MUSIC_TRACK"
|
|
55
|
+
fi
|
|
56
|
+
if [[ -n "$_PROFILE_MUSIC_VOL" ]]; then
|
|
57
|
+
export AGENTVIBES_BG_VOLUME="$_PROFILE_MUSIC_VOL"
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
34
61
|
# Step 1: Generate TTS WITHOUT playback
|
|
35
62
|
export AGENTVIBES_NO_PLAYBACK=true
|
|
36
|
-
|
|
63
|
+
export AGENTVIBES_WAV_OUTPATH="${XDG_RUNTIME_DIR:-/tmp}/agentvibes-last-wav-$$.txt"
|
|
37
64
|
|
|
38
|
-
#
|
|
39
|
-
|
|
65
|
+
# Cleanup temp outpath file on exit
|
|
66
|
+
trap 'rm -f "$AGENTVIBES_WAV_OUTPATH"' EXIT
|
|
67
|
+
"$SCRIPT_DIR/play-tts.sh" "$TEXT" "$VOICE_OVERRIDE"
|
|
68
|
+
|
|
69
|
+
# Read the generated file path (written by play-tts-piper.sh via AGENTVIBES_WAV_OUTPATH)
|
|
70
|
+
GENERATED_FILE=""
|
|
71
|
+
if [[ -f "$AGENTVIBES_WAV_OUTPATH" ]]; then
|
|
72
|
+
GENERATED_FILE=$(cat "$AGENTVIBES_WAV_OUTPATH")
|
|
73
|
+
rm -f "$AGENTVIBES_WAV_OUTPATH"
|
|
74
|
+
fi
|
|
75
|
+
unset AGENTVIBES_WAV_OUTPATH
|
|
40
76
|
|
|
41
77
|
if [[ -z "$GENERATED_FILE" ]] || [[ ! -f "$GENERATED_FILE" ]]; then
|
|
42
|
-
echo "Error:
|
|
43
|
-
echo "$OUTPUT" >&2
|
|
78
|
+
echo "Error: Could not find generated audio file" >&2
|
|
44
79
|
exit 1
|
|
45
80
|
fi
|
|
46
81
|
|
|
@@ -48,6 +48,17 @@ fi
|
|
|
48
48
|
TEXT="$1"
|
|
49
49
|
VOICE_OVERRIDE="$2" # Optional: voice name (e.g., "Samantha", "Daniel")
|
|
50
50
|
|
|
51
|
+
# Strip emojis, asterisks, and markdown formatting
|
|
52
|
+
TEXT=$(printf '%s' "$TEXT" | perl -CSD -pe '
|
|
53
|
+
s/[\x{1F300}-\x{1F9FF}]//g;
|
|
54
|
+
s/[\x{2600}-\x{27BF}]//g;
|
|
55
|
+
s/[\x{FE00}-\x{FE0F}]//g;
|
|
56
|
+
s/[\x{200D}]//g;
|
|
57
|
+
s/[\x{2500}-\x{257F}]//g;
|
|
58
|
+
s/[\x{2580}-\x{259F}]//g;
|
|
59
|
+
s/\*+//g; s/#+\s*//g; s/`//g; s/~+//g; s/^\s*[-]\s*//g;
|
|
60
|
+
')
|
|
61
|
+
|
|
51
62
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
52
63
|
|
|
53
64
|
# Source audio cache utilities
|
|
@@ -168,10 +179,9 @@ fi
|
|
|
168
179
|
|
|
169
180
|
mkdir -p "$AUDIO_DIR"
|
|
170
181
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
FINAL_FILE="$AUDIO_DIR/tts-padded-${TIMESTAMP}.wav"
|
|
182
|
+
# SECURITY: Use mktemp for unpredictable filenames (#130)
|
|
183
|
+
TEMP_FILE=$(mktemp "$AUDIO_DIR/tts-XXXXXX.aiff")
|
|
184
|
+
FINAL_FILE=$(mktemp "$AUDIO_DIR/tts-padded-XXXXXX.wav")
|
|
175
185
|
|
|
176
186
|
# @function get_speech_rate
|
|
177
187
|
# @intent Determine speech rate for synthesis
|
|
@@ -226,14 +236,27 @@ if command -v ffmpeg &> /dev/null; then
|
|
|
226
236
|
fi
|
|
227
237
|
else
|
|
228
238
|
# No ffmpeg - use AIFF directly (rename for consistency)
|
|
229
|
-
FINAL_FILE
|
|
239
|
+
FINAL_FILE=$(mktemp "$AUDIO_DIR/tts-padded-XXXXXX.aiff")
|
|
230
240
|
mv "$TEMP_FILE" "$FINAL_FILE"
|
|
231
241
|
TEMP_FILE="$FINAL_FILE"
|
|
232
242
|
fi
|
|
233
243
|
|
|
234
244
|
# @function play_audio
|
|
235
245
|
# @intent Play generated audio - via PulseAudio tunnel for SSH, afplay for local
|
|
236
|
-
|
|
246
|
+
# SECURITY: Use user-isolated lock directory (#129)
|
|
247
|
+
_LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp/agentvibes-$(id -u)}"
|
|
248
|
+
mkdir -p "$_LOCK_DIR"
|
|
249
|
+
chmod 700 "$_LOCK_DIR"
|
|
250
|
+
LOCK_FILE="$_LOCK_DIR/agentvibes-audio.lock"
|
|
251
|
+
|
|
252
|
+
# Auto-remove stale lock files (older than 30 seconds)
|
|
253
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
254
|
+
_lock_mtime=$(stat -f %m "$LOCK_FILE" 2>/dev/null || echo 0)
|
|
255
|
+
_lock_age=$(( $(date +%s) - _lock_mtime ))
|
|
256
|
+
if [[ $_lock_age -gt 30 ]]; then
|
|
257
|
+
rm -f "$LOCK_FILE"
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
237
260
|
|
|
238
261
|
# Wait for previous audio to finish (max 30 seconds)
|
|
239
262
|
for i in {1..60}; do
|