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.
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +290 -290
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/play-tts-enhanced.sh +105 -105
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +14 -5
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/voice-manager.sh +552 -548
- package/.claude/hooks-windows/play-tts.ps1 +2 -2
- package/README.md +11 -2
- package/RELEASE_NOTES.md +38 -0
- package/bin/mcp-server.sh +206 -206
- package/mcp-server/server.py +35 -6
- package/package.json +1 -1
- package/src/console/tabs/setup-tab.js +59 -23
- package/src/installer.js +79 -213
- 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
|