agentvibes 5.1.3 → 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 +212 -64
- package/README.md +16 -1
- package/RELEASE_NOTES.md +77 -0
- package/mcp-server/server.py +23 -16
- package/package.json +1 -1
- package/src/console/tabs/setup-tab.js +69 -12
- package/src/installer.js +29 -25
- 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=""
|
|
@@ -17,18 +17,141 @@ param(
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
# Security: Validate LLM provider name (alphanumeric, hyphens, underscores
|
|
20
|
-
# only)
|
|
20
|
+
# only) -- mirrors play-tts.sh line 92. This prevents weird values from
|
|
21
21
|
# poisoning the audio-effects.cfg lookup or the AGENTVIBES_LLM_KEY env var
|
|
22
22
|
# we export to child scripts. An invalid value is treated as unset rather
|
|
23
23
|
# than aborting, so the script falls back to the default config and the
|
|
24
24
|
# rest of TTS still works.
|
|
25
25
|
if ($llm -and $llm -notmatch '^[a-zA-Z0-9_-]+$') {
|
|
26
|
-
Write-Error "Invalid LLM provider name: '
|
|
26
|
+
Write-Error ("Invalid LLM provider name: '{0}' - must match {1}. Falling back to default config." -f $llm, '^[a-zA-Z0-9_-]+$')
|
|
27
27
|
$llm = ""
|
|
28
28
|
}
|
|
29
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
|
+
|
|
30
153
|
# Configuration paths
|
|
31
|
-
# Priority: CLAUDE_PROJECT_DIR env var
|
|
154
|
+
# Priority: CLAUDE_PROJECT_DIR env var -> script's parent project -> user profile
|
|
32
155
|
# Local project settings ALWAYS override global (~/.claude)
|
|
33
156
|
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
34
157
|
|
|
@@ -58,16 +181,19 @@ if (Test-Path $MuteFile) {
|
|
|
58
181
|
}
|
|
59
182
|
|
|
60
183
|
# Per-LLM config lookup: if --llm is passed, look up llm:<name> in audio-effects.cfg
|
|
61
|
-
# Format: llm:<name>|
|
|
184
|
+
# Format: llm:<name>|REVERB|BG_FILE|BG_VOLUME|VOICE|PRETEXT|ENGINE
|
|
62
185
|
$LlmVoice = ""
|
|
63
186
|
$LlmPretext = ""
|
|
64
187
|
$LlmReverb = ""
|
|
65
188
|
$LlmEngine = ""
|
|
189
|
+
$LlmBgTrack = ""
|
|
190
|
+
$LlmBgVolume = ""
|
|
66
191
|
$ProjectRoot = Split-Path -Parent $ClaudeDir
|
|
67
192
|
$ConfigDir = "$ClaudeDir\config"
|
|
68
193
|
|
|
69
194
|
if ($llm) {
|
|
70
195
|
$llmKey = "llm:$llm"
|
|
196
|
+
$llmKeyPattern = '^' + [regex]::Escape($llmKey) + '\|'
|
|
71
197
|
# Check project-local audio-effects.cfg first, then global
|
|
72
198
|
$cfgPaths = @(
|
|
73
199
|
"$ConfigDir\audio-effects.cfg",
|
|
@@ -76,12 +202,18 @@ if ($llm) {
|
|
|
76
202
|
foreach ($cfgPath in $cfgPaths) {
|
|
77
203
|
if (-not $LlmVoice -and -not $LlmPretext -and (Test-Path $cfgPath)) {
|
|
78
204
|
foreach ($line in (Get-Content $cfgPath)) {
|
|
79
|
-
if ($line -match
|
|
205
|
+
if ($line -match $llmKeyPattern) {
|
|
80
206
|
$parts = $line -split '\|'
|
|
81
|
-
# parts: [0]=key [1]=
|
|
207
|
+
# parts: [0]=key [1]=reverb [2]=bg_file [3]=bg_vol [4]=voice [5]=pretext [6]=engine
|
|
82
208
|
if ($parts.Length -ge 2 -and $parts[1].Trim()) {
|
|
83
209
|
$LlmReverb = $parts[1].Trim()
|
|
84
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
|
+
}
|
|
85
217
|
if ($parts.Length -ge 5 -and $parts[4].Trim()) {
|
|
86
218
|
$LlmVoice = $parts[4].Trim()
|
|
87
219
|
}
|
|
@@ -96,8 +228,21 @@ if ($llm) {
|
|
|
96
228
|
}
|
|
97
229
|
}
|
|
98
230
|
}
|
|
99
|
-
#
|
|
100
|
-
|
|
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) {
|
|
101
246
|
$VoiceOverride = $LlmVoice
|
|
102
247
|
}
|
|
103
248
|
# Export LLM key for child scripts (process-local, not system-wide)
|
|
@@ -105,8 +250,8 @@ if ($llm) {
|
|
|
105
250
|
}
|
|
106
251
|
|
|
107
252
|
# Prepend pretext if configured
|
|
108
|
-
# Priority: LLM-specific pretext
|
|
109
|
-
#
|
|
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
|
|
110
255
|
$Pretext = $LlmPretext
|
|
111
256
|
if (-not $Pretext) {
|
|
112
257
|
$PretextSources = @(
|
|
@@ -132,7 +277,6 @@ if (-not $Pretext) {
|
|
|
132
277
|
if ($Pretext) {
|
|
133
278
|
$Text = "$Pretext, $Text"
|
|
134
279
|
}
|
|
135
|
-
|
|
136
280
|
# Determine active provider
|
|
137
281
|
# LLM-specific engine overrides global provider
|
|
138
282
|
$ActiveProvider = "sapi"
|
|
@@ -194,6 +338,15 @@ if (Test-Path $AgentVibesConfig) {
|
|
|
194
338
|
}
|
|
195
339
|
}
|
|
196
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
|
+
|
|
197
350
|
# Check if reverb is enabled (allowlist validation)
|
|
198
351
|
# LLM-specific reverb overrides global setting
|
|
199
352
|
$ReverbLevel = "off"
|
|
@@ -227,7 +380,7 @@ if ($BgEnabled -or $HasReverb) {
|
|
|
227
380
|
}
|
|
228
381
|
}
|
|
229
382
|
|
|
230
|
-
# Check for pre-synthesized WAV (party mode optimization
|
|
383
|
+
# Check for pre-synthesized WAV (party mode optimization -- synthesis done before mutex acquisition)
|
|
231
384
|
$PreSynthWav = $env:AGENTVIBES_PRESYNTHESIZED_WAV
|
|
232
385
|
$UsePreSynth = $PreSynthWav -and (Test-Path $PreSynthWav) -and
|
|
233
386
|
(Get-Item $PreSynthWav -ErrorAction SilentlyContinue).Length -gt 0
|
|
@@ -245,14 +398,10 @@ if ($UsePreSynth) {
|
|
|
245
398
|
Write-Host "[SYNTH] Using pre-synthesized audio..." -ForegroundColor Cyan
|
|
246
399
|
# If no post-processing needed, play the pre-synth file directly and exit
|
|
247
400
|
if (-not $NeedsPostProcess) {
|
|
248
|
-
$player = $null
|
|
249
401
|
try {
|
|
250
|
-
|
|
251
|
-
$player.PlaySync()
|
|
402
|
+
Invoke-SerializedPlay -WavPath $PreSynthWav
|
|
252
403
|
} catch {
|
|
253
404
|
Write-Host "[WARNING] Pre-synth playback failed: $_" -ForegroundColor Yellow
|
|
254
|
-
} finally {
|
|
255
|
-
if ($player) { $player.Dispose() }
|
|
256
405
|
}
|
|
257
406
|
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
258
407
|
exit 0
|
|
@@ -279,6 +428,25 @@ if ($UsePreSynth) {
|
|
|
279
428
|
Write-Host "$item"
|
|
280
429
|
}
|
|
281
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
|
+
}
|
|
282
450
|
} else {
|
|
283
451
|
if ($VoiceOverride) {
|
|
284
452
|
& $ProviderScript $Text $VoiceOverride
|
|
@@ -298,13 +466,17 @@ if ($UsePreSynth) {
|
|
|
298
466
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
299
467
|
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
300
468
|
|
|
301
|
-
#
|
|
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.
|
|
302
473
|
$AudioDir = "$ClaudeDir\audio"
|
|
303
474
|
$RecentWav = if ($UsePreSynth) {
|
|
304
475
|
Get-Item $PreSynthWav -ErrorAction SilentlyContinue
|
|
476
|
+
} elseif ($FreshSynthFile -and (Test-Path $FreshSynthFile)) {
|
|
477
|
+
Get-Item $FreshSynthFile -ErrorAction SilentlyContinue
|
|
305
478
|
} else {
|
|
306
|
-
|
|
307
|
-
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
479
|
+
$null
|
|
308
480
|
}
|
|
309
481
|
|
|
310
482
|
if ($RecentWav -and $RecentWav.Length -gt 0) {
|
|
@@ -320,7 +492,10 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
320
492
|
default { "" }
|
|
321
493
|
}
|
|
322
494
|
if ($reverbFilter) {
|
|
323
|
-
|
|
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"
|
|
324
499
|
$reverbArgs = "-y -i `"$voicePath`" -af `"$reverbFilter`" `"$reverbedFile`""
|
|
325
500
|
$proc = Start-Process -FilePath "ffmpeg" -ArgumentList $reverbArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
|
|
326
501
|
if ($proc.ExitCode -eq 0 -and (Test-Path $reverbedFile)) {
|
|
@@ -340,7 +515,7 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
340
515
|
if (Test-Path $AudioEffectsCfg) {
|
|
341
516
|
# Try agent-specific config first, then fall back to default
|
|
342
517
|
# Format: AGENT_NAME|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME
|
|
343
|
-
# Lookup order: agent name
|
|
518
|
+
# Lookup order: agent name -> llm:<name> -> default
|
|
344
519
|
$agentName = $env:AGENTVIBES_AGENT_NAME
|
|
345
520
|
$configLine = $null
|
|
346
521
|
|
|
@@ -403,7 +578,14 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
403
578
|
}
|
|
404
579
|
|
|
405
580
|
if (Test-Path $BgTrackPath) {
|
|
406
|
-
|
|
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"
|
|
407
589
|
|
|
408
590
|
try {
|
|
409
591
|
# Get voice duration to calculate total length
|
|
@@ -426,62 +608,28 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
426
608
|
$proc = Start-Process -FilePath "ffmpeg" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
|
|
427
609
|
|
|
428
610
|
if ($proc.ExitCode -eq 0 -and (Test-Path $MixedFile) -and (Get-Item $MixedFile).Length -gt 0) {
|
|
429
|
-
# Play the mixed audio
|
|
430
|
-
$player = $null
|
|
611
|
+
# Play the mixed audio (via serialized mutex)
|
|
431
612
|
try {
|
|
432
|
-
|
|
433
|
-
$player.PlaySync()
|
|
613
|
+
Invoke-SerializedPlay -WavPath $MixedFile
|
|
434
614
|
} catch {
|
|
435
615
|
Write-Host "[WARNING] Mixed playback failed, playing voice only" -ForegroundColor Yellow
|
|
436
|
-
|
|
437
|
-
try {
|
|
438
|
-
$player2 = New-Object System.Media.SoundPlayer $voicePath
|
|
439
|
-
$player2.PlaySync()
|
|
440
|
-
} finally {
|
|
441
|
-
if ($player2) { $player2.Dispose() }
|
|
442
|
-
}
|
|
443
|
-
} finally {
|
|
444
|
-
if ($player) { $player.Dispose() }
|
|
616
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
445
617
|
}
|
|
446
618
|
} else {
|
|
447
619
|
# Mixing failed, play voice only
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
451
|
-
$player.PlaySync()
|
|
452
|
-
} finally {
|
|
453
|
-
if ($player) { $player.Dispose() }
|
|
454
|
-
}
|
|
620
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
455
621
|
}
|
|
456
622
|
} catch {
|
|
457
623
|
# ffmpeg failed, play voice only
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
461
|
-
$player.PlaySync()
|
|
462
|
-
} finally {
|
|
463
|
-
if ($player) { $player.Dispose() }
|
|
464
|
-
}
|
|
624
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
465
625
|
}
|
|
466
626
|
} else {
|
|
467
627
|
# No background track found, play voice only
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
471
|
-
$player.PlaySync()
|
|
472
|
-
} finally {
|
|
473
|
-
if ($player) { $player.Dispose() }
|
|
474
|
-
}
|
|
628
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
475
629
|
}
|
|
476
630
|
} else {
|
|
477
631
|
# No background music, play the (possibly reverbed) voice
|
|
478
|
-
|
|
479
|
-
try {
|
|
480
|
-
$player = New-Object System.Media.SoundPlayer $voicePath
|
|
481
|
-
$player.PlaySync()
|
|
482
|
-
} finally {
|
|
483
|
-
if ($player) { $player.Dispose() }
|
|
484
|
-
}
|
|
632
|
+
Invoke-SerializedPlay -WavPath $voicePath
|
|
485
633
|
}
|
|
486
634
|
}
|
|
487
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,21 @@ 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
|
+
|
|
46
61
|
## 🛡️ v5.1.3 — Hardening Pass (Adversarial Review Followup)
|
|
47
62
|
|
|
48
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.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
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
|
+
|
|
3
80
|
## 🛡️ v5.1.3 — Hardening Pass (Adversarial Review Followup)
|
|
4
81
|
|
|
5
82
|
**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,29 +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"
|
|
203
|
+
#
|
|
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.
|
|
205
211
|
#
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
# underscore only. This prevents weird values from poisoning
|
|
210
|
-
# the audio-effects.cfg lookup or being forwarded as ambiguous
|
|
211
|
-
# arguments to the child shell.
|
|
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.
|
|
212
215
|
import re as _re
|
|
213
216
|
llm_key = os.environ.get("AGENTVIBES_LLM", "").strip()
|
|
214
217
|
if llm_key and not _re.match(r"^[a-zA-Z0-9_-]+$", llm_key):
|
|
215
|
-
# Invalid value — log to stderr and treat as unset
|
|
216
218
|
print(
|
|
217
219
|
f"[AgentVibes] WARN: Ignoring invalid AGENTVIBES_LLM='{llm_key}' "
|
|
218
|
-
"(must match ^[a-zA-Z0-9_-]+$); falling back to
|
|
220
|
+
"(must match ^[a-zA-Z0-9_-]+$); falling back to auto-detect",
|
|
219
221
|
file=__import__('sys').stderr,
|
|
220
222
|
)
|
|
221
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"
|
|
222
229
|
tts_script = "play-tts.ps1" if self.is_windows else "play-tts.sh"
|
|
223
230
|
play_tts = self.hooks_dir / tts_script
|
|
224
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,10 +4323,13 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4320
4323
|
}
|
|
4321
4324
|
|
|
4322
4325
|
if (mcpExists) {
|
|
4323
|
-
// Existing config:
|
|
4324
|
-
//
|
|
4325
|
-
//
|
|
4326
|
-
//
|
|
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.
|
|
4327
4333
|
let migrated = false;
|
|
4328
4334
|
let migrationError = null;
|
|
4329
4335
|
try {
|
|
@@ -4334,20 +4340,21 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4334
4340
|
existingCfg.mcpServers = {};
|
|
4335
4341
|
}
|
|
4336
4342
|
const current = existingCfg.mcpServers.agentvibes;
|
|
4337
|
-
const
|
|
4338
|
-
|
|
4339
|
-
// is missing OR its env doesn't include the AGENTVIBES_LLM key.
|
|
4340
|
-
const needsWrite =
|
|
4341
|
-
!current ||
|
|
4342
|
-
!current.env ||
|
|
4343
|
-
current.env.AGENTVIBES_LLM !== 'claude-code';
|
|
4343
|
+
const hasStaleEnv = current?.env?.AGENTVIBES_LLM !== undefined;
|
|
4344
|
+
const needsWrite = !current || hasStaleEnv;
|
|
4344
4345
|
if (needsWrite) {
|
|
4345
|
-
|
|
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 = {
|
|
4346
4351
|
command: 'npx',
|
|
4347
4352
|
args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
|
|
4348
|
-
// Preserve any other env keys the user added manually.
|
|
4349
|
-
env: { ...(current?.env ?? {}), ...wantEnv },
|
|
4350
4353
|
};
|
|
4354
|
+
if (Object.keys(cleanEnv).length > 0) {
|
|
4355
|
+
newEntry.env = cleanEnv;
|
|
4356
|
+
}
|
|
4357
|
+
existingCfg.mcpServers.agentvibes = newEntry;
|
|
4351
4358
|
await fs.writeFile(mcpConfigPath, JSON.stringify(existingCfg, null, 2) + '\n');
|
|
4352
4359
|
migrated = true;
|
|
4353
4360
|
}
|
|
@@ -4360,9 +4367,8 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4360
4367
|
console.log(
|
|
4361
4368
|
boxen(
|
|
4362
4369
|
chalk.green.bold('✅ MCP Configuration Updated\n\n') +
|
|
4363
|
-
chalk.white('Your existing ') + chalk.cyan('.mcp.json') + chalk.white(' has been updated
|
|
4364
|
-
chalk.white('
|
|
4365
|
-
chalk.cyan('AGENTVIBES_LLM=claude-code') + chalk.white(' env var for per-LLM routing.'),
|
|
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.'),
|
|
4366
4372
|
{
|
|
4367
4373
|
padding: 1,
|
|
4368
4374
|
margin: { top: 1, bottom: 1, left: 0, right: 0 },
|
|
@@ -4397,8 +4403,7 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4397
4403
|
console.log(
|
|
4398
4404
|
'\n"agentvibes": {\n' +
|
|
4399
4405
|
' "command": "npx",\n' +
|
|
4400
|
-
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]
|
|
4401
|
-
' "env": { "AGENTVIBES_LLM": "claude-code" }\n' +
|
|
4406
|
+
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]\n' +
|
|
4402
4407
|
'}\n'
|
|
4403
4408
|
);
|
|
4404
4409
|
|
|
@@ -4503,8 +4508,7 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4503
4508
|
' "mcpServers": {\n' +
|
|
4504
4509
|
' "agentvibes": {\n' +
|
|
4505
4510
|
' "command": "npx",\n' +
|
|
4506
|
-
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]
|
|
4507
|
-
' "env": { "AGENTVIBES_LLM": "claude-code" }\n' +
|
|
4511
|
+
' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]\n' +
|
|
4508
4512
|
' }\n' +
|
|
4509
4513
|
' }\n' +
|
|
4510
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 };
|