agentvibes 5.4.0 → 5.5.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 +12 -0
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +6 -2
- package/.claude/hooks/play-tts-piper.sh +2 -2
- package/.claude/hooks/play-tts.sh +22 -1
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +178 -164
- package/.claude/hooks-windows/play-tts.ps1 +208 -41
- package/README.md +14 -9
- package/RELEASE_NOTES.md +40 -0
- package/bin/agentvibes.js +28 -0
- package/mcp-server/server.py +17 -1
- package/package.json +1 -1
- package/src/console/tabs/music-tab.js +5 -2
- package/src/console/tabs/voices-tab.js +71 -37
- package/src/installer.js +10 -0
- package/src/services/llm-provider-service.js +1 -1
|
@@ -50,3 +50,15 @@ _party_mode|compand 0.3,1 6:-70,-60,-20|agent_vibes_dark_chill_step_loop.mp3|0.4
|
|
|
50
50
|
|||
|
|
51
51
|
# Default (no agent specified) - clean with Bachata background|||
|
|
52
52
|
default|reverb 20 50 50|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
53
|
+
|||
|
|
54
|
+
# Per-LLM routing rows — looked up by play-tts.sh / play-tts.ps1 when -llm is passed|||
|
|
55
|
+
# Format: llm:<name>|REVERB_PRESET|BACKGROUND_FILE|BACKGROUND_VOLUME|VOICE|PRETEXT|ENGINE|||
|
|
56
|
+
|||
|
|
57
|
+
# Claude Code (claude-code CLI via CLAUDECODE=1 env or AGENTVIBES_LLM=claude-code)|||
|
|
58
|
+
llm:claude-code|off||||Agent Vibes Here|piper
|
|
59
|
+
|||
|
|
60
|
+
# GitHub Copilot Chat (VS Code MCP or AGENTVIBES_LLM=copilot)|||
|
|
61
|
+
llm:copilot|off||||Agent Vibes Here|piper
|
|
62
|
+
|||
|
|
63
|
+
# OpenAI Codex CLI (AGENTVIBES_LLM=codex)|||
|
|
64
|
+
llm:codex|off||||Agent Vibes Here|piper
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
true
|
|
@@ -24,4 +24,4 @@ agent_vibes_arabic_v2_loop.mp3:.00000000000000000006132724
|
|
|
24
24
|
agent_vibes_chillwave_v2_loop.mp3:14.628390
|
|
25
25
|
agent_vibes_bachata_v1_loop.mp3:.00000000000000000005344000
|
|
26
26
|
agent_vibes_goa_trance_v2_loop.mp3:.00000000000000000002499918
|
|
27
|
-
agentvibes_soft_flamenco_loop.mp3:10.
|
|
27
|
+
agentvibes_soft_flamenco_loop.mp3:10.584014
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20260427
|
|
File without changes
|
|
@@ -50,11 +50,15 @@ if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
|
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# ---------------------------------------------------------------------------
|
|
53
|
-
# Per-agent profile reader — reads from
|
|
53
|
+
# Per-agent profile reader — reads from project .agentvibes/bmad-voice-map.json (falls back to global)
|
|
54
54
|
# Uses node for reliable JSON parsing (jq may not be installed)
|
|
55
55
|
# Returns empty string if field not found or file missing
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
if [[ -f "$PROJECT_ROOT/.agentvibes/bmad-voice-map.json" ]]; then
|
|
58
|
+
VOICE_MAP_FILE="$PROJECT_ROOT/.agentvibes/bmad-voice-map.json"
|
|
59
|
+
else
|
|
60
|
+
VOICE_MAP_FILE="$HOME/.agentvibes/bmad-voice-map.json"
|
|
61
|
+
fi
|
|
58
62
|
|
|
59
63
|
# Read a field from the per-agent profile in bmad-voice-map.json
|
|
60
64
|
# Usage: read_agent_profile <agent_id> <field>
|
|
@@ -551,8 +551,8 @@ if [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]] && [[ "${AGENTVIBES_NO_PLAYB
|
|
|
551
551
|
termux-media-player play "$TEMP_FILE" >/dev/null 2>&1 &
|
|
552
552
|
PLAYER_PID=$!
|
|
553
553
|
else
|
|
554
|
-
# Linux/WSL:
|
|
555
|
-
(paplay "$TEMP_FILE" || mpv "$TEMP_FILE" || aplay "$TEMP_FILE") >/dev/null 2>&1 &
|
|
554
|
+
# Linux/WSL: paplay with 500ms latency buffer prevents choppiness over RDP/network audio
|
|
555
|
+
(paplay --latency-msec=500 "$TEMP_FILE" || mpv "$TEMP_FILE" || aplay -B 2000000 "$TEMP_FILE") >/dev/null 2>&1 &
|
|
556
556
|
PLAYER_PID=$!
|
|
557
557
|
fi
|
|
558
558
|
fi
|
|
@@ -103,8 +103,29 @@ elif [[ -f "$GLOBAL_MUTE_FILE" ]]; then
|
|
|
103
103
|
exit 0
|
|
104
104
|
fi
|
|
105
105
|
|
|
106
|
+
# Parse named flags (e.g. --llm) before positional arguments.
|
|
107
|
+
# This allows callers to pass: play-tts.sh --llm claude-code "text to speak"
|
|
108
|
+
# Named args are extracted; remaining positional args are shifted into $1/$2/$3.
|
|
109
|
+
LLM_PROVIDER="${LLM_PROVIDER:-}"
|
|
110
|
+
_POSITIONAL_ARGS=()
|
|
111
|
+
while [[ $# -gt 0 ]]; do
|
|
112
|
+
case "$1" in
|
|
113
|
+
--llm)
|
|
114
|
+
LLM_PROVIDER="${2:-}"
|
|
115
|
+
shift 2
|
|
116
|
+
;;
|
|
117
|
+
*)
|
|
118
|
+
_POSITIONAL_ARGS+=("$1")
|
|
119
|
+
shift
|
|
120
|
+
;;
|
|
121
|
+
esac
|
|
122
|
+
done
|
|
123
|
+
set -- "${_POSITIONAL_ARGS[@]+"${_POSITIONAL_ARGS[@]}"}"
|
|
124
|
+
unset _POSITIONAL_ARGS
|
|
125
|
+
|
|
106
126
|
TEXT="${1:-}"
|
|
107
127
|
VOICE_OVERRIDE="${2:-}" # Optional: voice name or ID
|
|
128
|
+
AGENT_PROFILE_FILE="${3:-}" # Optional: path to agent profile file
|
|
108
129
|
|
|
109
130
|
# Security: Validate inputs
|
|
110
131
|
if [[ -z "$TEXT" ]]; then
|
|
@@ -397,7 +418,7 @@ fi
|
|
|
397
418
|
# Normal single-language mode - route to appropriate provider implementation
|
|
398
419
|
case "$ACTIVE_PROVIDER" in
|
|
399
420
|
piper)
|
|
400
|
-
exec bash "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" "$AGENT_PROFILE_FILE"
|
|
421
|
+
exec bash "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" "${AGENT_PROFILE_FILE:-}"
|
|
401
422
|
;;
|
|
402
423
|
soprano)
|
|
403
424
|
exec bash "$SCRIPT_DIR/play-tts-soprano.sh" "$TEXT" "$VOICE_OVERRIDE"
|
|
@@ -1,164 +1,178 @@
|
|
|
1
|
-
#
|
|
2
|
-
# File: .claude/hooks-windows/play-tts-windows-piper.ps1
|
|
3
|
-
#
|
|
4
|
-
# AgentVibes - Windows Piper TTS Provider
|
|
5
|
-
# High-quality neural TTS using Piper.exe
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
param(
|
|
9
|
-
[Parameter(Mandatory = $true)]
|
|
10
|
-
[string]$Text,
|
|
11
|
-
|
|
12
|
-
[Parameter(Mandatory = $false)]
|
|
13
|
-
[string]$VoiceOverride
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
# Configuration paths
|
|
17
|
-
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
18
|
-
$ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
|
|
19
|
-
|
|
20
|
-
if (Test-Path $ProjectClaudeDir) {
|
|
21
|
-
$ClaudeDir = $ProjectClaudeDir
|
|
22
|
-
} else {
|
|
23
|
-
$ClaudeDir = "$env:USERPROFILE\.claude"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
# Audio cache and voice config use project-local .claude
|
|
27
|
-
$AudioDir = "$ClaudeDir\audio"
|
|
28
|
-
$VoiceFile = "$ClaudeDir\tts-voice-piper.txt"
|
|
29
|
-
|
|
30
|
-
# Voices and Piper binary are global (shared across projects, ~100MB+)
|
|
31
|
-
$UserClaudeDir = "$env:USERPROFILE\.claude"
|
|
32
|
-
$VoicesDir = "$UserClaudeDir\piper-voices"
|
|
33
|
-
$PiperExe = "$env:LOCALAPPDATA\Programs\Piper\piper.exe"
|
|
34
|
-
|
|
35
|
-
# Ensure directories exist
|
|
36
|
-
foreach ($dir in @($AudioDir, $VoicesDir)) {
|
|
37
|
-
if (-not (Test-Path $dir)) {
|
|
38
|
-
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
# Check if Piper is installed
|
|
43
|
-
if (-not (Test-Path $PiperExe)) {
|
|
44
|
-
Write-Host "[ERROR] Piper not found at: $PiperExe" -ForegroundColor Red
|
|
45
|
-
Write-Host "Run: .\setup-windows.ps1 to install Piper" -ForegroundColor Yellow
|
|
46
|
-
exit 1
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
# Determine voice to use
|
|
50
|
-
$VoiceName = ""
|
|
51
|
-
|
|
52
|
-
if ($VoiceOverride) {
|
|
53
|
-
$VoiceName = $VoiceOverride
|
|
54
|
-
}
|
|
55
|
-
elseif (Test-Path $VoiceFile) {
|
|
56
|
-
$VoiceName = (Get-Content $VoiceFile -Raw).Trim()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# Default voice if not specified
|
|
60
|
-
if (-not $VoiceName) {
|
|
61
|
-
$VoiceName = "en_US-ryan-high"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Security: Validate voice name to prevent path traversal
|
|
65
|
-
# Only allow alphanumeric, underscore, hyphen, and period
|
|
66
|
-
if ($VoiceName -notmatch '^[a-zA-Z0-9_\-\.]+$') {
|
|
67
|
-
Write-Host "[ERROR] Invalid voice name: $VoiceName" -ForegroundColor Red
|
|
68
|
-
exit 1
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
# Resolve voice model path and validate it stays within VoicesDir
|
|
72
|
-
$VoiceModelFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx")
|
|
73
|
-
$VoiceJsonFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx.json")
|
|
74
|
-
$ResolvedVoicesDir = [System.IO.Path]::GetFullPath($VoicesDir)
|
|
75
|
-
if (-not $VoiceModelFile.StartsWith($ResolvedVoicesDir)) {
|
|
76
|
-
Write-Host "[ERROR] Voice path outside voices directory" -ForegroundColor Red
|
|
77
|
-
exit 1
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
# Check if voice model exists, download if missing
|
|
81
|
-
if (-not (Test-Path $VoiceModelFile)) {
|
|
82
|
-
Write-Host "[DOWNLOAD] Voice model: $VoiceName" -ForegroundColor Yellow
|
|
83
|
-
|
|
84
|
-
# Try to download from Hugging Face
|
|
85
|
-
# Voice name format: {lang}_{region}-{speaker}-{quality}
|
|
86
|
-
# HF path format: {lang}/{lang}_{region}/{speaker}/{quality}/{voicename}.onnx
|
|
87
|
-
try {
|
|
88
|
-
# Parse voice name to build correct HF path
|
|
89
|
-
# e.g. en_US-ryan-high -> en/en_US/ryan/high/en_US-ryan-high.onnx
|
|
90
|
-
if ($VoiceName -match '^([a-z]{2})_([A-Z]{2})-([a-zA-Z0-9_]+)-([a-z]+)$') {
|
|
91
|
-
$Lang = $Matches[1]
|
|
92
|
-
$LangRegion = "$($Matches[1])_$($Matches[2])"
|
|
93
|
-
$Speaker = $Matches[3]
|
|
94
|
-
$Quality = $Matches[4]
|
|
95
|
-
$HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/$Lang/$LangRegion/$Speaker/$Quality"
|
|
96
|
-
} else {
|
|
97
|
-
# Fallback for non-standard voice names
|
|
98
|
-
$HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high"
|
|
99
|
-
}
|
|
100
|
-
$ModelUrl = "$HFBase/$VoiceName.onnx"
|
|
101
|
-
$JsonUrl = "$HFBase/$VoiceName.onnx.json"
|
|
102
|
-
|
|
103
|
-
Write-Host " Downloading model..." -ForegroundColor Cyan
|
|
104
|
-
Invoke-WebRequest -Uri $ModelUrl -OutFile $VoiceModelFile -ErrorAction Stop
|
|
105
|
-
Write-Host " Downloading config..." -ForegroundColor Cyan
|
|
106
|
-
Invoke-WebRequest -Uri $JsonUrl -OutFile $VoiceJsonFile -ErrorAction Stop
|
|
107
|
-
Write-Host "[OK] Voice model downloaded" -ForegroundColor Green
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
Write-Host "[ERROR] Failed to download voice model: $_" -ForegroundColor Red
|
|
111
|
-
Write-Host "Make sure you have internet connection" -ForegroundColor Yellow
|
|
112
|
-
exit 1
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
# Sanitize text for speech - strip shell metacharacters and PS special chars
|
|
117
|
-
$Text = $Text -replace '\\', ' '
|
|
118
|
-
$Text = $Text -replace '[{}<>|`~^$;"''()]', ''
|
|
119
|
-
$Text = $Text -replace '\s+', ' '
|
|
120
|
-
$Text = $Text.Trim()
|
|
121
|
-
|
|
122
|
-
# Create audio file path
|
|
123
|
-
$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss-ffff'
|
|
124
|
-
$AudioFile = "$AudioDir\tts-$Timestamp.wav"
|
|
125
|
-
|
|
126
|
-
# Synthesize with Piper
|
|
127
|
-
try {
|
|
128
|
-
Write-Host "[SYNTH] Synthesizing with Piper..." -ForegroundColor Cyan
|
|
129
|
-
|
|
130
|
-
# Run Piper with text input
|
|
131
|
-
$Text | & $PiperExe `
|
|
132
|
-
--model $VoiceModelFile `
|
|
133
|
-
--output-file $AudioFile `
|
|
134
|
-
2>$null
|
|
135
|
-
|
|
136
|
-
if (-not (Test-Path $AudioFile)) {
|
|
137
|
-
Write-Host "[ERROR] Piper synthesis failed" -ForegroundColor Red
|
|
138
|
-
exit 1
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
# Display results
|
|
142
|
-
Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
|
|
143
|
-
Write-Host "[VOICE] Voice used: $VoiceName (Piper)" -ForegroundColor Green
|
|
144
|
-
|
|
145
|
-
# Play the audio
|
|
146
|
-
if (-not $env:AGENTVIBES_NO_PLAY) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
1
|
+
#
|
|
2
|
+
# File: .claude/hooks-windows/play-tts-windows-piper.ps1
|
|
3
|
+
#
|
|
4
|
+
# AgentVibes - Windows Piper TTS Provider
|
|
5
|
+
# High-quality neural TTS using Piper.exe
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
param(
|
|
9
|
+
[Parameter(Mandatory = $true)]
|
|
10
|
+
[string]$Text,
|
|
11
|
+
|
|
12
|
+
[Parameter(Mandatory = $false)]
|
|
13
|
+
[string]$VoiceOverride
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Configuration paths
|
|
17
|
+
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
18
|
+
$ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
|
|
19
|
+
|
|
20
|
+
if (Test-Path $ProjectClaudeDir) {
|
|
21
|
+
$ClaudeDir = $ProjectClaudeDir
|
|
22
|
+
} else {
|
|
23
|
+
$ClaudeDir = "$env:USERPROFILE\.claude"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Audio cache and voice config use project-local .claude
|
|
27
|
+
$AudioDir = "$ClaudeDir\audio"
|
|
28
|
+
$VoiceFile = "$ClaudeDir\tts-voice-piper.txt"
|
|
29
|
+
|
|
30
|
+
# Voices and Piper binary are global (shared across projects, ~100MB+)
|
|
31
|
+
$UserClaudeDir = "$env:USERPROFILE\.claude"
|
|
32
|
+
$VoicesDir = "$UserClaudeDir\piper-voices"
|
|
33
|
+
$PiperExe = "$env:LOCALAPPDATA\Programs\Piper\piper.exe"
|
|
34
|
+
|
|
35
|
+
# Ensure directories exist
|
|
36
|
+
foreach ($dir in @($AudioDir, $VoicesDir)) {
|
|
37
|
+
if (-not (Test-Path $dir)) {
|
|
38
|
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Check if Piper is installed
|
|
43
|
+
if (-not (Test-Path $PiperExe)) {
|
|
44
|
+
Write-Host "[ERROR] Piper not found at: $PiperExe" -ForegroundColor Red
|
|
45
|
+
Write-Host "Run: .\setup-windows.ps1 to install Piper" -ForegroundColor Yellow
|
|
46
|
+
exit 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Determine voice to use
|
|
50
|
+
$VoiceName = ""
|
|
51
|
+
|
|
52
|
+
if ($VoiceOverride) {
|
|
53
|
+
$VoiceName = $VoiceOverride
|
|
54
|
+
}
|
|
55
|
+
elseif (Test-Path $VoiceFile) {
|
|
56
|
+
$VoiceName = (Get-Content $VoiceFile -Raw).Trim()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Default voice if not specified
|
|
60
|
+
if (-not $VoiceName) {
|
|
61
|
+
$VoiceName = "en_US-ryan-high"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Security: Validate voice name to prevent path traversal
|
|
65
|
+
# Only allow alphanumeric, underscore, hyphen, and period
|
|
66
|
+
if ($VoiceName -notmatch '^[a-zA-Z0-9_\-\.]+$') {
|
|
67
|
+
Write-Host "[ERROR] Invalid voice name: $VoiceName" -ForegroundColor Red
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Resolve voice model path and validate it stays within VoicesDir
|
|
72
|
+
$VoiceModelFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx")
|
|
73
|
+
$VoiceJsonFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx.json")
|
|
74
|
+
$ResolvedVoicesDir = [System.IO.Path]::GetFullPath($VoicesDir)
|
|
75
|
+
if (-not $VoiceModelFile.StartsWith($ResolvedVoicesDir)) {
|
|
76
|
+
Write-Host "[ERROR] Voice path outside voices directory" -ForegroundColor Red
|
|
77
|
+
exit 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Check if voice model exists, download if missing
|
|
81
|
+
if (-not (Test-Path $VoiceModelFile)) {
|
|
82
|
+
Write-Host "[DOWNLOAD] Voice model: $VoiceName" -ForegroundColor Yellow
|
|
83
|
+
|
|
84
|
+
# Try to download from Hugging Face
|
|
85
|
+
# Voice name format: {lang}_{region}-{speaker}-{quality}
|
|
86
|
+
# HF path format: {lang}/{lang}_{region}/{speaker}/{quality}/{voicename}.onnx
|
|
87
|
+
try {
|
|
88
|
+
# Parse voice name to build correct HF path
|
|
89
|
+
# e.g. en_US-ryan-high -> en/en_US/ryan/high/en_US-ryan-high.onnx
|
|
90
|
+
if ($VoiceName -match '^([a-z]{2})_([A-Z]{2})-([a-zA-Z0-9_]+)-([a-z]+)$') {
|
|
91
|
+
$Lang = $Matches[1]
|
|
92
|
+
$LangRegion = "$($Matches[1])_$($Matches[2])"
|
|
93
|
+
$Speaker = $Matches[3]
|
|
94
|
+
$Quality = $Matches[4]
|
|
95
|
+
$HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/$Lang/$LangRegion/$Speaker/$Quality"
|
|
96
|
+
} else {
|
|
97
|
+
# Fallback for non-standard voice names
|
|
98
|
+
$HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high"
|
|
99
|
+
}
|
|
100
|
+
$ModelUrl = "$HFBase/$VoiceName.onnx"
|
|
101
|
+
$JsonUrl = "$HFBase/$VoiceName.onnx.json"
|
|
102
|
+
|
|
103
|
+
Write-Host " Downloading model..." -ForegroundColor Cyan
|
|
104
|
+
Invoke-WebRequest -Uri $ModelUrl -OutFile $VoiceModelFile -ErrorAction Stop
|
|
105
|
+
Write-Host " Downloading config..." -ForegroundColor Cyan
|
|
106
|
+
Invoke-WebRequest -Uri $JsonUrl -OutFile $VoiceJsonFile -ErrorAction Stop
|
|
107
|
+
Write-Host "[OK] Voice model downloaded" -ForegroundColor Green
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
Write-Host "[ERROR] Failed to download voice model: $_" -ForegroundColor Red
|
|
111
|
+
Write-Host "Make sure you have internet connection" -ForegroundColor Yellow
|
|
112
|
+
exit 1
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Sanitize text for speech - strip shell metacharacters and PS special chars
|
|
117
|
+
$Text = $Text -replace '\\', ' '
|
|
118
|
+
$Text = $Text -replace '[{}<>|`~^$;"''()]', ''
|
|
119
|
+
$Text = $Text -replace '\s+', ' '
|
|
120
|
+
$Text = $Text.Trim()
|
|
121
|
+
|
|
122
|
+
# Create audio file path
|
|
123
|
+
$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss-ffff'
|
|
124
|
+
$AudioFile = "$AudioDir\tts-$Timestamp.wav"
|
|
125
|
+
|
|
126
|
+
# Synthesize with Piper
|
|
127
|
+
try {
|
|
128
|
+
Write-Host "[SYNTH] Synthesizing with Piper..." -ForegroundColor Cyan
|
|
129
|
+
|
|
130
|
+
# Run Piper with text input
|
|
131
|
+
$Text | & $PiperExe `
|
|
132
|
+
--model $VoiceModelFile `
|
|
133
|
+
--output-file $AudioFile `
|
|
134
|
+
2>$null
|
|
135
|
+
|
|
136
|
+
if (-not (Test-Path $AudioFile)) {
|
|
137
|
+
Write-Host "[ERROR] Piper synthesis failed" -ForegroundColor Red
|
|
138
|
+
exit 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Display results
|
|
142
|
+
Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
|
|
143
|
+
Write-Host "[VOICE] Voice used: $VoiceName (Piper)" -ForegroundColor Green
|
|
144
|
+
|
|
145
|
+
# Play the audio (skip if AGENTVIBES_NO_PLAY is set)
|
|
146
|
+
if (-not $env:AGENTVIBES_NO_PLAY) {
|
|
147
|
+
# Prefer ffplay: handles 22050 Hz → 48000 Hz resampling cleanly (SoundPlayer uses
|
|
148
|
+
# WinMM's low-quality resampler which produces choppy audio at non-native rates).
|
|
149
|
+
$ffplayPath = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
|
|
150
|
+
if (-not $ffplayPath) {
|
|
151
|
+
# SSH/watcher sessions may have a minimal PATH — refresh from registry
|
|
152
|
+
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
|
|
153
|
+
[System.Environment]::GetEnvironmentVariable("Path","User")
|
|
154
|
+
$ffplayPath = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
|
|
155
|
+
}
|
|
156
|
+
if ($ffplayPath) {
|
|
157
|
+
& $ffplayPath -autoexit -nodisp -loglevel quiet $AudioFile 2>$null
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
$player = $null
|
|
161
|
+
try {
|
|
162
|
+
$player = New-Object System.Media.SoundPlayer $AudioFile
|
|
163
|
+
$player.PlaySync()
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
Write-Host "[WARNING] Could not play audio (SoundPlayer unavailable)" -ForegroundColor Yellow
|
|
167
|
+
Write-Host "Audio saved to: $AudioFile" -ForegroundColor Gray
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if ($player) { $player.Dispose() }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
Write-Host "[ERROR] Error running Piper: $_" -ForegroundColor Red
|
|
177
|
+
exit 1
|
|
178
|
+
}
|
|
@@ -10,7 +10,13 @@ param(
|
|
|
10
10
|
[string]$Text,
|
|
11
11
|
|
|
12
12
|
[Parameter(Mandatory = $false, Position = 1)]
|
|
13
|
-
[string]$VoiceOverride
|
|
13
|
+
[string]$VoiceOverride,
|
|
14
|
+
|
|
15
|
+
# LLM identity for per-LLM audio routing (e.g. "claude-code", "copilot", "codex").
|
|
16
|
+
# When provided, the router looks up an `llm:<name>` row in audio-effects.cfg
|
|
17
|
+
# to apply LLM-specific voice, pretext, reverb, and engine settings.
|
|
18
|
+
[Parameter(Mandatory = $false)]
|
|
19
|
+
[string]$llm = ""
|
|
14
20
|
)
|
|
15
21
|
|
|
16
22
|
# Configuration paths
|
|
@@ -97,6 +103,201 @@ if ($BgEnabled -or $HasReverb) {
|
|
|
97
103
|
} catch {}
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
# ===========================================================================
|
|
107
|
+
# Per-LLM Audio Routing
|
|
108
|
+
# ===========================================================================
|
|
109
|
+
# When mcp-server/server.py invokes play-tts.ps1 on Windows it passes the
|
|
110
|
+
# -llm flag with the active LLM identity (e.g. "claude-code", "copilot",
|
|
111
|
+
# "codex"). The router reads audio-effects.cfg and looks up the row whose
|
|
112
|
+
# key is `llm:<name>`, allowing each LLM to have its own voice, pretext,
|
|
113
|
+
# reverb, and engine without requiring global settings to be reconfigured.
|
|
114
|
+
#
|
|
115
|
+
# Expected audio-effects.cfg row format (pipe-delimited):
|
|
116
|
+
# llm:<name>|REVERB_PRESET|BACKGROUND_FILE|BACKGROUND_VOLUME|VOICE|PRETEXT|ENGINE
|
|
117
|
+
#
|
|
118
|
+
# Column descriptions:
|
|
119
|
+
# 1. Key - Must start with "llm:" followed by the LLM name
|
|
120
|
+
# 2. REVERB_PRESET - One of: off, light, medium, heavy, cathedral (or blank)
|
|
121
|
+
# 3. BACKGROUND_FILE - Filename relative to .claude/audio/tracks/ (or blank)
|
|
122
|
+
# 4. BACKGROUND_VOLUME - Float 0.0-1.0 (or blank for default 0.25)
|
|
123
|
+
# 5. VOICE - Provider voice name to use (or blank for global default)
|
|
124
|
+
# 6. PRETEXT - Text prepended to all TTS utterances (or blank)
|
|
125
|
+
# 7. ENGINE - Windows engine: windows-sapi, windows-piper, soprano (or blank)
|
|
126
|
+
#
|
|
127
|
+
# Example rows:
|
|
128
|
+
# llm:claude-code|off|||en_US-amy-medium|Agent Vibes Here|windows-piper
|
|
129
|
+
# llm:copilot|light|||en_US-ryan-low||windows-sapi
|
|
130
|
+
# llm:codex|off||||Code complete|windows-piper
|
|
131
|
+
# llm:default|off|||||
|
|
132
|
+
#
|
|
133
|
+
# The "default" key is always looked up when no explicit -llm flag is
|
|
134
|
+
# provided. Configure it via Setup → Default → Configure in the TUI to
|
|
135
|
+
# apply consistent audio settings across all LLM sessions.
|
|
136
|
+
#
|
|
137
|
+
# Security: The -llm value is validated against an allowlist regex so that
|
|
138
|
+
# injected values like "-rf" or path-traversal strings are rejected before
|
|
139
|
+
# they can appear in lookup keys, environment variables, or file paths.
|
|
140
|
+
|
|
141
|
+
# --- Validate -llm parameter format ------------------------------------------
|
|
142
|
+
if ($llm -and $llm -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_-]*$') {
|
|
143
|
+
Write-Host "[WARNING] play-tts.ps1: Invalid -llm value '$llm' ignored" `
|
|
144
|
+
"(must match ^[a-zA-Z0-9][a-zA-Z0-9_-]*`$)" -ForegroundColor Yellow
|
|
145
|
+
$llm = ""
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# --- Default fallback --------------------------------------------------------
|
|
149
|
+
# An empty $llm routes through the "default" pseudo-LLM. Users who configure
|
|
150
|
+
# an `llm:default` row in audio-effects.cfg get consistent audio settings for
|
|
151
|
+
# every LLM that doesn't pass its own -llm flag — a convenient global override
|
|
152
|
+
# that doesn't require per-LLM configuration.
|
|
153
|
+
if (-not $llm) {
|
|
154
|
+
$llm = "default"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# --- Export LLM key for child scripts ----------------------------------------
|
|
158
|
+
# Provider scripts (play-tts-windows-*.ps1) and any other downstream tooling
|
|
159
|
+
# can inspect AGENTVIBES_LLM_KEY to identify which LLM is currently speaking.
|
|
160
|
+
# This mirrors the `export AGENTVIBES_LLM_KEY="llm:${LLM_PROVIDER}"` line in
|
|
161
|
+
# the POSIX play-tts.sh so the cross-platform contract is symmetric.
|
|
162
|
+
$env:AGENTVIBES_LLM_KEY = "llm:$llm"
|
|
163
|
+
|
|
164
|
+
# --- Lookup per-LLM config in audio-effects.cfg ------------------------------
|
|
165
|
+
# Scan project config first, then user-profile config. Stop at first match.
|
|
166
|
+
# Variables are intentionally prefixed with _ to distinguish LLM-local state
|
|
167
|
+
# from the global session state set earlier in this script.
|
|
168
|
+
$_LlmVoice = ""
|
|
169
|
+
$_LlmPretext = ""
|
|
170
|
+
$_LlmReverb = ""
|
|
171
|
+
$_LlmEngine = ""
|
|
172
|
+
$_LlmBgFile = ""
|
|
173
|
+
$_LlmBgVol = ""
|
|
174
|
+
$_LlmKey = "llm:$llm"
|
|
175
|
+
|
|
176
|
+
$_AudioEffectsCfgPaths = @(
|
|
177
|
+
(Join-Path $ClaudeDir "config\audio-effects.cfg"),
|
|
178
|
+
(Join-Path $env:USERPROFILE ".claude\config\audio-effects.cfg")
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
:llmCfgSearch foreach ($_cfgFile in $_AudioEffectsCfgPaths) {
|
|
182
|
+
if ((-not $_LlmVoice) -and (-not $_LlmPretext) -and (Test-Path $_cfgFile)) {
|
|
183
|
+
$cfgContent = Get-Content $_cfgFile -ErrorAction SilentlyContinue
|
|
184
|
+
if ($null -ne $cfgContent) {
|
|
185
|
+
foreach ($_cfgLine in $cfgContent) {
|
|
186
|
+
# Skip blank lines and comment / separator lines
|
|
187
|
+
$stripped = $_cfgLine.Trim()
|
|
188
|
+
if ($stripped.Length -eq 0 -or $stripped.StartsWith('#')) { continue }
|
|
189
|
+
|
|
190
|
+
# Split on pipe; expect at least the key column
|
|
191
|
+
$_cols = $_cfgLine -split '\|'
|
|
192
|
+
if ($_cols.Count -ge 1 -and $_cols[0].Trim() -eq $_LlmKey) {
|
|
193
|
+
# Unpack columns defensively — missing columns stay empty
|
|
194
|
+
if ($_cols.Count -ge 2) { $_LlmReverb = $_cols[1].Trim() }
|
|
195
|
+
if ($_cols.Count -ge 3) { $_LlmBgFile = $_cols[2].Trim() }
|
|
196
|
+
if ($_cols.Count -ge 4) { $_LlmBgVol = $_cols[3].Trim() }
|
|
197
|
+
if ($_cols.Count -ge 5) { $_LlmVoice = $_cols[4].Trim() }
|
|
198
|
+
if ($_cols.Count -ge 6) { $_LlmPretext = $_cols[5].Trim() }
|
|
199
|
+
if ($_cols.Count -ge 7) { $_LlmEngine = $_cols[6].Trim() }
|
|
200
|
+
break llmCfgSearch
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# --- Voice priority order (highest wins) -------------------------------------
|
|
208
|
+
# 1. Explicit -VoiceOverride parameter (caller always wins)
|
|
209
|
+
# 2. LLM-specific voice from audio-effects.cfg llm:<key> row
|
|
210
|
+
# 3. BMAD agent voice from bmad-voice-map.json (resolved in provider scripts)
|
|
211
|
+
# 4. Global active voice from tts-provider.txt / active-voice.txt
|
|
212
|
+
|
|
213
|
+
# Apply LLM-specific voice only when no explicit -VoiceOverride was passed
|
|
214
|
+
if ($_LlmVoice -and -not $VoiceOverride) {
|
|
215
|
+
$VoiceOverride = $_LlmVoice
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# --- Apply LLM-specific pretext ----------------------------------------------
|
|
219
|
+
# Prepend the configured pretext (e.g. "Agent Vibes Here") to the speech
|
|
220
|
+
# text. Guard against double-prefixing on re-entrant or looped calls by
|
|
221
|
+
# checking whether the text already starts with the pretext string.
|
|
222
|
+
if ($_LlmPretext -and -not $Text.StartsWith($_LlmPretext)) {
|
|
223
|
+
$Text = "$_LlmPretext, $Text"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# --- Reverb override from per-LLM config -------------------------------------
|
|
227
|
+
# If the llm:<key> row specifies a reverb preset, override the file-based
|
|
228
|
+
# $ReverbLevel that was set from reverb-level.txt earlier. The allowlist
|
|
229
|
+
# check is repeated here so a malformed config row can't inject arbitrary
|
|
230
|
+
# strings into the ffmpeg filter chain.
|
|
231
|
+
if ($_LlmReverb) {
|
|
232
|
+
$validReverbLevels = @("off", "light", "medium", "heavy", "cathedral")
|
|
233
|
+
if ($validReverbLevels -contains $_LlmReverb) {
|
|
234
|
+
$ReverbLevel = $_LlmReverb
|
|
235
|
+
$HasReverb = $ReverbLevel -ne "off"
|
|
236
|
+
# If the LLM config enables reverb and ffmpeg wasn't found yet, retry
|
|
237
|
+
if ($HasReverb -and -not $HasFfmpeg) {
|
|
238
|
+
try { $null = Get-Command ffmpeg -ErrorAction Stop; $HasFfmpeg = $true } catch {}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# --- Apply LLM-specific engine override --------------------------------------
|
|
244
|
+
# Allowed local Windows engines: windows-sapi, windows-piper, soprano.
|
|
245
|
+
# Transport providers (ssh-remote etc.) are not listed because they forward
|
|
246
|
+
# TTS to a remote host — overriding with a local engine would synthesize on
|
|
247
|
+
# the wrong machine.
|
|
248
|
+
if ($_LlmEngine) {
|
|
249
|
+
$allowedLocalEngines = @("windows-sapi", "windows-piper", "soprano")
|
|
250
|
+
if ($allowedLocalEngines -contains $_LlmEngine) {
|
|
251
|
+
switch ($_LlmEngine) {
|
|
252
|
+
"windows-sapi" { $ProviderScript = "$HooksDir\play-tts-windows-sapi.ps1" }
|
|
253
|
+
"windows-piper" { $ProviderScript = "$HooksDir\play-tts-windows-piper.ps1" }
|
|
254
|
+
"soprano" { $ProviderScript = "$HooksDir\play-tts-soprano.ps1" }
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
Write-Host "[INFO] play-tts.ps1: Unrecognised engine '$_LlmEngine' in audio-effects.cfg — keeping default provider" -ForegroundColor DarkGray
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# --- BMAD Party Mode note ----------------------------------------------------
|
|
262
|
+
# When BMAD party mode is active, multiple agents speak in rapid succession.
|
|
263
|
+
# Each agent's voice is resolved from bmad-voice-map.json inside the provider
|
|
264
|
+
# scripts — that BMAD-level routing is independent of this per-LLM system.
|
|
265
|
+
# The -llm flag is still used to set AGENTVIBES_LLM_KEY and can supply a
|
|
266
|
+
# background music track and reverb preset that stays consistent throughout
|
|
267
|
+
# the entire party mode session regardless of which agent is speaking.
|
|
268
|
+
|
|
269
|
+
# --- Diagnostic output -------------------------------------------------------
|
|
270
|
+
# Set AGENTVIBES_VERBOSE=1 in the shell environment to print routing state.
|
|
271
|
+
if ($env:AGENTVIBES_VERBOSE -eq "1") {
|
|
272
|
+
Write-Host "[DEBUG] play-tts.ps1 LLM routing: llm=$llm | voice=$VoiceOverride | engine=$_LlmEngine | pretext=$_LlmPretext" -ForegroundColor DarkCyan
|
|
273
|
+
Write-Host "[DEBUG] play-tts.ps1 LLM routing: reverb=$ReverbLevel | HasFfmpeg=$HasFfmpeg | BgEnabled=$BgEnabled | script=$ProviderScript" -ForegroundColor DarkCyan
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# ===========================================================================
|
|
277
|
+
# End of Per-LLM Audio Routing
|
|
278
|
+
# ===========================================================================
|
|
279
|
+
|
|
280
|
+
# Helper: play a WAV file preferring ffplay over SoundPlayer.
|
|
281
|
+
# SoundPlayer uses WinMM's low-quality resampler (22050 Hz → 48000 Hz is choppy);
|
|
282
|
+
# ffplay uses libswresample with sinc resampling — no artefacts.
|
|
283
|
+
function Invoke-AudioPlay {
|
|
284
|
+
param([string]$FilePath)
|
|
285
|
+
$fp = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
|
|
286
|
+
if (-not $fp) {
|
|
287
|
+
# Watcher sessions may inherit a minimal PATH — refresh from registry
|
|
288
|
+
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
|
|
289
|
+
[System.Environment]::GetEnvironmentVariable("Path","User")
|
|
290
|
+
$fp = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
|
|
291
|
+
}
|
|
292
|
+
if ($fp) {
|
|
293
|
+
& $fp -autoexit -nodisp -loglevel quiet $FilePath 2>$null
|
|
294
|
+
} else {
|
|
295
|
+
$p = $null
|
|
296
|
+
try { $p = New-Object System.Media.SoundPlayer $FilePath; $p.PlaySync() }
|
|
297
|
+
finally { if ($p) { $p.Dispose() } }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
100
301
|
# If background music or reverb enabled and ffmpeg available, tell provider to skip playback
|
|
101
302
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
102
303
|
$env:AGENTVIBES_NO_PLAY = "1"
|
|
@@ -204,61 +405,27 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
204
405
|
|
|
205
406
|
if ($proc.ExitCode -eq 0 -and (Test-Path $MixedFile) -and (Get-Item $MixedFile).Length -gt 0) {
|
|
206
407
|
# Play the mixed audio
|
|
207
|
-
$player = $null
|
|
208
408
|
try {
|
|
209
|
-
|
|
210
|
-
$player.PlaySync()
|
|
409
|
+
Invoke-AudioPlay $MixedFile
|
|
211
410
|
} catch {
|
|
212
411
|
Write-Host "[WARNING] Mixed playback failed, playing voice only" -ForegroundColor Yellow
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
$player2 = New-Object System.Media.SoundPlayer $voicePath
|
|
216
|
-
$player2.PlaySync()
|
|
217
|
-
} finally {
|
|
218
|
-
if ($player2) { $player2.Dispose() }
|
|
219
|
-
}
|
|
220
|
-
} finally {
|
|
221
|
-
if ($player) { $player.Dispose() }
|
|
412
|
+
Invoke-AudioPlay $voicePath
|
|
222
413
|
}
|
|
223
414
|
} else {
|
|
224
415
|
# Mixing failed, play voice only
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
228
|
-
$player.PlaySync()
|
|
229
|
-
} finally {
|
|
230
|
-
if ($player) { $player.Dispose() }
|
|
231
|
-
}
|
|
416
|
+
Invoke-AudioPlay $voicePath
|
|
232
417
|
}
|
|
233
418
|
} catch {
|
|
234
419
|
# ffmpeg failed, play voice only
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
238
|
-
$player.PlaySync()
|
|
239
|
-
} finally {
|
|
240
|
-
if ($player) { $player.Dispose() }
|
|
241
|
-
}
|
|
420
|
+
Invoke-AudioPlay $voicePath
|
|
242
421
|
}
|
|
243
422
|
} else {
|
|
244
423
|
# No background track found, play voice only
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
248
|
-
$player.PlaySync()
|
|
249
|
-
} finally {
|
|
250
|
-
if ($player) { $player.Dispose() }
|
|
251
|
-
}
|
|
424
|
+
Invoke-AudioPlay $voicePath
|
|
252
425
|
}
|
|
253
426
|
} else {
|
|
254
427
|
# No background music, play the (possibly reverbed) voice
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
258
|
-
$player.PlaySync()
|
|
259
|
-
} finally {
|
|
260
|
-
if ($player) { $player.Dispose() }
|
|
261
|
-
}
|
|
428
|
+
Invoke-AudioPlay $voicePath
|
|
262
429
|
}
|
|
263
430
|
}
|
|
264
431
|
} else {
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml)
|
|
12
12
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
13
13
|
|
|
14
|
-
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.5
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -40,11 +40,21 @@ Whether you're coding in Claude Code, chatting in Claude Desktop, using Warp Ter
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
## 🌟 NEW
|
|
43
|
+
## 🌟 NEW IN v5.5 — Per-LLM Audio Routing
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Give **each LLM its own voice, pretext, and music** — Claude Code, Copilot, and Codex can all sound different without touching global settings.
|
|
46
|
+
|
|
47
|
+
- Add `llm:<name>|...|voice|pretext|engine` rows to `audio-effects.cfg`
|
|
48
|
+
- MCP server auto-detects which LLM is calling and passes `--llm <key>`
|
|
49
|
+
- Configure via **Setup → Default → Configure** in the TUI
|
|
50
|
+
|
|
51
|
+
Also fixed: Windows installer crash (`spinner.info is not a function`) on **reinstall** with an older global AgentVibes install.
|
|
52
|
+
|
|
53
|
+
---
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
## v5.4 — TUI Installer, Spinner Fix & Dependency Cleanup
|
|
56
|
+
|
|
57
|
+
### 🎤 Agent Vibes v1.0 Voice Browser
|
|
48
58
|
|
|
49
59
|
**🎤 Browse, Sample & Install 914 Voices in Real-Time**
|
|
50
60
|
|
|
@@ -297,8 +307,6 @@ All 50+ Piper voices AgentVibes provides are sourced from Hugging Face's open-so
|
|
|
297
307
|
|
|
298
308
|
**Browse and sample 914 voices in real-time!**
|
|
299
309
|
|
|
300
|
-

|
|
301
|
-
|
|
302
310
|
```bash
|
|
303
311
|
npx agentvibes-voice-browser
|
|
304
312
|
```
|
|
@@ -453,9 +461,6 @@ macOS ships with bash 3.2 (from 2007). After this, everything works perfectly!
|
|
|
453
461
|
|
|
454
462
|
**The easiest way to find your perfect voice!**
|
|
455
463
|
|
|
456
|
-

|
|
457
|
-
*Browse, sample, and install from 914 voices with real-time audio preview*
|
|
458
|
-
|
|
459
464
|
### Launch the Browser
|
|
460
465
|
|
|
461
466
|
```bash
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🎵 v5.5.0 — Per-LLM Audio Routing & Windows Installer Resilience
|
|
4
|
+
|
|
5
|
+
**Released:** 2026-04-27
|
|
6
|
+
|
|
7
|
+
### 🆕 Per-LLM Audio Routing
|
|
8
|
+
Each LLM (Claude Code, Copilot, Codex) can now have its own voice, pretext, reverb, and
|
|
9
|
+
background-music settings. The MCP server passes `--llm <key>` to both `play-tts.sh`
|
|
10
|
+
(Linux/macOS) and `play-tts.ps1` (Windows), and the scripts look up `llm:<key>` rows in
|
|
11
|
+
`audio-effects.cfg`. Default rows for `claude-code`, `copilot`, and `codex` ship out of the
|
|
12
|
+
box; configure them via **Setup → Default → Configure** in the TUI.
|
|
13
|
+
|
|
14
|
+
### 🐛 Windows Installer Crash Fix
|
|
15
|
+
Fixed `spinner.info is not a function` error that crashed AgentVibes **reinstalls** on Windows
|
|
16
|
+
when users had an older global install. All 10 file-copy functions in the installer now wrap
|
|
17
|
+
their spinner with `createRobustSpinner()` so stale callers can never cause a crash regardless
|
|
18
|
+
of which methods they expose.
|
|
19
|
+
|
|
20
|
+
### 🎶 Windows Background Music Parity
|
|
21
|
+
Windows TTS playback now prefers `ffplay` (sinc resampling, no artefacts) over the low-quality
|
|
22
|
+
WinMM `SoundPlayer` resampler. The new `Invoke-AudioPlay` helper handles the fallback
|
|
23
|
+
transparently — if `ffplay` is unavailable, `SoundPlayer` is used as before.
|
|
24
|
+
|
|
25
|
+
### 🎉 Party Mode Cross-Platform Entry Point
|
|
26
|
+
BMAD party mode step files and the Copilot skill now consistently reference
|
|
27
|
+
`node bin/bmad-speak.js` — the single cross-platform entry point that delegates to
|
|
28
|
+
`bmad-speak.ps1` on Windows and `bmad-speak.sh` elsewhere.
|
|
29
|
+
|
|
30
|
+
### 🔧 Other Fixes
|
|
31
|
+
- `play-tts.sh` now accepts a named `--llm <key>` flag in addition to the `LLM_PROVIDER` env var
|
|
32
|
+
- `mcp-server/server.py` routes `AGENTVIBES_LLM` → `CLAUDECODE=1` → `AGENTVIBES_MCP_FALLBACK`
|
|
33
|
+
priority chain and forwards the resolved key as `-llm`/`--llm` to TTS scripts
|
|
34
|
+
- Added `audio-effects.cfg` rows for `llm:claude-code`, `llm:copilot`, `llm:codex`
|
|
35
|
+
- Added `command-routing.test.js` and `ConfigService` unit tests
|
|
36
|
+
- npm pack content guard now catches untracked publishable files
|
|
37
|
+
|
|
38
|
+
### 📊 Technical
|
|
39
|
+
- 231 tests passing (0 failures)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
3
43
|
## 🎛️ v5.4.0 — TUI Installer, Spinner Fix & Dependency Cleanup
|
|
4
44
|
|
|
5
45
|
**Released:** 2026-04-22
|
package/bin/agentvibes.js
CHANGED
|
@@ -75,6 +75,16 @@ export function resolveStartTab(args, configService) {
|
|
|
75
75
|
return { startTab: 'install' };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
if (cmd === 'update') {
|
|
79
|
+
// Always route update to CLI installer (src/installer.js)
|
|
80
|
+
return { cliUpdate: true, args: args.slice(1) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (cmd === 'uninstall') {
|
|
84
|
+
// Always route uninstall to CLI installer (src/installer.js)
|
|
85
|
+
return { cliUninstall: true, args: args.slice(1) };
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
if (cmd === 'config' || cmd === 'configure') {
|
|
79
89
|
return { startTab: 'settings' };
|
|
80
90
|
}
|
|
@@ -154,6 +164,24 @@ if (_argv1 === _thisFile) {
|
|
|
154
164
|
process.exit(0);
|
|
155
165
|
}
|
|
156
166
|
|
|
167
|
+
if (result.cliUpdate) {
|
|
168
|
+
const installerPath = path.resolve(__dirname, '..', 'src', 'installer.js');
|
|
169
|
+
execFileSync(process.execPath, [installerPath, 'update', ...result.args], {
|
|
170
|
+
stdio: 'inherit',
|
|
171
|
+
shell: false,
|
|
172
|
+
});
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (result.cliUninstall) {
|
|
177
|
+
const installerPath = path.resolve(__dirname, '..', 'src', 'installer.js');
|
|
178
|
+
execFileSync(process.execPath, [installerPath, 'uninstall', ...result.args], {
|
|
179
|
+
stdio: 'inherit',
|
|
180
|
+
shell: false,
|
|
181
|
+
});
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
launchConsole({ startTab: result.startTab }).catch(err => {
|
|
158
186
|
process.stderr.write(`Failed to launch AgentVibes console: ${err.message}\n`);
|
|
159
187
|
process.exit(1);
|
package/mcp-server/server.py
CHANGED
|
@@ -192,6 +192,17 @@ class AgentVibesServer:
|
|
|
192
192
|
original_language = await self._get_language()
|
|
193
193
|
await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
|
|
194
194
|
|
|
195
|
+
# Resolve LLM key: AGENTVIBES_LLM > CLAUDECODE=1 > AGENTVIBES_MCP_FALLBACK > "default"
|
|
196
|
+
llm_key = os.environ.get("AGENTVIBES_LLM", "").strip()
|
|
197
|
+
if llm_key and not _re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", llm_key):
|
|
198
|
+
llm_key = ""
|
|
199
|
+
if not llm_key and os.environ.get("CLAUDECODE", "").strip() == "1":
|
|
200
|
+
llm_key = "claude-code"
|
|
201
|
+
if not llm_key:
|
|
202
|
+
fallback = os.environ.get("AGENTVIBES_MCP_FALLBACK", "").strip()
|
|
203
|
+
if fallback and _re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", fallback):
|
|
204
|
+
llm_key = fallback
|
|
205
|
+
|
|
195
206
|
# Call the TTS script via appropriate shell
|
|
196
207
|
tts_script = "play-tts.ps1" if self.is_windows else "play-tts.sh"
|
|
197
208
|
play_tts = self.hooks_dir / tts_script
|
|
@@ -199,8 +210,13 @@ class AgentVibesServer:
|
|
|
199
210
|
args = ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(play_tts), text]
|
|
200
211
|
if voice:
|
|
201
212
|
args.extend(["-VoiceOverride", voice])
|
|
213
|
+
if llm_key:
|
|
214
|
+
args.extend(["-llm", llm_key])
|
|
202
215
|
else:
|
|
203
|
-
args = ["bash", str(play_tts)
|
|
216
|
+
args = ["bash", str(play_tts)]
|
|
217
|
+
if llm_key:
|
|
218
|
+
args.extend(["--llm", llm_key])
|
|
219
|
+
args.append(text)
|
|
204
220
|
if voice:
|
|
205
221
|
args.append(voice)
|
|
206
222
|
|
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": "5.
|
|
4
|
+
"version": "5.5.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": [
|
|
@@ -185,12 +185,15 @@ export function scanTracks() {
|
|
|
185
185
|
const tracksDir = _getTracksDir();
|
|
186
186
|
try {
|
|
187
187
|
const files = fs.readdirSync(tracksDir);
|
|
188
|
+
const mp3s = files.filter(f => /\.mp3$/i.test(f));
|
|
189
|
+
// If the directory exists but has no mp3s (e.g. empty npm package dir),
|
|
190
|
+
// fall back to the static catalog so bundled tracks always show.
|
|
191
|
+
if (mp3s.length === 0) return BUILT_IN_TRACK_CATALOG.map(t => ({ ...t, isBuiltIn: true }));
|
|
188
192
|
const builtInIds = new Set(BUILT_IN_TRACK_CATALOG.map(t => t.id));
|
|
189
193
|
// Sort by the alphabetic part of the label (skip leading emoji/symbols)
|
|
190
194
|
// so the order reflects the track NAME, not the emoji codepoint.
|
|
191
195
|
const _sortKey = (s) => s.replace(/^[^a-zA-Z]+/, '');
|
|
192
|
-
return
|
|
193
|
-
.filter(f => /\.mp3$/i.test(f))
|
|
196
|
+
return mp3s
|
|
194
197
|
.map(f => ({ id: f, label: formatTrackLabel(f), isBuiltIn: builtInIds.has(f) }))
|
|
195
198
|
.sort((a, b) => _sortKey(a.label).localeCompare(_sortKey(b.label), undefined, { sensitivity: 'base' }));
|
|
196
199
|
} catch {
|
|
@@ -133,6 +133,10 @@ function loadCatalog() {
|
|
|
133
133
|
// Build lookup map for O(1) access by voiceId
|
|
134
134
|
_catalogMap = new Map();
|
|
135
135
|
for (const c of _catalogEntries) _catalogMap.set(c.voiceId, c);
|
|
136
|
+
|
|
137
|
+
// Patch libritts_r onnx.json files so their speaker IDs become friendly names.
|
|
138
|
+
// Must run after catalog loads so the name mapping is available.
|
|
139
|
+
patchLibriTTSSpeakerNames();
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
/**
|
|
@@ -142,45 +146,48 @@ function loadCatalog() {
|
|
|
142
146
|
* Safe to call multiple times — skips if already patched.
|
|
143
147
|
*/
|
|
144
148
|
function patchLibriTTSSpeakerNames() {
|
|
149
|
+
// Load catalog once for all models
|
|
150
|
+
const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
|
|
151
|
+
if (!fs.existsSync(catalogPath)) return;
|
|
152
|
+
let speakers;
|
|
145
153
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
speakers = JSON.parse(fs.readFileSync(catalogPath, 'utf8')).libritts_speakers ?? {};
|
|
155
|
+
} catch { return; }
|
|
156
|
+
|
|
157
|
+
// Models to patch and how to detect unpatched keys:
|
|
158
|
+
// libritts-high → raw keys are p-prefixed corpus IDs (p3922, p8699, …)
|
|
159
|
+
// libritts_r-* → raw keys are plain numeric corpus IDs (3922, 8699, …)
|
|
160
|
+
const MODELS = [
|
|
161
|
+
{ file: 'en_US-libritts-high.onnx.json', notPatched: (k) => /^p\d+$/.test(k) },
|
|
162
|
+
{ file: 'en_US-libritts_r-medium.onnx.json', notPatched: (k) => /^\d+$/.test(k) },
|
|
163
|
+
{ file: 'en_US-libritts_r-high.onnx.json', notPatched: (k) => /^\d+$/.test(k) },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const { file, notPatched } of MODELS) {
|
|
167
|
+
try {
|
|
168
|
+
const jsonPath = path.join(PIPER_VOICES_DIR, file);
|
|
169
|
+
if (!fs.existsSync(jsonPath)) continue;
|
|
170
|
+
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
171
|
+
if (!data.speaker_id_map || data.num_speakers <= 1) continue;
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const friendly = speakers[idx]?.voice_name;
|
|
171
|
-
if (friendly) {
|
|
172
|
-
newMap[friendly] = parseInt(idx, 10);
|
|
173
|
-
} else {
|
|
174
|
-
newMap[pname] = parseInt(idx, 10);
|
|
173
|
+
const names = Object.keys(data.speaker_id_map);
|
|
174
|
+
// Skip if already patched (first key is a friendly name, not a raw corpus ID)
|
|
175
|
+
if (names.length === 0 || !notPatched(names[0])) continue;
|
|
176
|
+
|
|
177
|
+
// Values are 0-based sequential indices into the model — use as catalog key
|
|
178
|
+
const newMap = {};
|
|
179
|
+
for (const [rawKey, idx] of Object.entries(data.speaker_id_map)) {
|
|
180
|
+
const friendly = speakers[String(idx)]?.voice_name;
|
|
181
|
+
newMap[friendly ?? rawKey] = idx;
|
|
175
182
|
}
|
|
176
|
-
}
|
|
177
183
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
data.speaker_id_map = newMap;
|
|
185
|
+
// Verify file ownership before writing (security: CLAUDE.md)
|
|
186
|
+
const stat = fs.statSync(jsonPath);
|
|
187
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) continue;
|
|
188
|
+
fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
|
|
189
|
+
} catch { /* non-fatal — skip this model */ }
|
|
190
|
+
}
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
// Column widths for the multi-column voice list
|
|
@@ -417,9 +424,9 @@ export function parseMultiSpeaker(voiceId) {
|
|
|
417
424
|
try {
|
|
418
425
|
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
419
426
|
let speakerId = data.speaker_id_map?.[speakerName] ?? null;
|
|
420
|
-
// Fallback: if the .onnx.json still has raw
|
|
427
|
+
// Fallback: if the .onnx.json still has raw corpus IDs (not yet patched),
|
|
421
428
|
// look up the numeric speaker ID from voice-assignments.json catalog.
|
|
422
|
-
if (speakerId == null && model === 'en_US-libritts-high') {
|
|
429
|
+
if (speakerId == null && (model === 'en_US-libritts-high' || /^en_US-libritts_r-/.test(model))) {
|
|
423
430
|
try {
|
|
424
431
|
const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
|
|
425
432
|
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
|
@@ -443,6 +450,9 @@ export function parseMultiSpeaker(voiceId) {
|
|
|
443
450
|
* @returns {string[]}
|
|
444
451
|
*/
|
|
445
452
|
export function scanInstalledVoices() {
|
|
453
|
+
// Ensure catalog is loaded and libritts_r onnx.json files are patched
|
|
454
|
+
// before we read their speaker_id_map keys (otherwise we get raw corpus IDs).
|
|
455
|
+
loadCatalog();
|
|
446
456
|
try {
|
|
447
457
|
const files = fs.readdirSync(PIPER_VOICES_DIR);
|
|
448
458
|
const onnxFiles = files
|
|
@@ -588,6 +598,30 @@ export function getVoiceMeta(voiceId) {
|
|
|
588
598
|
_metaCache.set(voiceId, result);
|
|
589
599
|
return result;
|
|
590
600
|
}
|
|
601
|
+
// libritts_r variants share speaker names with libritts-high after patching
|
|
602
|
+
if (/^en_US-libritts_r-/.test(ms.model)) {
|
|
603
|
+
// After patching: speakerName is a friendly name — look up in libritts-high catalog
|
|
604
|
+
const highCat = _catalogMap.get(`en_US-libritts-high${MS_SEP}${ms.speakerName}`);
|
|
605
|
+
if (highCat) {
|
|
606
|
+
const result = { displayName: highCat.displayName, gender: highCat.gender, provider: `Piper (${ms.model})` };
|
|
607
|
+
_metaCache.set(voiceId, result);
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
// Before patching: speakerName is a raw numeric corpus ID — resolve via onnx.json value
|
|
611
|
+
if (/^\d+$/.test(ms.speakerName)) {
|
|
612
|
+
try {
|
|
613
|
+
const jsonPath = path.join(PIPER_VOICES_DIR, ms.model + '.onnx.json');
|
|
614
|
+
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
615
|
+
const seqIdx = data.speaker_id_map?.[ms.speakerName];
|
|
616
|
+
if (seqIdx != null && _catalogEntries[seqIdx]) {
|
|
617
|
+
const cat = _catalogEntries[seqIdx];
|
|
618
|
+
const result = { displayName: cat.displayName, gender: cat.gender, provider: `Piper (${ms.model})` };
|
|
619
|
+
_metaCache.set(voiceId, result);
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
} catch { /* fall through */ }
|
|
623
|
+
}
|
|
624
|
+
}
|
|
591
625
|
// Fallback for speakers not in the catalog (e.g. 16Speakers model)
|
|
592
626
|
const displayName = uniquifyVoiceName(ms.speakerName.replace(/_/g, ' '));
|
|
593
627
|
const result = {
|
package/src/installer.js
CHANGED
|
@@ -3249,6 +3249,7 @@ async function handleTermuxSshConfiguration() {
|
|
|
3249
3249
|
* @returns {Promise<{count: number, boxen: string}>} Number of files copied and boxen content
|
|
3250
3250
|
*/
|
|
3251
3251
|
async function copyCommandFiles(targetDir, spinner) {
|
|
3252
|
+
spinner = createRobustSpinner(spinner);
|
|
3252
3253
|
spinner.start('Installing /agent-vibes slash commands...');
|
|
3253
3254
|
const srcCommandsDir = path.join(__dirname, '..', '.claude', 'commands', 'agent-vibes');
|
|
3254
3255
|
const commandsDir = path.join(targetDir, '.claude', 'commands');
|
|
@@ -3462,6 +3463,7 @@ function buildHookInstallationBoxen(installedFiles, failedFiles) {
|
|
|
3462
3463
|
* @returns {Promise<{count: number, boxen: string|null}>} Number of files copied and boxen content
|
|
3463
3464
|
*/
|
|
3464
3465
|
async function copyHookFiles(targetDir, spinner) {
|
|
3466
|
+
spinner = createRobustSpinner(spinner);
|
|
3465
3467
|
spinner.start('Installing TTS helper scripts...');
|
|
3466
3468
|
const hooksSubdir = isNativeWindows() ? 'hooks-windows' : 'hooks';
|
|
3467
3469
|
const srcHooksDir = path.join(__dirname, '..', '.claude', hooksSubdir);
|
|
@@ -3516,6 +3518,7 @@ async function copyHookFiles(targetDir, spinner) {
|
|
|
3516
3518
|
* @returns {Promise<{count: number, boxen: string|null}>} Number of files copied and boxen content
|
|
3517
3519
|
*/
|
|
3518
3520
|
async function copyPersonalityFiles(targetDir, spinner) {
|
|
3521
|
+
spinner = createRobustSpinner(spinner);
|
|
3519
3522
|
spinner.start('Installing personality templates...');
|
|
3520
3523
|
const srcPersonalitiesDir = path.join(__dirname, '..', '.claude', 'personalities');
|
|
3521
3524
|
const destPersonalitiesDir = path.join(targetDir, '.claude', 'personalities');
|
|
@@ -3598,6 +3601,7 @@ async function copyPersonalityFiles(targetDir, spinner) {
|
|
|
3598
3601
|
* @returns {Promise<number>} Number of files copied
|
|
3599
3602
|
*/
|
|
3600
3603
|
async function copyPluginFiles(targetDir, spinner) {
|
|
3604
|
+
spinner = createRobustSpinner(spinner);
|
|
3601
3605
|
spinner.start('Installing BMAD plugin files...');
|
|
3602
3606
|
const srcPluginsDir = path.join(__dirname, '..', '.claude', 'plugins');
|
|
3603
3607
|
const destPluginsDir = path.join(targetDir, '.claude', 'plugins');
|
|
@@ -3632,6 +3636,7 @@ async function copyPluginFiles(targetDir, spinner) {
|
|
|
3632
3636
|
* @returns {Promise<number>} Number of files copied
|
|
3633
3637
|
*/
|
|
3634
3638
|
async function copyBmadConfigFiles(targetDir, spinner) {
|
|
3639
|
+
spinner = createRobustSpinner(spinner);
|
|
3635
3640
|
spinner.start('Installing BMAD config files...');
|
|
3636
3641
|
const srcBmadDir = path.join(__dirname, '..', '.agentvibes', 'bmad');
|
|
3637
3642
|
const destBmadDir = path.join(targetDir, '.agentvibes', 'bmad');
|
|
@@ -3664,6 +3669,7 @@ async function copyBmadConfigFiles(targetDir, spinner) {
|
|
|
3664
3669
|
* @returns {Promise<{count: number, boxen: string}>} Number of files copied and boxen content
|
|
3665
3670
|
*/
|
|
3666
3671
|
async function copyBackgroundMusicFiles(targetDir, spinner) {
|
|
3672
|
+
spinner = createRobustSpinner(spinner);
|
|
3667
3673
|
spinner.start('Installing background music tracks...');
|
|
3668
3674
|
const srcBackgroundsDir = path.join(__dirname, '..', '.claude', 'audio', 'tracks');
|
|
3669
3675
|
const destBackgroundsDir = path.join(targetDir, '.claude', 'audio', 'tracks');
|
|
@@ -3786,6 +3792,7 @@ async function copyBackgroundMusicFiles(targetDir, spinner) {
|
|
|
3786
3792
|
* @returns {Promise<number>} Number of files copied
|
|
3787
3793
|
*/
|
|
3788
3794
|
async function copyConfigFiles(targetDir, spinner) {
|
|
3795
|
+
spinner = createRobustSpinner(spinner);
|
|
3789
3796
|
spinner.start('Installing configuration files...');
|
|
3790
3797
|
const srcConfigDir = path.join(__dirname, '..', '.claude', 'config');
|
|
3791
3798
|
const destConfigDir = path.join(targetDir, '.claude', 'config');
|
|
@@ -3848,6 +3855,7 @@ async function copyConfigFiles(targetDir, spinner) {
|
|
|
3848
3855
|
* @param {Object} spinner - Ora spinner instance
|
|
3849
3856
|
*/
|
|
3850
3857
|
async function copyCodexFiles(targetDir, spinner) {
|
|
3858
|
+
spinner = createRobustSpinner(spinner);
|
|
3851
3859
|
spinner.start('Installing Codex integration files...');
|
|
3852
3860
|
const srcCodexDir = path.join(__dirname, '..', '.codex');
|
|
3853
3861
|
const destCodexDir = path.join(targetDir, '.codex');
|
|
@@ -3901,6 +3909,7 @@ async function copyCodexFiles(targetDir, spinner) {
|
|
|
3901
3909
|
* @param {Object} spinner - Ora spinner instance
|
|
3902
3910
|
*/
|
|
3903
3911
|
async function configureSessionStartHook(targetDir, spinner) {
|
|
3912
|
+
spinner = createRobustSpinner(spinner);
|
|
3904
3913
|
spinner.start('Configuring AgentVibes hook for automatic TTS...');
|
|
3905
3914
|
const claudeDir = path.join(targetDir, '.claude');
|
|
3906
3915
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
@@ -3956,6 +3965,7 @@ async function configureSessionStartHook(targetDir, spinner) {
|
|
|
3956
3965
|
* @param {Object} spinner - Ora spinner instance
|
|
3957
3966
|
*/
|
|
3958
3967
|
async function configurePartyModeHook(targetDir, spinner, homeDirOverride) {
|
|
3968
|
+
spinner = createRobustSpinner(spinner);
|
|
3959
3969
|
spinner.start('Configuring BMAD party mode TTS hook...');
|
|
3960
3970
|
const homeDir = homeDirOverride || os.homedir();
|
|
3961
3971
|
const globalClaudeDir = path.join(homeDir, '.claude');
|
|
@@ -198,7 +198,7 @@ export async function installClaudeMcp(targetDir) {
|
|
|
198
198
|
|
|
199
199
|
try {
|
|
200
200
|
// Copy hooks, commands, config, personality, plugin, bmad config files
|
|
201
|
-
const silentSpinner = { start: () => {}, succeed: () => {}, fail: () => {} };
|
|
201
|
+
const silentSpinner = { start: () => {}, stop: () => {}, succeed: () => {}, fail: () => {}, warn: () => {}, info: () => {}, stopAndPersist: () => {}, get text() { return ''; }, set text(_) {}, get isSpinning() { return false; } };
|
|
202
202
|
const installer = await import('../installer.js');
|
|
203
203
|
await installer.copyHookFiles(targetDir, silentSpinner);
|
|
204
204
|
await installer.copyCommandFiles(targetDir, silentSpinner);
|