agentvibes 5.1.2 → 5.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/config/audio-effects.cfg +3 -1
- package/.claude/hooks/play-tts.sh +10 -0
- package/.claude/hooks-windows/play-tts.ps1 +221 -62
- package/README.md +24 -1
- package/RELEASE_NOTES.md +112 -0
- package/mcp-server/server.py +30 -11
- package/package.json +1 -1
- package/src/console/tabs/setup-tab.js +69 -12
- package/src/installer.js +73 -12
- package/src/services/llm-provider-service.js +114 -11
|
@@ -51,4 +51,6 @@ _party_mode|compand 0.3,1 6:-70,-60,-20|agent_vibes_dark_chill_step_loop.mp3|0.4
|
|
|
51
51
|
# Default (no agent specified) - clean with Bachata background|||
|
|
52
52
|
default||agent_vibes_chillwave_v2_loop.mp3|0.15
|
|
53
53
|
analyst|reverb 70 50 100|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
54
|
-
llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Claude Code here|piper
|
|
54
|
+
llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Claude Code here|piper
|
|
55
|
+
llm:copilot|light|agent_vibes_bossa_nova_v2_loop.mp3|0.15|en_US-libritts-high::Anna-11|Copilot here|piper
|
|
56
|
+
llm:codex|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Codex here|piper
|
|
@@ -126,6 +126,16 @@ TEXT="${TEXT//\\?/?}" # Remove \?
|
|
|
126
126
|
TEXT="${TEXT//\\,/,}" # Remove \,
|
|
127
127
|
TEXT="${TEXT//\\./.}" # Remove \. (keep the period)
|
|
128
128
|
|
|
129
|
+
# When no --llm is supplied, route through the "default" pseudo-LLM so the
|
|
130
|
+
# user-managed `llm:default` row in audio-effects.cfg becomes the global
|
|
131
|
+
# fallback for voice / pretext / music / effects. This is configured via
|
|
132
|
+
# Setup → Default → Configure in the TUI. If `llm:default` doesn't exist,
|
|
133
|
+
# the lookup returns empty and the script falls through to the legacy
|
|
134
|
+
# global config chain (project / user .agentvibes/config.json).
|
|
135
|
+
if [[ -z "$LLM_PROVIDER" ]]; then
|
|
136
|
+
LLM_PROVIDER="default"
|
|
137
|
+
fi
|
|
138
|
+
|
|
129
139
|
# Per-LLM config lookup: if --llm is passed, look up llm:<name> in audio-effects.cfg
|
|
130
140
|
# Format: llm:<name>|REVERB_PRESET|BACKGROUND_FILE|BACKGROUND_VOLUME|VOICE|PRETEXT
|
|
131
141
|
_LLM_VOICE=""
|
|
@@ -16,8 +16,142 @@ param(
|
|
|
16
16
|
[string]$llm = ""
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
+
# Security: Validate LLM provider name (alphanumeric, hyphens, underscores
|
|
20
|
+
# only) -- mirrors play-tts.sh line 92. This prevents weird values from
|
|
21
|
+
# poisoning the audio-effects.cfg lookup or the AGENTVIBES_LLM_KEY env var
|
|
22
|
+
# we export to child scripts. An invalid value is treated as unset rather
|
|
23
|
+
# than aborting, so the script falls back to the default config and the
|
|
24
|
+
# rest of TTS still works.
|
|
25
|
+
if ($llm -and $llm -notmatch '^[a-zA-Z0-9_-]+$') {
|
|
26
|
+
Write-Error ("Invalid LLM provider name: '{0}' - must match {1}. Falling back to default config." -f $llm, '^[a-zA-Z0-9_-]+$')
|
|
27
|
+
$llm = ""
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# When no -llm is supplied, route through the "default" pseudo-LLM so the
|
|
31
|
+
# user-managed `llm:default` row in audio-effects.cfg becomes the global
|
|
32
|
+
# fallback for voice / pretext / music / effects. This is configured via
|
|
33
|
+
# Setup -> Default -> Configure in the TUI. If `llm:default` doesn't exist,
|
|
34
|
+
# the lookup will return empty and the script falls through to the
|
|
35
|
+
# legacy global config chain (project / user .agentvibes/config.json).
|
|
36
|
+
if (-not $llm) {
|
|
37
|
+
$llm = "default"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# --- Cross-process playback serialization ---
|
|
41
|
+
# Without this, any two callers of play-tts.ps1 (Claude Code PostToolUse hook,
|
|
42
|
+
# Codex MCP text_to_speech, Copilot MCP text_to_speech, direct CLI) race each
|
|
43
|
+
# other and produce overlapping / interleaved audio. Party mode already has
|
|
44
|
+
# its own mutex (AgentVibesPartyModeTTSQueue) at the bmad-party-speak.ps1
|
|
45
|
+
# level, but MCP-initiated calls bypass it entirely.
|
|
46
|
+
#
|
|
47
|
+
# We use a DIFFERENT mutex name ("AgentVibesPlaybackLock") so there's no
|
|
48
|
+
# deadlock risk with the party-mode mutex -- they can be held independently
|
|
49
|
+
# by nested processes.
|
|
50
|
+
#
|
|
51
|
+
# The mutex is acquired immediately before PlaySync() and released right
|
|
52
|
+
# after, so CPU-bound synthesis/ffmpeg work can overlap with another
|
|
53
|
+
# process's playback.
|
|
54
|
+
$_PlaybackMutex = New-Object System.Threading.Mutex($false, "AgentVibesPlaybackLock")
|
|
55
|
+
|
|
56
|
+
# --- Script-level watchdog ---
|
|
57
|
+
# If anything in this script hangs (SoundPlayer deadlock, audio device
|
|
58
|
+
# locked, ffmpeg stuck, etc.), a sibling PowerShell job waits 25 seconds
|
|
59
|
+
# and force-kills this process. Without this, a stuck play-tts.ps1 holds
|
|
60
|
+
# the playback mutex forever and silently blocks every subsequent TTS
|
|
61
|
+
# call across all LLMs. The watchdog guarantees forward progress.
|
|
62
|
+
#
|
|
63
|
+
# 25s is chosen to be LONGER than the mutex timeout (15s) but SHORT
|
|
64
|
+
# enough that a stuck process clears before the user's next turn. If
|
|
65
|
+
# you fire two calls per turn and the first is stuck, the watchdog kills
|
|
66
|
+
# it before the second turn arrives so the audio subsystem recovers
|
|
67
|
+
# without manual intervention. Long legitimate messages (>25s of speech)
|
|
68
|
+
# are rare at default verbosity levels; when they do occur the watchdog
|
|
69
|
+
# kills playback mid-sentence, which is acceptable degradation vs. a
|
|
70
|
+
# deadlocked queue.
|
|
71
|
+
$_WatchdogJob = $null
|
|
72
|
+
try {
|
|
73
|
+
$_WatchdogJob = Start-Job -ArgumentList $PID -ScriptBlock {
|
|
74
|
+
param($parentPid)
|
|
75
|
+
Start-Sleep -Seconds 25
|
|
76
|
+
try {
|
|
77
|
+
# Only kill if still alive -- harmless if already exited
|
|
78
|
+
$p = Get-Process -Id $parentPid -ErrorAction SilentlyContinue
|
|
79
|
+
if ($p) {
|
|
80
|
+
[Console]::Error.WriteLine("[AgentVibes] play-tts.ps1 watchdog fired -- force-killing pid $parentPid after 25s")
|
|
81
|
+
Stop-Process -Id $parentPid -Force -ErrorAction SilentlyContinue
|
|
82
|
+
}
|
|
83
|
+
} catch { }
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
# If Start-Job fails (rare), just continue without the watchdog -- no
|
|
87
|
+
# regression from pre-watchdog behavior.
|
|
88
|
+
$_WatchdogJob = $null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function Invoke-SerializedPlay {
|
|
92
|
+
param([Parameter(Mandatory)][string]$WavPath)
|
|
93
|
+
$acquired = $false
|
|
94
|
+
try {
|
|
95
|
+
try {
|
|
96
|
+
# 15s timeout to acquire the playback mutex. If we can't get
|
|
97
|
+
# it in 15s, the holder is almost certainly a stuck/crashed
|
|
98
|
+
# prior run. AbandonedMutexException means the holder's
|
|
99
|
+
# process actually died -- we inherit ownership.
|
|
100
|
+
$acquired = $_PlaybackMutex.WaitOne(15000)
|
|
101
|
+
} catch [System.Threading.AbandonedMutexException] {
|
|
102
|
+
$acquired = $true
|
|
103
|
+
}
|
|
104
|
+
if (-not $acquired) {
|
|
105
|
+
# Self-heal: kill any stuck play-tts.ps1 processes (other than
|
|
106
|
+
# ourselves) that have been alive longer than 20 seconds. This
|
|
107
|
+
# frees the mutex so the NEXT call can succeed without the user
|
|
108
|
+
# running taskkill manually. We still exit with code 2 because
|
|
109
|
+
# this call's audio is lost, but the queue recovers immediately.
|
|
110
|
+
try {
|
|
111
|
+
$myPid = $PID
|
|
112
|
+
$cutoff = (Get-Date).AddSeconds(-20)
|
|
113
|
+
$stuck = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
114
|
+
Where-Object {
|
|
115
|
+
$_.Name -eq 'powershell.exe' -and
|
|
116
|
+
$_.ProcessId -ne $myPid -and
|
|
117
|
+
$_.CommandLine -like '*play-tts.ps1*' -and
|
|
118
|
+
$_.CreationDate -lt $cutoff
|
|
119
|
+
}
|
|
120
|
+
foreach ($p in $stuck) {
|
|
121
|
+
[Console]::Error.WriteLine("[AgentVibes] Self-heal: killing stuck play-tts.ps1 pid $($p.ProcessId) (alive since $($p.CreationDate))")
|
|
122
|
+
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
|
123
|
+
}
|
|
124
|
+
} catch { }
|
|
125
|
+
[Console]::Error.WriteLine("[AgentVibes] ERROR: play-tts.ps1 could not acquire playback mutex within 15s. A prior play-tts.ps1 process was stuck holding it and has been killed; the next TTS call should succeed.")
|
|
126
|
+
exit 2
|
|
127
|
+
}
|
|
128
|
+
$player = $null
|
|
129
|
+
try {
|
|
130
|
+
$player = New-Object System.Media.SoundPlayer $WavPath
|
|
131
|
+
$player.PlaySync()
|
|
132
|
+
} finally {
|
|
133
|
+
if ($player) { $player.Dispose() }
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
if ($acquired) {
|
|
137
|
+
try { $_PlaybackMutex.ReleaseMutex() } catch { }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Register an exit handler that stops the watchdog job on normal exit so
|
|
143
|
+
# it doesn't fire on successful short runs.
|
|
144
|
+
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
|
|
145
|
+
try {
|
|
146
|
+
if ($_WatchdogJob) {
|
|
147
|
+
Stop-Job -Job $_WatchdogJob -ErrorAction SilentlyContinue
|
|
148
|
+
Remove-Job -Job $_WatchdogJob -Force -ErrorAction SilentlyContinue
|
|
149
|
+
}
|
|
150
|
+
} catch { }
|
|
151
|
+
} | Out-Null
|
|
152
|
+
|
|
19
153
|
# Configuration paths
|
|
20
|
-
# Priority: CLAUDE_PROJECT_DIR env var
|
|
154
|
+
# Priority: CLAUDE_PROJECT_DIR env var -> script's parent project -> user profile
|
|
21
155
|
# Local project settings ALWAYS override global (~/.claude)
|
|
22
156
|
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
23
157
|
|
|
@@ -47,16 +181,19 @@ if (Test-Path $MuteFile) {
|
|
|
47
181
|
}
|
|
48
182
|
|
|
49
183
|
# Per-LLM config lookup: if --llm is passed, look up llm:<name> in audio-effects.cfg
|
|
50
|
-
# Format: llm:<name>|
|
|
184
|
+
# Format: llm:<name>|REVERB|BG_FILE|BG_VOLUME|VOICE|PRETEXT|ENGINE
|
|
51
185
|
$LlmVoice = ""
|
|
52
186
|
$LlmPretext = ""
|
|
53
187
|
$LlmReverb = ""
|
|
54
188
|
$LlmEngine = ""
|
|
189
|
+
$LlmBgTrack = ""
|
|
190
|
+
$LlmBgVolume = ""
|
|
55
191
|
$ProjectRoot = Split-Path -Parent $ClaudeDir
|
|
56
192
|
$ConfigDir = "$ClaudeDir\config"
|
|
57
193
|
|
|
58
194
|
if ($llm) {
|
|
59
195
|
$llmKey = "llm:$llm"
|
|
196
|
+
$llmKeyPattern = '^' + [regex]::Escape($llmKey) + '\|'
|
|
60
197
|
# Check project-local audio-effects.cfg first, then global
|
|
61
198
|
$cfgPaths = @(
|
|
62
199
|
"$ConfigDir\audio-effects.cfg",
|
|
@@ -65,12 +202,18 @@ if ($llm) {
|
|
|
65
202
|
foreach ($cfgPath in $cfgPaths) {
|
|
66
203
|
if (-not $LlmVoice -and -not $LlmPretext -and (Test-Path $cfgPath)) {
|
|
67
204
|
foreach ($line in (Get-Content $cfgPath)) {
|
|
68
|
-
if ($line -match
|
|
205
|
+
if ($line -match $llmKeyPattern) {
|
|
69
206
|
$parts = $line -split '\|'
|
|
70
|
-
# parts: [0]=key [1]=
|
|
207
|
+
# parts: [0]=key [1]=reverb [2]=bg_file [3]=bg_vol [4]=voice [5]=pretext [6]=engine
|
|
71
208
|
if ($parts.Length -ge 2 -and $parts[1].Trim()) {
|
|
72
209
|
$LlmReverb = $parts[1].Trim()
|
|
73
210
|
}
|
|
211
|
+
if ($parts.Length -ge 3 -and $parts[2].Trim()) {
|
|
212
|
+
$LlmBgTrack = $parts[2].Trim()
|
|
213
|
+
}
|
|
214
|
+
if ($parts.Length -ge 4 -and $parts[3].Trim()) {
|
|
215
|
+
$LlmBgVolume = $parts[3].Trim()
|
|
216
|
+
}
|
|
74
217
|
if ($parts.Length -ge 5 -and $parts[4].Trim()) {
|
|
75
218
|
$LlmVoice = $parts[4].Trim()
|
|
76
219
|
}
|
|
@@ -85,8 +228,21 @@ if ($llm) {
|
|
|
85
228
|
}
|
|
86
229
|
}
|
|
87
230
|
}
|
|
88
|
-
#
|
|
89
|
-
|
|
231
|
+
# LLM per-LLM voice routing.
|
|
232
|
+
#
|
|
233
|
+
# PRIORITY CHANGE: when -llm is passed AND the llm row has a voice,
|
|
234
|
+
# the per-LLM voice always wins — even over an explicit VoiceOverride
|
|
235
|
+
# parameter passed by the MCP caller. Rationale: Codex / Copilot /
|
|
236
|
+
# Claude Code all call `get_config` at session start and then echo
|
|
237
|
+
# the global voice back on every `text_to_speech` call. With the
|
|
238
|
+
# old "explicit wins" priority, that global voice overrode our
|
|
239
|
+
# per-LLM routing and broke the entire point of having llm:<key>
|
|
240
|
+
# rows in audio-effects.cfg.
|
|
241
|
+
#
|
|
242
|
+
# To request a specific voice for a specific call that bypasses the
|
|
243
|
+
# LLM routing, the caller should NOT pass -llm, or should use the
|
|
244
|
+
# `llm:default` row (which has no voice column to override).
|
|
245
|
+
if ($LlmVoice) {
|
|
90
246
|
$VoiceOverride = $LlmVoice
|
|
91
247
|
}
|
|
92
248
|
# Export LLM key for child scripts (process-local, not system-wide)
|
|
@@ -94,8 +250,8 @@ if ($llm) {
|
|
|
94
250
|
}
|
|
95
251
|
|
|
96
252
|
# Prepend pretext if configured
|
|
97
|
-
# Priority: LLM-specific pretext
|
|
98
|
-
#
|
|
253
|
+
# Priority: LLM-specific pretext -> project .agentvibes/config.json -> project .claude/config/tts-pretext.txt
|
|
254
|
+
# -> global ~/.agentvibes/config.json -> global ~/.claude/config/tts-pretext.txt
|
|
99
255
|
$Pretext = $LlmPretext
|
|
100
256
|
if (-not $Pretext) {
|
|
101
257
|
$PretextSources = @(
|
|
@@ -121,7 +277,6 @@ if (-not $Pretext) {
|
|
|
121
277
|
if ($Pretext) {
|
|
122
278
|
$Text = "$Pretext, $Text"
|
|
123
279
|
}
|
|
124
|
-
|
|
125
280
|
# Determine active provider
|
|
126
281
|
# LLM-specific engine overrides global provider
|
|
127
282
|
$ActiveProvider = "sapi"
|
|
@@ -183,6 +338,15 @@ if (Test-Path $AgentVibesConfig) {
|
|
|
183
338
|
}
|
|
184
339
|
}
|
|
185
340
|
|
|
341
|
+
# When a per-LLM row in audio-effects.cfg has a background track configured,
|
|
342
|
+
# that's an implicit "bg music enabled for this LLM" — force it on regardless
|
|
343
|
+
# of the global backgroundMusic.enabled flag. Without this, setting a per-LLM
|
|
344
|
+
# track in the TUI's Configure modal would have no effect unless the user
|
|
345
|
+
# ALSO toggled global bg music on.
|
|
346
|
+
if ($LlmBgTrack) {
|
|
347
|
+
$BgEnabled = $true
|
|
348
|
+
}
|
|
349
|
+
|
|
186
350
|
# Check if reverb is enabled (allowlist validation)
|
|
187
351
|
# LLM-specific reverb overrides global setting
|
|
188
352
|
$ReverbLevel = "off"
|
|
@@ -216,7 +380,7 @@ if ($BgEnabled -or $HasReverb) {
|
|
|
216
380
|
}
|
|
217
381
|
}
|
|
218
382
|
|
|
219
|
-
# Check for pre-synthesized WAV (party mode optimization
|
|
383
|
+
# Check for pre-synthesized WAV (party mode optimization -- synthesis done before mutex acquisition)
|
|
220
384
|
$PreSynthWav = $env:AGENTVIBES_PRESYNTHESIZED_WAV
|
|
221
385
|
$UsePreSynth = $PreSynthWav -and (Test-Path $PreSynthWav) -and
|
|
222
386
|
(Get-Item $PreSynthWav -ErrorAction SilentlyContinue).Length -gt 0
|
|
@@ -234,14 +398,10 @@ if ($UsePreSynth) {
|
|
|
234
398
|
Write-Host "[SYNTH] Using pre-synthesized audio..." -ForegroundColor Cyan
|
|
235
399
|
# If no post-processing needed, play the pre-synth file directly and exit
|
|
236
400
|
if (-not $NeedsPostProcess) {
|
|
237
|
-
$player = $null
|
|
238
401
|
try {
|
|
239
|
-
|
|
240
|
-
$player.PlaySync()
|
|
402
|
+
Invoke-SerializedPlay -WavPath $PreSynthWav
|
|
241
403
|
} catch {
|
|
242
404
|
Write-Host "[WARNING] Pre-synth playback failed: $_" -ForegroundColor Yellow
|
|
243
|
-
} finally {
|
|
244
|
-
if ($player) { $player.Dispose() }
|
|
245
405
|
}
|
|
246
406
|
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
247
407
|
exit 0
|
|
@@ -268,6 +428,25 @@ if ($UsePreSynth) {
|
|
|
268
428
|
Write-Host "$item"
|
|
269
429
|
}
|
|
270
430
|
}
|
|
431
|
+
# Parse the provider output for "[OK] Saved to: <path>" so we can
|
|
432
|
+
# use the EXACT file the provider just wrote. This replaces the
|
|
433
|
+
# old "pick most recent tts-XXXXXXXX.wav" heuristic which would
|
|
434
|
+
# silently replay stale audio whenever synthesis failed.
|
|
435
|
+
$FreshSynthFile = $null
|
|
436
|
+
foreach ($item in $providerOutput) {
|
|
437
|
+
$line = if ($item -is [System.Management.Automation.InformationRecord]) {
|
|
438
|
+
$m = $item.MessageData
|
|
439
|
+
if ($m -is [System.Management.Automation.HostInformationMessage]) { $m.Message } else { "$item" }
|
|
440
|
+
} else { "$item" }
|
|
441
|
+
if ($line -match '^\[OK\] Saved to:\s*(.+\.wav)\s*$') {
|
|
442
|
+
$FreshSynthFile = $Matches[1].Trim()
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (-not $FreshSynthFile -or -not (Test-Path $FreshSynthFile)) {
|
|
446
|
+
[Console]::Error.WriteLine("[AgentVibes] ERROR: Provider synthesis did not produce an output file. NOT falling back to stale audio. Check provider logs above.")
|
|
447
|
+
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
448
|
+
exit 3
|
|
449
|
+
}
|
|
271
450
|
} else {
|
|
272
451
|
if ($VoiceOverride) {
|
|
273
452
|
& $ProviderScript $Text $VoiceOverride
|
|
@@ -287,13 +466,17 @@ if ($UsePreSynth) {
|
|
|
287
466
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
288
467
|
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
289
468
|
|
|
290
|
-
#
|
|
469
|
+
# Use the EXACT file the provider script just wrote (captured from its
|
|
470
|
+
# "[OK] Saved to: <path>" output line above). The old "pick most recent
|
|
471
|
+
# tts-XXXXXXXX.wav" heuristic silently replayed stale audio whenever
|
|
472
|
+
# synthesis failed — there is no safe way to guess which file is fresh.
|
|
291
473
|
$AudioDir = "$ClaudeDir\audio"
|
|
292
474
|
$RecentWav = if ($UsePreSynth) {
|
|
293
475
|
Get-Item $PreSynthWav -ErrorAction SilentlyContinue
|
|
476
|
+
} elseif ($FreshSynthFile -and (Test-Path $FreshSynthFile)) {
|
|
477
|
+
Get-Item $FreshSynthFile -ErrorAction SilentlyContinue
|
|
294
478
|
} else {
|
|
295
|
-
|
|
296
|
-
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
479
|
+
$null
|
|
297
480
|
}
|
|
298
481
|
|
|
299
482
|
if ($RecentWav -and $RecentWav.Length -gt 0) {
|
|
@@ -309,7 +492,10 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
309
492
|
default { "" }
|
|
310
493
|
}
|
|
311
494
|
if ($reverbFilter) {
|
|
312
|
-
|
|
495
|
+
# Use a fixed name OUTSIDE the `tts-XXXXXXXX` random-name
|
|
496
|
+
# namespace so the "pick most recent tts-*.wav" logic can't
|
|
497
|
+
# accidentally pick this post-processed file as a synth input.
|
|
498
|
+
$reverbedFile = "$AudioDir\av-reverbed-scratch.wav"
|
|
313
499
|
$reverbArgs = "-y -i `"$voicePath`" -af `"$reverbFilter`" `"$reverbedFile`""
|
|
314
500
|
$proc = Start-Process -FilePath "ffmpeg" -ArgumentList $reverbArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
|
|
315
501
|
if ($proc.ExitCode -eq 0 -and (Test-Path $reverbedFile)) {
|
|
@@ -329,7 +515,7 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
329
515
|
if (Test-Path $AudioEffectsCfg) {
|
|
330
516
|
# Try agent-specific config first, then fall back to default
|
|
331
517
|
# Format: AGENT_NAME|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME
|
|
332
|
-
# Lookup order: agent name
|
|
518
|
+
# Lookup order: agent name -> llm:<name> -> default
|
|
333
519
|
$agentName = $env:AGENTVIBES_AGENT_NAME
|
|
334
520
|
$configLine = $null
|
|
335
521
|
|
|
@@ -392,7 +578,14 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
392
578
|
}
|
|
393
579
|
|
|
394
580
|
if (Test-Path $BgTrackPath) {
|
|
395
|
-
|
|
581
|
+
# Mixed output goes to a fixed name OUTSIDE the tts-XXXXXXXX
|
|
582
|
+
# random-name namespace so the "pick most recent tts-*.wav"
|
|
583
|
+
# logic can't accidentally pick this as a synth input in the
|
|
584
|
+
# next invocation. (Previously we'd name this as
|
|
585
|
+
# "$voicePath-mixed.wav" which generated files like
|
|
586
|
+
# tts-xxx.wav.effected-mixed.wav that kept re-matching and
|
|
587
|
+
# compounding on every run.)
|
|
588
|
+
$MixedFile = "$AudioDir\av-mixed-scratch.wav"
|
|
396
589
|
|
|
397
590
|
try {
|
|
398
591
|
# Get voice duration to calculate total length
|
|
@@ -415,62 +608,28 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
415
608
|
$proc = Start-Process -FilePath "ffmpeg" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
|
|
416
609
|
|
|
417
610
|
if ($proc.ExitCode -eq 0 -and (Test-Path $MixedFile) -and (Get-Item $MixedFile).Length -gt 0) {
|
|
418
|
-
# Play the mixed audio
|
|
419
|
-
$player = $null
|
|
611
|
+
# Play the mixed audio (via serialized mutex)
|
|
420
612
|
try {
|
|
421
|
-
|
|
422
|
-
$player.PlaySync()
|
|
613
|
+
Invoke-SerializedPlay -WavPath $MixedFile
|
|
423
614
|
} catch {
|
|
424
615
|
Write-Host "[WARNING] Mixed playback failed, playing voice only" -ForegroundColor Yellow
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
$player2 = New-Object System.Media.SoundPlayer $voicePath
|
|
428
|
-
$player2.PlaySync()
|
|
429
|
-
} finally {
|
|
430
|
-
if ($player2) { $player2.Dispose() }
|
|
431
|
-
}
|
|
432
|
-
} finally {
|
|
433
|
-
if ($player) { $player.Dispose() }
|
|
616
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
434
617
|
}
|
|
435
618
|
} else {
|
|
436
619
|
# Mixing failed, play voice only
|
|
437
|
-
|
|
438
|
-
try {
|
|
439
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
440
|
-
$player.PlaySync()
|
|
441
|
-
} finally {
|
|
442
|
-
if ($player) { $player.Dispose() }
|
|
443
|
-
}
|
|
620
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
444
621
|
}
|
|
445
622
|
} catch {
|
|
446
623
|
# ffmpeg failed, play voice only
|
|
447
|
-
|
|
448
|
-
try {
|
|
449
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
450
|
-
$player.PlaySync()
|
|
451
|
-
} finally {
|
|
452
|
-
if ($player) { $player.Dispose() }
|
|
453
|
-
}
|
|
624
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
454
625
|
}
|
|
455
626
|
} else {
|
|
456
627
|
# No background track found, play voice only
|
|
457
|
-
|
|
458
|
-
try {
|
|
459
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
460
|
-
$player.PlaySync()
|
|
461
|
-
} finally {
|
|
462
|
-
if ($player) { $player.Dispose() }
|
|
463
|
-
}
|
|
628
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
464
629
|
}
|
|
465
630
|
} else {
|
|
466
631
|
# No background music, play the (possibly reverbed) voice
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
470
|
-
$player.PlaySync()
|
|
471
|
-
} finally {
|
|
472
|
-
if ($player) { $player.Dispose() }
|
|
473
|
-
}
|
|
632
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
474
633
|
}
|
|
475
634
|
}
|
|
476
635
|
} 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.1.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.1.4
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -43,6 +43,29 @@ Whether you're using Claude Code, GitHub Copilot, OpenAI Codex, Claude Desktop,
|
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
46
|
+
## 🛡️ NEW IN v5.1.4 — TTS Resilience Overhaul + Default LLM Provider
|
|
47
|
+
|
|
48
|
+
- **Default LLM provider** — New fallback entry at the bottom of Setup → Providers. Config-only; opens the standard Configure modal. Used when a tool calls TTS without identifying its LLM.
|
|
49
|
+
- **Per-LLM background music auto-enables** — Setting a bg track on the per-LLM Configure modal actually plays it now (no need to also toggle global bg music).
|
|
50
|
+
- **Copilot CLI support** — `installCopilotMcp` now writes both `.vscode/mcp.json` (Copilot Chat) AND `~/.copilot/mcp-config.json` (Copilot CLI — different product, different config path).
|
|
51
|
+
- **Per-client routing architecture** — `.mcp.json` no longer sets `AGENTVIBES_LLM`. Claude Code is auto-detected via `CLAUDECODE=1` env var. Copilot CLI reads its own global config. No more client config conflicts.
|
|
52
|
+
- **Self-healing TTS mutex** — When a stuck `play-tts.ps1` process blocks the playback queue, the next caller auto-kills it (no manual `taskkill` needed). 25-second watchdog guarantees forward progress.
|
|
53
|
+
- **No more stale audio replay** — `play-tts.ps1` captures the exact synth output filename from provider stdout instead of guessing "most recent `tts-*.wav`". Silent replay of old audio is gone.
|
|
54
|
+
- **Per-LLM voice wins over explicit `VoiceOverride`** — LLMs echo back `get_config` results on every call, which was overriding per-LLM routing. Fixed.
|
|
55
|
+
- **`lessac-medium` → `lessac-high`** default for codex — Silent synthesis failure workaround.
|
|
56
|
+
- **Scratch file rename + ASCII-only encoding** — Eliminates accumulating compound audio files and CP1252 parse errors on Windows.
|
|
57
|
+
- **Setup → Install confirmation** now advances focus to the next provider row (Install → Install → Install flow).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🛡️ v5.1.3 — Hardening Pass (Adversarial Review Followup)
|
|
62
|
+
|
|
63
|
+
- **Existing `.mcp.json` is now auto-migrated** — v5.1.2's installer detected an existing `.mcp.json` and printed instructions instead of fixing it, leaving v5.1.0/v5.1.1 users still broken after upgrade. v5.1.3 merges the `AGENTVIBES_LLM` env var into existing configs in-place.
|
|
64
|
+
- **`AGENTVIBES_LLM` is now validated** in both `mcp-server/server.py` (Python regex) and `play-tts.ps1` (PowerShell regex), matching `play-tts.sh`'s `^[a-zA-Z0-9_-]+$` check. Cross-platform contract is now symmetric.
|
|
65
|
+
- **`npm pack` content guard hardened**: hard-fails (not silent-passes) when `npm pack` errors; uses `git status --porcelain` to catch UNTRACKED publishable files (the v5.1.0 disaster also could've happened with a stray new file); has explicit 60s timeout to prevent CI hangs.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
46
69
|
## 🔀 v5.1.2 — MCP Per-LLM Routing Hotfix
|
|
47
70
|
|
|
48
71
|
- **MCP server now reads `AGENTVIBES_LLM` env var** instead of hardcoding `copilot` — Codex / Copilot / Claude Code each get routed to their own per-LLM voice / pretext / music / effects config from `audio-effects.cfg`.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,117 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🛡️ v5.1.4 — TTS Resilience Overhaul + Default LLM Provider + Per-Client Routing
|
|
4
|
+
|
|
5
|
+
**Release Date:** April 2026
|
|
6
|
+
|
|
7
|
+
This release closes a long-running cluster of bugs around per-LLM TTS routing, parallel audio playback, stuck-process deadlocks, and stale-audio replay. It also adds a new "Default" provider slot in the Setup tab for fallback audio configuration, and switches to a per-client config scheme that correctly routes Claude Code, GitHub Copilot (Chat + CLI), and OpenAI Codex to their own voices and pretexts.
|
|
8
|
+
|
|
9
|
+
### New Features
|
|
10
|
+
|
|
11
|
+
- **Default LLM provider** — New entry at the bottom of Setup → Providers. Config-only (no install/remove buttons) with a Configure button that opens the standard per-LLM audio modal. When any tool calls TTS without identifying its LLM, the `llm:default` row in `audio-effects.cfg` provides the fallback voice, pretext, music, reverb, and engine. Empty pretext by default — users opt in.
|
|
12
|
+
|
|
13
|
+
- **Per-LLM background music now auto-enables** — Setting a `bg_track` on any per-LLM Configure modal now actually plays that track. Previously you also had to toggle the global `backgroundMusic.enabled` flag, which made the per-LLM bg track field silently dead.
|
|
14
|
+
|
|
15
|
+
- **Copilot CLI support** — `installCopilotMcp` now writes BOTH `.vscode/mcp.json` (for VS Code Copilot Chat) AND `~/.copilot/mcp-config.json` (for the standalone `copilot` CLI, which is a different product). Fresh installs support both tools automatically.
|
|
16
|
+
|
|
17
|
+
### Per-Client Routing Architecture
|
|
18
|
+
|
|
19
|
+
Previously `AGENTVIBES_LLM=claude-code` was set in `.mcp.json`, which broke Copilot CLI because Copilot CLI also reads `.mcp.json` with precedence over its own `~/.copilot/mcp-config.json` — so it adopted Claude Code's env and mis-routed.
|
|
20
|
+
|
|
21
|
+
The new architecture splits per-LLM identification per-client:
|
|
22
|
+
|
|
23
|
+
- `.mcp.json` (project) has **no `AGENTVIBES_LLM` env block**
|
|
24
|
+
- `~/.copilot/mcp-config.json` sets `AGENTVIBES_LLM=copilot` for GitHub Copilot CLI
|
|
25
|
+
- `~/.codex/config.toml` sets `AGENTVIBES_LLM=codex` for OpenAI Codex
|
|
26
|
+
- The MCP server (`mcp-server/server.py`) **auto-detects Claude Code** via the `CLAUDECODE=1` env var that Claude Code sets on every subprocess it spawns. Copilot CLI and Codex don't set this var, so each client reliably routes to its own config.
|
|
27
|
+
|
|
28
|
+
Upgrade path: re-run the AgentVibes installer in any existing project. The new `installClaudeMcp` auto-strips any stale `AGENTVIBES_LLM` env block from existing `.mcp.json` files so Copilot CLI stops mis-routing.
|
|
29
|
+
|
|
30
|
+
### TTS Resilience Overhaul (`play-tts.ps1`)
|
|
31
|
+
|
|
32
|
+
- **Cross-process playback mutex** — `AgentVibesPlaybackLock` (named mutex) serializes playback across all callers: Claude Code hooks, MCP `text_to_speech`, direct CLI, party mode. No more overlapping or parallel audio when multiple LLMs run in the same project.
|
|
33
|
+
|
|
34
|
+
- **Self-healing on mutex timeout** — When the 15-second mutex acquisition fails, the new code queries `Win32_Process` for any stuck `play-tts.ps1` process older than 20 seconds, calls `Stop-Process -Force` on each, and logs the kill to stderr. The next TTS call succeeds immediately — no manual `taskkill` needed.
|
|
35
|
+
|
|
36
|
+
- **25-second script watchdog** — A sibling PowerShell job force-kills `play-tts.ps1` after 25 seconds regardless of where it's stuck (SoundPlayer deadlock, locked audio device, zombie ffmpeg). Guarantees forward progress.
|
|
37
|
+
|
|
38
|
+
- **Hard error on mutex timeout** — Replaces the old "play anyway" fallback which just stacked more stuck processes behind the first. Now exits cleanly with code 2 and a diagnostic message.
|
|
39
|
+
|
|
40
|
+
- **Exact filename capture from provider stdout** — `play-tts.ps1` parses the `[OK] Saved to: <path>` line from `play-tts-piper.ps1` and uses that exact file. Replaces the old "pick most recent `tts-*.wav` in the audio dir" heuristic which silently replayed stale audio from earlier sessions whenever synthesis failed. Root cause of the "Codex speaks Claude Code's audio" bug.
|
|
41
|
+
|
|
42
|
+
- **Synthesis-failure hard error** — When the provider doesn't produce an output file, `play-tts.ps1` exits with code 3 and a loud error instead of falling back to any older file in the cache.
|
|
43
|
+
|
|
44
|
+
- **Scratch file rename** — Reverb post-processing now writes to `av-reverbed-scratch.wav` and mix post-processing writes to `av-mixed-scratch.wav`. Fixed names outside the `tts-XXXXXXXX` random namespace so the file lookup can never pick them up as a synth input. Eliminates the compound `tts-xxx.wav.effected-mixed-mixed-mixed-mixed.wav` chain that accumulated on every run.
|
|
45
|
+
|
|
46
|
+
- **Per-LLM voice overrides explicit `VoiceOverride`** — LLMs call `get_config` at session start and echo the returned voice back on every `text_to_speech` call as an explicit MCP parameter. With the old "explicit wins" priority, that global voice overrode per-LLM routing. Now the `llm:<key>` voice row always wins when `-llm` is set.
|
|
47
|
+
|
|
48
|
+
- **`$LlmBgTrack` and `$LlmBgVolume` parsed from cfg row** — Previously fields 2 and 3 of the `llm:<key>` line were ignored in the PowerShell version (the bash version already parsed them).
|
|
49
|
+
|
|
50
|
+
- **Per-LLM `bg_track` force-enables `$BgEnabled`** — When a per-LLM row specifies a background track, it's auto-enabled regardless of the global `backgroundMusic.enabled` flag.
|
|
51
|
+
|
|
52
|
+
- **ASCII-only encoding** — Removed em-dash `—` and right-arrow `→` from `play-tts.ps1`. PowerShell on some Windows locales loads scripts in CP1252 and choked on the UTF-8 bytes with a misleading "Missing closing `}`" parse error.
|
|
53
|
+
|
|
54
|
+
- **`lessac-medium` → `lessac-high` default for codex** — `en_US-lessac-medium` silently fails to synthesize on some Windows Piper installs (loads the model, exits with empty output). `lessac-high` works reliably and is the new default in `DEFAULT_LLM_CONFIGS.codex` and the packaged `audio-effects.cfg`.
|
|
55
|
+
|
|
56
|
+
### UX Improvements
|
|
57
|
+
|
|
58
|
+
- **Setup → Providers Install confirmation** — Pressing Enter to dismiss the post-install confirmation page now advances focus to the NEXT provider row's same-column button (Install → Install, Configure → Configure), instead of snapping back to Claude Code's Install. Installing all three providers is now a natural Enter-Enter-Enter flow.
|
|
59
|
+
|
|
60
|
+
### Testing & Hardening
|
|
61
|
+
|
|
62
|
+
- **30 new/updated regression tests** in `test/unit/llm-provider-mcp-routing.test.js` and `test/unit/windows-tts.test.js` covering:
|
|
63
|
+
- Default LLM provider config shape + setup-tab wiring
|
|
64
|
+
- `CLAUDECODE` auto-detection in `server.py`
|
|
65
|
+
- `.mcp.json` template MUST NOT contain `AGENTVIBES_LLM`
|
|
66
|
+
- `~/.copilot/mcp-config.json` writer in `installCopilotMcp`
|
|
67
|
+
- `play-tts.ps1` PowerShell-parseable (catches em-dash / encoding regressions via `[System.Management.Automation.Language.Parser]`)
|
|
68
|
+
|
|
69
|
+
### How to Update
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
npm cache clean --force
|
|
73
|
+
npx --yes agentvibes@5.1.4
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If you have any existing project with AgentVibes installed, re-run the installer there once so the per-client config migration takes effect. The `.mcp.json` in each project will be auto-upgraded to strip the stale `AGENTVIBES_LLM` env block, and `~/.copilot/mcp-config.json` will be created if you have the Copilot provider enabled.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🛡️ v5.1.3 — Hardening Pass (Adversarial Review Followup)
|
|
81
|
+
|
|
82
|
+
**Release Date:** April 2026
|
|
83
|
+
|
|
84
|
+
This release addresses HIGH/MEDIUM findings from a parallel adversarial review (Blind Hunter + Edge Case Hunter) of the v5.1.2 hotfix.
|
|
85
|
+
|
|
86
|
+
### Bug Fixes
|
|
87
|
+
|
|
88
|
+
- **Existing `.mcp.json` is now auto-migrated** — v5.1.2's installer detected an existing `.mcp.json` and printed instructions to fix it manually. Result: anyone upgrading from v5.1.0/v5.1.1 was still broken after `npm i -g agentvibes@5.1.2`. v5.1.3 detects an existing config, merges the `AGENTVIBES_LLM` env var (and the rest of the agentvibes server entry) in-place, and shows a green "✅ MCP Configuration Updated" message. Existing user `env` keys are preserved.
|
|
89
|
+
|
|
90
|
+
- **`AGENTVIBES_LLM` is now validated** before being forwarded to the child shell. The bash version of `play-tts.sh` already validated `^[a-zA-Z0-9_-]+$` and rejected weird values; the Python `mcp-server/server.py` and the Windows `play-tts.ps1` did not. Now all three do, with the same regex. Invalid values are logged to stderr and treated as "unset" so TTS still works (falls back to default config).
|
|
91
|
+
|
|
92
|
+
### Testing & Hardening
|
|
93
|
+
|
|
94
|
+
- **`npm pack` content guard now hard-fails on `packError`** instead of silently returning early. The previous v5.1.2 implementation had `if (packError) return;` in 6 of 8 tests, which meant a real `npm pack` failure surfaced as one failed test and SIX false-greens — defeating the entire purpose of the publish guard.
|
|
95
|
+
|
|
96
|
+
- **Dirty-tree check now uses `git status --porcelain`** instead of `git diff HEAD`. The previous implementation only caught modified-but-uncommitted tracked files; it missed brand-new untracked files in publishable directories (e.g. a `play-tts.ps1.new` someone forgot to delete). The v5.1.0 disaster could just as easily have shipped via an untracked stray file.
|
|
97
|
+
|
|
98
|
+
- **`npm pack` exec timeout** — added explicit 60-second timeout with `SIGKILL` so the publish guard can't hang CI on a stuck registry call.
|
|
99
|
+
|
|
100
|
+
- **`git status` failures hard-fail** instead of silently skipping. A missing git binary in CI was previously treated as "no findings" — that's exactly the opposite of what a security guard should do. Now it fails loudly with a message explaining the guard requires git.
|
|
101
|
+
|
|
102
|
+
- **3 new regression tests** for the new validations and migration logic.
|
|
103
|
+
|
|
104
|
+
### How to Update
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
npm cache clean --force
|
|
108
|
+
npx --yes agentvibes@5.1.3
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If you upgraded from v5.1.0 or v5.1.1, **re-run the AgentVibes installer once** so the new in-place migration touches your existing `.mcp.json` / `.codex/config.toml` / `.vscode/mcp.json`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
3
115
|
## 🔀 v5.1.2 — MCP Per-LLM Routing Hotfix
|
|
4
116
|
|
|
5
117
|
**Release Date:** April 2026
|
package/mcp-server/server.py
CHANGED
|
@@ -51,8 +51,6 @@ from typing import Optional
|
|
|
51
51
|
from mcp.server import Server
|
|
52
52
|
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
|
|
53
53
|
import mcp.server.stdio
|
|
54
|
-
|
|
55
|
-
|
|
56
54
|
class AgentVibesServer:
|
|
57
55
|
"""MCP Server for AgentVibes TTS functionality"""
|
|
58
56
|
|
|
@@ -196,17 +194,38 @@ class AgentVibesServer:
|
|
|
196
194
|
|
|
197
195
|
# Call the TTS script via appropriate shell.
|
|
198
196
|
#
|
|
199
|
-
# The LLM key is
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
197
|
+
# The LLM key is determined with the following priority:
|
|
198
|
+
#
|
|
199
|
+
# 1. AGENTVIBES_LLM env var (explicit — wins over everything).
|
|
200
|
+
# Set in each provider's MCP launcher config:
|
|
201
|
+
# ~/.codex/config.toml -> "codex"
|
|
202
|
+
# ~/.copilot/mcp-config.json -> "copilot"
|
|
205
203
|
#
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
204
|
+
# 2. Auto-detection via CLAUDECODE=1 env var (Claude Code sets
|
|
205
|
+
# this automatically when it spawns any subprocess). This
|
|
206
|
+
# lets project-level .mcp.json files omit the env block —
|
|
207
|
+
# which is critical because GitHub Copilot CLI also reads
|
|
208
|
+
# .mcp.json and would otherwise pick up claude-code's env.
|
|
209
|
+
#
|
|
210
|
+
# 3. Unset — play-tts falls back to llm:default or global config.
|
|
211
|
+
#
|
|
212
|
+
# Validation mirrors play-tts.sh line 92 — alphanumeric /
|
|
213
|
+
# hyphen / underscore only. Prevents weird values from
|
|
214
|
+
# poisoning the audio-effects.cfg lookup or child-shell args.
|
|
215
|
+
import re as _re
|
|
209
216
|
llm_key = os.environ.get("AGENTVIBES_LLM", "").strip()
|
|
217
|
+
if llm_key and not _re.match(r"^[a-zA-Z0-9_-]+$", llm_key):
|
|
218
|
+
print(
|
|
219
|
+
f"[AgentVibes] WARN: Ignoring invalid AGENTVIBES_LLM='{llm_key}' "
|
|
220
|
+
"(must match ^[a-zA-Z0-9_-]+$); falling back to auto-detect",
|
|
221
|
+
file=__import__('sys').stderr,
|
|
222
|
+
)
|
|
223
|
+
llm_key = ""
|
|
224
|
+
# Claude Code sets CLAUDECODE=1 when it spawns subprocesses.
|
|
225
|
+
# Use that as a fallback identifier when no explicit env var
|
|
226
|
+
# was provided. Copilot CLI and Codex do NOT set this.
|
|
227
|
+
if not llm_key and os.environ.get("CLAUDECODE", "").strip() == "1":
|
|
228
|
+
llm_key = "claude-code"
|
|
210
229
|
tts_script = "play-tts.ps1" if self.is_windows else "play-tts.sh"
|
|
211
230
|
play_tts = self.hooks_dir / tts_script
|
|
212
231
|
if self.is_windows:
|
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.1.
|
|
4
|
+
"version": "5.1.4",
|
|
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": [
|
|
@@ -565,6 +565,13 @@ export function createSetupTab(screen, services) {
|
|
|
565
565
|
},
|
|
566
566
|
});
|
|
567
567
|
|
|
568
|
+
// The "default" provider is config-only — it has no install/remove
|
|
569
|
+
// semantics. Hide those buttons and only show Configure.
|
|
570
|
+
if (provider.isDefault) {
|
|
571
|
+
installBtn.hide();
|
|
572
|
+
removeBtn.hide();
|
|
573
|
+
}
|
|
574
|
+
|
|
568
575
|
// Wire actions
|
|
569
576
|
installBtn.on('press', async () => { await handleProviderInstall(provider); });
|
|
570
577
|
installBtn.key(['enter', 'space'], async () => { await handleProviderInstall(provider); });
|
|
@@ -575,8 +582,10 @@ export function createSetupTab(screen, services) {
|
|
|
575
582
|
configBtn.on('press', async () => { await handleProviderConfigure(provider); });
|
|
576
583
|
configBtn.key(['enter', 'space'], async () => { await handleProviderConfigure(provider); });
|
|
577
584
|
|
|
578
|
-
// Navigation on each button
|
|
579
|
-
|
|
585
|
+
// Navigation on each button — for the default provider, only Configure
|
|
586
|
+
// is focusable since install/remove are hidden.
|
|
587
|
+
const navButtons = provider.isDefault ? [configBtn] : [installBtn, removeBtn, configBtn];
|
|
588
|
+
for (const btn of navButtons) {
|
|
580
589
|
btn.key(['tab', 'right'], () => { cycleFocus(1); });
|
|
581
590
|
btn.key(['S-tab', 'left'], () => { cycleFocus(-1); });
|
|
582
591
|
btn.key(['escape'], () => {
|
|
@@ -609,10 +618,17 @@ export function createSetupTab(screen, services) {
|
|
|
609
618
|
return { installBtn, removeBtn, configBtn };
|
|
610
619
|
}
|
|
611
620
|
|
|
612
|
-
// Build all provider rows
|
|
621
|
+
// Build all provider rows.
|
|
622
|
+
// For the default provider, install/remove are hidden — push configBtn
|
|
623
|
+
// three times so the row-of-3 arrow-nav arithmetic still works (every
|
|
624
|
+
// "slot" in the default row lands on Configure, the only visible button).
|
|
613
625
|
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
614
626
|
const { installBtn, removeBtn, configBtn } = createProviderRow(PROVIDERS[i], i);
|
|
615
|
-
|
|
627
|
+
if (PROVIDERS[i].isDefault) {
|
|
628
|
+
providerFocusableItems.push(configBtn, configBtn, configBtn);
|
|
629
|
+
} else {
|
|
630
|
+
providerFocusableItems.push(installBtn, removeBtn, configBtn);
|
|
631
|
+
}
|
|
616
632
|
}
|
|
617
633
|
|
|
618
634
|
function cycleFocus(dir) {
|
|
@@ -624,6 +640,10 @@ export function createSetupTab(screen, services) {
|
|
|
624
640
|
// ── Provider install/remove handlers ──────────────────────────────────────
|
|
625
641
|
|
|
626
642
|
async function handleProviderInstall(provider) {
|
|
643
|
+
// Remember which button the user was on so we can advance focus to
|
|
644
|
+
// the NEXT row (same column) after they dismiss the info page.
|
|
645
|
+
_preInfoFocusIndex = providerFocusIndex;
|
|
646
|
+
|
|
627
647
|
if (provider.id === 'claude-code') {
|
|
628
648
|
const wasInstalled = installedState[provider.id];
|
|
629
649
|
const result = await installClaudeMcp(targetDir);
|
|
@@ -643,7 +663,6 @@ export function createSetupTab(screen, services) {
|
|
|
643
663
|
if (provider.id === 'openai-codex') {
|
|
644
664
|
const wasInstalled = installedState[provider.id];
|
|
645
665
|
const result = await installCodexMcp(targetDir);
|
|
646
|
-
await installCopilotMcp(targetDir);
|
|
647
666
|
await installCodexInstructions(targetDir, packageDir);
|
|
648
667
|
await installCodexHooks(targetDir, packageDir);
|
|
649
668
|
await refreshInstalledState();
|
|
@@ -652,6 +671,10 @@ export function createSetupTab(screen, services) {
|
|
|
652
671
|
}
|
|
653
672
|
|
|
654
673
|
async function handleProviderRemove(provider) {
|
|
674
|
+
// Remember which button the user was on so we can advance focus to
|
|
675
|
+
// the NEXT row (same column) after they dismiss the info page.
|
|
676
|
+
_preInfoFocusIndex = providerFocusIndex;
|
|
677
|
+
|
|
655
678
|
if (provider.id === 'claude-code') {
|
|
656
679
|
const result = await uninstallClaude(targetDir);
|
|
657
680
|
await refreshInstalledState();
|
|
@@ -668,7 +691,6 @@ export function createSetupTab(screen, services) {
|
|
|
668
691
|
|
|
669
692
|
if (provider.id === 'openai-codex') {
|
|
670
693
|
await removeCodexMcp(targetDir);
|
|
671
|
-
await removeCopilotMcp(targetDir);
|
|
672
694
|
await removeCodexInstructions(targetDir);
|
|
673
695
|
await removeCodexHooks(targetDir);
|
|
674
696
|
await refreshInstalledState();
|
|
@@ -683,6 +705,7 @@ export function createSetupTab(screen, services) {
|
|
|
683
705
|
'claude-code': 'claude-code',
|
|
684
706
|
'github-copilot': 'copilot',
|
|
685
707
|
'openai-codex': 'codex',
|
|
708
|
+
'default': 'default',
|
|
686
709
|
};
|
|
687
710
|
const llmKey = llmKeyMap[provider.id] || provider.id;
|
|
688
711
|
const config = loadLlmConfigSync(llmKey, targetDir);
|
|
@@ -701,6 +724,7 @@ export function createSetupTab(screen, services) {
|
|
|
701
724
|
'claude-code': 'Claude Code here',
|
|
702
725
|
'copilot': 'Copilot here',
|
|
703
726
|
'codex': 'Codex here',
|
|
727
|
+
'default': '', // empty by default — user sets via Configure
|
|
704
728
|
};
|
|
705
729
|
|
|
706
730
|
// Read global defaults for display
|
|
@@ -1432,10 +1456,18 @@ export function createSetupTab(screen, services) {
|
|
|
1432
1456
|
function showAllProviderRows() {
|
|
1433
1457
|
providerHeader.show();
|
|
1434
1458
|
for (const row of providerRows) {
|
|
1459
|
+
const provider = PROVIDERS.find(p => p.id === row.id);
|
|
1435
1460
|
row.label.show();
|
|
1436
1461
|
row.statusText.show();
|
|
1437
|
-
|
|
1438
|
-
|
|
1462
|
+
// The "default" provider has no install/remove semantics — keep its
|
|
1463
|
+
// install/remove buttons hidden so only Configure shows.
|
|
1464
|
+
if (provider?.isDefault) {
|
|
1465
|
+
row.installBtn.hide();
|
|
1466
|
+
row.removeBtn.hide();
|
|
1467
|
+
} else {
|
|
1468
|
+
row.installBtn.show();
|
|
1469
|
+
row.removeBtn.show();
|
|
1470
|
+
}
|
|
1439
1471
|
row.configBtn.show();
|
|
1440
1472
|
}
|
|
1441
1473
|
}
|
|
@@ -1587,28 +1619,53 @@ export function createSetupTab(screen, services) {
|
|
|
1587
1619
|
screen.render();
|
|
1588
1620
|
}
|
|
1589
1621
|
|
|
1590
|
-
function showProviderListView() {
|
|
1622
|
+
function showProviderListView(targetIdx = 0) {
|
|
1591
1623
|
providerView = 'list';
|
|
1592
1624
|
infoBox.hide();
|
|
1593
1625
|
contentBox.hide();
|
|
1594
1626
|
showAllProviderRows();
|
|
1595
|
-
|
|
1596
|
-
if (
|
|
1627
|
+
const max = providerFocusableItems.length;
|
|
1628
|
+
if (max > 0) {
|
|
1629
|
+
providerFocusIndex = ((targetIdx % max) + max) % max;
|
|
1630
|
+
providerFocusableItems[providerFocusIndex].focus();
|
|
1631
|
+
}
|
|
1597
1632
|
screen.render();
|
|
1598
1633
|
}
|
|
1599
1634
|
|
|
1600
1635
|
infoBox.key(['escape', 'enter'], () => {
|
|
1601
|
-
|
|
1636
|
+
// After dismissing the install/remove info page, advance focus to the
|
|
1637
|
+
// NEXT provider row but keep the same column (Install stays on Install,
|
|
1638
|
+
// Remove stays on Remove, Configure stays on Configure). Each row has
|
|
1639
|
+
// 3 focusable slots so +3 moves one full row down with wraparound.
|
|
1640
|
+
const max = providerFocusableItems.length;
|
|
1641
|
+
const nextIdx = max > 0 ? (_preInfoFocusIndex + 3) % max : 0;
|
|
1642
|
+
showProviderListView(nextIdx);
|
|
1602
1643
|
});
|
|
1603
1644
|
|
|
1645
|
+
// Captured by handleProviderInstall/Remove right before showing info.
|
|
1646
|
+
// Defaults to 0 so the first-time flow still lands on Claude Code Install.
|
|
1647
|
+
let _preInfoFocusIndex = 0;
|
|
1648
|
+
|
|
1604
1649
|
async function refreshInstalledState() {
|
|
1605
1650
|
for (const p of PROVIDERS) {
|
|
1651
|
+
// The "default" provider is config-only — always treat as available.
|
|
1652
|
+
if (p.isDefault) {
|
|
1653
|
+
installedState[p.id] = true;
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1606
1656
|
const checkFn = p.id === 'claude-code' ? checkClaudeInstalled
|
|
1607
1657
|
: p.id === 'github-copilot' ? checkCopilotInstalled
|
|
1608
1658
|
: checkCodexInstalled;
|
|
1609
1659
|
installedState[p.id] = await checkFn(targetDir);
|
|
1610
1660
|
}
|
|
1611
1661
|
for (const row of providerRows) {
|
|
1662
|
+
const provider = PROVIDERS.find(p => p.id === row.id);
|
|
1663
|
+
// The default provider has no install state to display — show its
|
|
1664
|
+
// config-only nature instead.
|
|
1665
|
+
if (provider?.isDefault) {
|
|
1666
|
+
row.statusText.setContent('{cyan-fg}[Config Only]{/cyan-fg}');
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1612
1669
|
const installed = installedState[row.id];
|
|
1613
1670
|
row.statusText.setContent(
|
|
1614
1671
|
installed
|
package/src/installer.js
CHANGED
|
@@ -4298,14 +4298,17 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4298
4298
|
const mcpConfigPath = path.join(targetDir, '.mcp.json');
|
|
4299
4299
|
|
|
4300
4300
|
// MCP server configuration for AgentVibes.
|
|
4301
|
-
//
|
|
4302
|
-
//
|
|
4301
|
+
//
|
|
4302
|
+
// No `env.AGENTVIBES_LLM` block: GitHub Copilot CLI also reads project
|
|
4303
|
+
// `.mcp.json` with precedence over its own `~/.copilot/mcp-config.json`,
|
|
4304
|
+
// so setting `claude-code` here would mis-route Copilot CLI too.
|
|
4305
|
+
// Instead, the MCP server auto-detects Claude Code via the `CLAUDECODE=1`
|
|
4306
|
+
// env var that Claude Code sets on every subprocess it spawns.
|
|
4303
4307
|
const mcpConfig = {
|
|
4304
4308
|
mcpServers: {
|
|
4305
4309
|
agentvibes: {
|
|
4306
4310
|
command: 'npx',
|
|
4307
|
-
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server']
|
|
4308
|
-
env: { AGENTVIBES_LLM: 'claude-code' }
|
|
4311
|
+
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server']
|
|
4309
4312
|
}
|
|
4310
4313
|
}
|
|
4311
4314
|
};
|
|
@@ -4320,18 +4323,78 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4320
4323
|
}
|
|
4321
4324
|
|
|
4322
4325
|
if (mcpExists) {
|
|
4323
|
-
//
|
|
4326
|
+
// Existing config: upgrade it in-place. Two jobs:
|
|
4327
|
+
// 1. Ensure the agentvibes server entry exists.
|
|
4328
|
+
// 2. STRIP any stale `env.AGENTVIBES_LLM` from earlier versions
|
|
4329
|
+
// (v5.1.2..v5.1.4) — setting it in `.mcp.json` broke Copilot CLI
|
|
4330
|
+
// routing because Copilot CLI also reads `.mcp.json` and would
|
|
4331
|
+
// adopt claude-code's env. Claude Code is now auto-detected
|
|
4332
|
+
// downstream via the CLAUDECODE=1 env var.
|
|
4333
|
+
let migrated = false;
|
|
4334
|
+
let migrationError = null;
|
|
4335
|
+
try {
|
|
4336
|
+
const existingRaw = await fs.readFile(mcpConfigPath, 'utf8');
|
|
4337
|
+
const existingCfg = JSON.parse(existingRaw);
|
|
4338
|
+
if (existingCfg && typeof existingCfg === 'object') {
|
|
4339
|
+
if (!existingCfg.mcpServers || typeof existingCfg.mcpServers !== 'object') {
|
|
4340
|
+
existingCfg.mcpServers = {};
|
|
4341
|
+
}
|
|
4342
|
+
const current = existingCfg.mcpServers.agentvibes;
|
|
4343
|
+
const hasStaleEnv = current?.env?.AGENTVIBES_LLM !== undefined;
|
|
4344
|
+
const needsWrite = !current || hasStaleEnv;
|
|
4345
|
+
if (needsWrite) {
|
|
4346
|
+
// Preserve any OTHER env keys the user added manually (rare) but
|
|
4347
|
+
// drop AGENTVIBES_LLM so Copilot CLI doesn't mis-route.
|
|
4348
|
+
const cleanEnv = { ...(current?.env ?? {}) };
|
|
4349
|
+
delete cleanEnv.AGENTVIBES_LLM;
|
|
4350
|
+
const newEntry = {
|
|
4351
|
+
command: 'npx',
|
|
4352
|
+
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
|
|
4353
|
+
};
|
|
4354
|
+
if (Object.keys(cleanEnv).length > 0) {
|
|
4355
|
+
newEntry.env = cleanEnv;
|
|
4356
|
+
}
|
|
4357
|
+
existingCfg.mcpServers.agentvibes = newEntry;
|
|
4358
|
+
await fs.writeFile(mcpConfigPath, JSON.stringify(existingCfg, null, 2) + '\n');
|
|
4359
|
+
migrated = true;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
} catch (err) {
|
|
4363
|
+
migrationError = err;
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
if (migrated) {
|
|
4367
|
+
console.log(
|
|
4368
|
+
boxen(
|
|
4369
|
+
chalk.green.bold('✅ MCP Configuration Updated\n\n') +
|
|
4370
|
+
chalk.white('Your existing ') + chalk.cyan('.mcp.json') + chalk.white(' has been updated.\n') +
|
|
4371
|
+
chalk.white('Claude Code is auto-detected via ') + chalk.cyan('CLAUDECODE=1') + chalk.white(' at runtime.'),
|
|
4372
|
+
{
|
|
4373
|
+
padding: 1,
|
|
4374
|
+
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
4375
|
+
borderStyle: 'double',
|
|
4376
|
+
borderColor: 'green',
|
|
4377
|
+
}
|
|
4378
|
+
)
|
|
4379
|
+
);
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
// Migration was not needed (already correct) or failed — fall through
|
|
4384
|
+
// to the manual-instructions box.
|
|
4324
4385
|
console.log(
|
|
4325
4386
|
boxen(
|
|
4326
4387
|
chalk.yellow.bold('ℹ️ MCP Configuration Already Exists\n\n') +
|
|
4327
4388
|
chalk.white('An ') + chalk.cyan('.mcp.json') + chalk.white(' file already exists in this project.\n\n') +
|
|
4328
|
-
|
|
4329
|
-
|
|
4389
|
+
(migrationError
|
|
4390
|
+
? chalk.red('Could not auto-update it: ' + migrationError.message + '\n\n')
|
|
4391
|
+
: chalk.gray('It already has the correct AgentVibes entry.\n\n')) +
|
|
4392
|
+
chalk.white('To add or fix the AgentVibes MCP server manually, use:'),
|
|
4330
4393
|
{
|
|
4331
4394
|
padding: 1,
|
|
4332
4395
|
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
4333
4396
|
borderStyle: 'round',
|
|
4334
|
-
borderColor: 'yellow',
|
|
4397
|
+
borderColor: migrationError ? 'red' : 'yellow',
|
|
4335
4398
|
}
|
|
4336
4399
|
)
|
|
4337
4400
|
);
|
|
@@ -4340,8 +4403,7 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4340
4403
|
console.log(
|
|
4341
4404
|
'\n"agentvibes": {\n' +
|
|
4342
4405
|
' "command": "npx",\n' +
|
|
4343
|
-
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]
|
|
4344
|
-
' "env": { "AGENTVIBES_LLM": "claude-code" }\n' +
|
|
4406
|
+
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]\n' +
|
|
4345
4407
|
'}\n'
|
|
4346
4408
|
);
|
|
4347
4409
|
|
|
@@ -4446,8 +4508,7 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4446
4508
|
' "mcpServers": {\n' +
|
|
4447
4509
|
' "agentvibes": {\n' +
|
|
4448
4510
|
' "command": "npx",\n' +
|
|
4449
|
-
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]
|
|
4450
|
-
' "env": { "AGENTVIBES_LLM": "claude-code" }\n' +
|
|
4511
|
+
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]\n' +
|
|
4451
4512
|
' }\n' +
|
|
4452
4513
|
' }\n' +
|
|
4453
4514
|
'}\n'
|
|
@@ -27,8 +27,66 @@ export const PROVIDERS = [
|
|
|
27
27
|
name: 'OpenAI Codex',
|
|
28
28
|
desc: 'OpenAI CLI agent — .codex/config.toml + AGENTS.md',
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
id: 'default',
|
|
32
|
+
name: 'Default (Fallback)',
|
|
33
|
+
desc: 'Used when any tool calls TTS without identifying its LLM',
|
|
34
|
+
// No install/uninstall — this is a config-only entry
|
|
35
|
+
isDefault: true,
|
|
36
|
+
},
|
|
30
37
|
];
|
|
31
38
|
|
|
39
|
+
const DEFAULT_LLM_CONFIGS = {
|
|
40
|
+
// Fallback used when play-tts is invoked with no -llm flag. Pretext is
|
|
41
|
+
// empty by default — users edit it via Setup → Default → Configure. When
|
|
42
|
+
// empty, no prefix is prepended at all.
|
|
43
|
+
default: {
|
|
44
|
+
effects: 'light',
|
|
45
|
+
bgTrack: '',
|
|
46
|
+
bgVolume: '0.15',
|
|
47
|
+
voice: 'en_US-lessac-high',
|
|
48
|
+
pretext: '',
|
|
49
|
+
ttsEngine: 'piper',
|
|
50
|
+
},
|
|
51
|
+
'claude-code': {
|
|
52
|
+
effects: 'light',
|
|
53
|
+
bgTrack: 'agent_vibes_chillwave_v2_loop.mp3',
|
|
54
|
+
bgVolume: '0.15',
|
|
55
|
+
voice: 'en_US-lessac-high',
|
|
56
|
+
pretext: 'Claude Code here',
|
|
57
|
+
ttsEngine: 'piper',
|
|
58
|
+
},
|
|
59
|
+
copilot: {
|
|
60
|
+
effects: 'light',
|
|
61
|
+
bgTrack: 'agent_vibes_bossa_nova_v2_loop.mp3',
|
|
62
|
+
bgVolume: '0.15',
|
|
63
|
+
voice: 'en_US-libritts-high::Anna-11',
|
|
64
|
+
pretext: 'Copilot here',
|
|
65
|
+
ttsEngine: 'piper',
|
|
66
|
+
},
|
|
67
|
+
codex: {
|
|
68
|
+
effects: 'light',
|
|
69
|
+
bgTrack: 'agent_vibes_chillwave_v2_loop.mp3',
|
|
70
|
+
bgVolume: '0.15',
|
|
71
|
+
// NOTE: lessac-medium appears to silently fail to synthesize on some
|
|
72
|
+
// Windows Piper installs (loads the model, exits with no output).
|
|
73
|
+
// lessac-high works reliably, so use it as the default for codex.
|
|
74
|
+
voice: 'en_US-lessac-high',
|
|
75
|
+
pretext: 'Codex here',
|
|
76
|
+
ttsEngine: 'piper',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function ensureDefaultLlmConfigSync(llmKey, targetDir) {
|
|
81
|
+
const existing = loadLlmConfigSync(llmKey, targetDir);
|
|
82
|
+
if (existing.sourcePath) return;
|
|
83
|
+
|
|
84
|
+
const defaults = DEFAULT_LLM_CONFIGS[llmKey];
|
|
85
|
+
if (!defaults) return;
|
|
86
|
+
|
|
87
|
+
saveLlmConfigSync(llmKey, defaults, targetDir);
|
|
88
|
+
}
|
|
89
|
+
|
|
32
90
|
// ── Provider install-checks ─────────────────────────────────────────────────
|
|
33
91
|
|
|
34
92
|
export async function checkClaudeInstalled(targetDir) {
|
|
@@ -73,12 +131,24 @@ export async function checkCodexInstalled(targetDir) {
|
|
|
73
131
|
export async function installClaudeMcp(targetDir) {
|
|
74
132
|
const mcpConfigPath = path.join(targetDir, '.mcp.json');
|
|
75
133
|
|
|
134
|
+
// The agentvibes server entry for Claude Code's .mcp.json.
|
|
135
|
+
//
|
|
136
|
+
// IMPORTANT: no `env.AGENTVIBES_LLM` block here. GitHub Copilot CLI
|
|
137
|
+
// also reads project-level `.mcp.json` with precedence over its own
|
|
138
|
+
// `~/.copilot/mcp-config.json` — so if we set `AGENTVIBES_LLM=claude-code`
|
|
139
|
+
// in `.mcp.json`, Copilot CLI picks up that value too and mis-routes.
|
|
140
|
+
// Instead, the MCP server (mcp-server/server.py) auto-detects Claude
|
|
141
|
+
// Code via the `CLAUDECODE=1` env var that Claude Code sets on every
|
|
142
|
+
// subprocess it spawns. Copilot CLI does NOT set that var, so its
|
|
143
|
+
// spawned MCP server correctly falls back to its own config.
|
|
144
|
+
const agentvibesServer = {
|
|
145
|
+
command: 'npx',
|
|
146
|
+
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
|
|
147
|
+
};
|
|
148
|
+
|
|
76
149
|
const mcpConfig = {
|
|
77
150
|
mcpServers: {
|
|
78
|
-
agentvibes:
|
|
79
|
-
command: 'npx',
|
|
80
|
-
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
|
|
81
|
-
},
|
|
151
|
+
agentvibes: agentvibesServer,
|
|
82
152
|
},
|
|
83
153
|
};
|
|
84
154
|
|
|
@@ -86,15 +156,15 @@ export async function installClaudeMcp(targetDir) {
|
|
|
86
156
|
let mcpCreated = false;
|
|
87
157
|
try {
|
|
88
158
|
await fs.access(mcpConfigPath);
|
|
89
|
-
// Already exists — merge agentvibes
|
|
159
|
+
// Already exists — merge / upgrade the agentvibes entry. This also
|
|
160
|
+
// STRIPS any stale AGENTVIBES_LLM env block left over from v5.1.2..4
|
|
161
|
+
// so Copilot CLI stops mis-routing.
|
|
90
162
|
try {
|
|
91
163
|
const existing = JSON.parse(await fs.readFile(mcpConfigPath, 'utf8'));
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
mcpCreated = true;
|
|
97
|
-
}
|
|
164
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
165
|
+
existing.mcpServers.agentvibes = { ...agentvibesServer };
|
|
166
|
+
await fs.writeFile(mcpConfigPath, JSON.stringify(existing, null, 2) + '\n');
|
|
167
|
+
mcpCreated = true;
|
|
98
168
|
} catch { /* parse error — don't corrupt */ }
|
|
99
169
|
} catch {
|
|
100
170
|
// File doesn't exist — create it
|
|
@@ -112,6 +182,7 @@ export async function installClaudeMcp(targetDir) {
|
|
|
112
182
|
await installer.copyPluginFiles(targetDir, silentSpinner);
|
|
113
183
|
await installer.copyBmadConfigFiles(targetDir, silentSpinner);
|
|
114
184
|
await installer.copyBackgroundMusicFiles(targetDir, silentSpinner);
|
|
185
|
+
ensureDefaultLlmConfigSync('claude-code', targetDir);
|
|
115
186
|
|
|
116
187
|
return { success: true, mcpCreated };
|
|
117
188
|
} catch (err) {
|
|
@@ -247,6 +318,37 @@ export async function installCopilotMcp(targetDir) {
|
|
|
247
318
|
|
|
248
319
|
mcpConfig.servers.agentvibes = agentvibesServer;
|
|
249
320
|
await fs.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
321
|
+
|
|
322
|
+
// Also write ~/.copilot/mcp-config.json so the GitHub Copilot CLI
|
|
323
|
+
// (different product from VS Code Copilot Chat!) can find the
|
|
324
|
+
// agentvibes MCP server. VS Code reads .vscode/mcp.json, but the
|
|
325
|
+
// CLI reads ONLY from ~/.copilot/mcp-config.json per docs:
|
|
326
|
+
// https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
|
|
327
|
+
try {
|
|
328
|
+
const copilotHome = process.env.COPILOT_HOME ||
|
|
329
|
+
path.join(process.env.USERPROFILE || process.env.HOME || '', '.copilot');
|
|
330
|
+
const copilotMcpPath = path.join(copilotHome, 'mcp-config.json');
|
|
331
|
+
await fs.mkdir(copilotHome, { recursive: true });
|
|
332
|
+
let cliConfig = { mcpServers: {} };
|
|
333
|
+
try {
|
|
334
|
+
const existingCli = await fs.readFile(copilotMcpPath, 'utf8');
|
|
335
|
+
const parsedCli = JSON.parse(existingCli);
|
|
336
|
+
if (parsedCli && typeof parsedCli === 'object') {
|
|
337
|
+
cliConfig = parsedCli;
|
|
338
|
+
if (!cliConfig.mcpServers) cliConfig.mcpServers = {};
|
|
339
|
+
}
|
|
340
|
+
} catch { /* new file */ }
|
|
341
|
+
cliConfig.mcpServers.agentvibes = {
|
|
342
|
+
type: 'local',
|
|
343
|
+
command: 'npx',
|
|
344
|
+
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
|
|
345
|
+
env: { AGENTVIBES_LLM: 'copilot' },
|
|
346
|
+
tools: ['*'],
|
|
347
|
+
};
|
|
348
|
+
await fs.writeFile(copilotMcpPath, JSON.stringify(cliConfig, null, 2) + '\n');
|
|
349
|
+
} catch { /* best effort — CLI might not be installed */ }
|
|
350
|
+
|
|
351
|
+
ensureDefaultLlmConfigSync('copilot', targetDir);
|
|
250
352
|
return { success: true };
|
|
251
353
|
} catch (err) {
|
|
252
354
|
return { success: false, error: err.message };
|
|
@@ -300,6 +402,7 @@ export async function installCodexMcp(targetDir) {
|
|
|
300
402
|
try { existing = await fs.readFile(tomlPath, 'utf8'); } catch { /* new file */ }
|
|
301
403
|
const content = buildCodexToml(existing);
|
|
302
404
|
await fs.writeFile(tomlPath, content);
|
|
405
|
+
ensureDefaultLlmConfigSync('codex', targetDir);
|
|
303
406
|
return { success: true };
|
|
304
407
|
} catch (err) {
|
|
305
408
|
return { success: false, error: err.message };
|