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