agentvibes 5.7.7 ā 5.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/config.json +0 -2
- package/.claude/config/audio-effects.cfg +4 -4
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/play-tts-piper.sh +20 -13
- package/.claude/hooks/play-tts-ssh-remote.sh +2 -2
- package/.claude/hooks/voice-manager.sh +6 -0
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
- package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
- package/.mcp.json +13 -9
- package/README.md +33 -2
- package/RELEASE_NOTES.md +80 -0
- package/mcp-server/server.py +17 -7
- package/package.json +2 -2
- package/src/commands/install-mcp.js +270 -16
- package/src/console/app.js +3 -3
- package/src/console/audio-env.js +4 -1
- package/src/console/tabs/agents-tab.js +89 -66
- package/src/console/tabs/music-tab.js +4 -3
- package/src/console/tabs/receiver-tab.js +13 -13
- package/src/console/tabs/settings-tab.js +2 -2
- package/src/console/tabs/setup-tab.js +291 -47
- package/src/console/tabs/voices-tab.js +17 -5
- package/src/console/widgets/personality-picker.js +2 -2
- package/src/console/widgets/reverb-picker.js +1 -1
- package/src/installer.js +32 -27
- package/src/services/provider-service.js +1 -1
- package/src/services/tts-engine-service.js +2 -2
- package/src/utils/audio-duration-validator.js +2 -2
- package/src/utils/list-formatter.js +9 -3
- package/src/utils/platform-resolver.js +369 -0
- package/src/utils/provider-validator.js +9 -9
- package/.agentvibes/install-manifest.json +0 -442
- package/.claude/config/background-music-position.txt +0 -27
- package/.claude/config/background-music-volume.txt +0 -1
- package/.claude/config/background-music.cfg +0 -1
- package/.claude/config/background-music.txt +0 -1
- package/.claude/config/reverb-level.txt +0 -1
- package/.claude/config/tts-speech-rate.txt +0 -1
- package/.claude/config/tts-verbosity.txt +0 -1
- package/.claude/hooks/bmad-party-manager.sh +0 -225
- package/.claude/hooks/stop.sh +0 -38
- package/.claude/piper-voices-dir.txt +0 -1
- /package/.claude/audio/tracks/{CelestialVelvet.mp3 ā celestial_velvet.mp3} +0 -0
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## š§ v5.9.0 ā SSH Remote + Windows Home Directory Fixes
|
|
4
|
+
|
|
5
|
+
**Released:** 2026-05-18
|
|
6
|
+
|
|
7
|
+
### š SSH Remote: Connection Timeout Added
|
|
8
|
+
|
|
9
|
+
The SSH remote transport could hang indefinitely if the remote host was unreachable or
|
|
10
|
+
slow to respond. A `ConnectTimeout=10` option is now applied to all SSH connections, so
|
|
11
|
+
a stuck session surfaces an error within 10 seconds instead of blocking forever.
|
|
12
|
+
|
|
13
|
+
The SSH subshell structure was also cleaned up so the process exit code is reliably
|
|
14
|
+
captured ā a previous formatting issue could cause `wait` to report "pid N is not a
|
|
15
|
+
child of this shell" in some shell environments.
|
|
16
|
+
|
|
17
|
+
### š Windows: Home Directory Detection Fixed
|
|
18
|
+
|
|
19
|
+
`detectRemoteLlm()` used `process.env.HOME || os.homedir()` to find the AgentVibes
|
|
20
|
+
config directory. On Windows, `HOME` is typically unset, but `||` would fall through to
|
|
21
|
+
`os.homedir()` correctly ā however `??` (null-coalescing) is strictly safer since it
|
|
22
|
+
only falls back on `null`/`undefined`, not on an empty string. The fix also adds test
|
|
23
|
+
injectability: passing a fake `HOME` in tests now reliably overrides the system value on
|
|
24
|
+
all platforms.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## šø v5.8.0 ā Soprano Now Works + Voice Picker Fixed for All Engines
|
|
29
|
+
|
|
30
|
+
**Released:** 2026-05-18
|
|
31
|
+
|
|
32
|
+
### š Soprano TTS Was Broken ā Now Fixed
|
|
33
|
+
|
|
34
|
+
Soprano (our 80M-parameter neural TTS engine, introduced in v5.6) was silently failing on
|
|
35
|
+
Windows. Several issues combined to break it end-to-end:
|
|
36
|
+
|
|
37
|
+
- The Windows voice picker showed Soprano as an option but launched it with the wrong binary
|
|
38
|
+
name (`soprano-tts` instead of `soprano`)
|
|
39
|
+
- `play-tts-soprano.ps1` was called from Node.js with a stripped PATH, so the `soprano`
|
|
40
|
+
and `soprano-webui` executables couldn't be found even when installed
|
|
41
|
+
- The wav file path was written to PowerShell's Information stream (`Write-Host`) instead
|
|
42
|
+
of stdout, so the reverb/background-music processor couldn't find it and exited with an error
|
|
43
|
+
- The Gradio WebUI was never auto-started ā you had to manually run `soprano-webui` before
|
|
44
|
+
every session
|
|
45
|
+
|
|
46
|
+
All of these are now fixed. AgentVibes auto-detects whether the Soprano WebUI server is
|
|
47
|
+
running on port 7860, starts it if not, and polls until it's ready (up to 90 seconds).
|
|
48
|
+
Three modes work in priority order: WebUI (fastest ā model stays loaded) ā OpenAI-compatible
|
|
49
|
+
API ā direct `soprano` CLI.
|
|
50
|
+
|
|
51
|
+
### š Voice Picker Ignored Windows SAPI and macOS Say
|
|
52
|
+
|
|
53
|
+
When opening the voice picker for an LLM configured to use **Windows SAPI** or **macOS Say**,
|
|
54
|
+
the picker displayed the full list of Piper voices instead of the engine's built-in voice.
|
|
55
|
+
This was confusing ā selecting a Piper voice while using SAPI or macOS Say had no effect,
|
|
56
|
+
and the Space-bar preview played through the wrong engine.
|
|
57
|
+
|
|
58
|
+
The picker now adapts to whichever engine is selected:
|
|
59
|
+
|
|
60
|
+
- **Windows SAPI / macOS Say / Soprano:** shows exactly one item (the engine's built-in voice),
|
|
61
|
+
auto-selects it, and the Space-bar preview speaks through the correct engine binary
|
|
62
|
+
- **Piper:** shows the full installed-voice catalog as before
|
|
63
|
+
|
|
64
|
+
Additionally, saving the config no longer silently overwrites the `ttsEngine` field to `piper`
|
|
65
|
+
when a native engine is in use.
|
|
66
|
+
|
|
67
|
+
### š Soprano Reliability (9 Adversarial-Review Fixes)
|
|
68
|
+
|
|
69
|
+
- **Crash fix:** socket `destroy()` could emit a late `error` event with no listener,
|
|
70
|
+
crashing the Node.js process ā an absorber handler is now in place
|
|
71
|
+
- **Loop cancellation:** the 90-second WebUI polling loop now stops immediately when
|
|
72
|
+
the modal or voice picker is closed (via AbortController)
|
|
73
|
+
- **No unhandled rejections:** `.catch()` handlers added to all async WebUI-check calls
|
|
74
|
+
- **No duplicate processes:** a 10-second cooldown prevents spawning two `soprano-webui`
|
|
75
|
+
instances when Preview is clicked rapidly
|
|
76
|
+
- **Better error feedback:** spawn failures and non-zero exit codes now surface a visible
|
|
77
|
+
error label in the voice picker instead of silently resetting
|
|
78
|
+
- **PATH preserved:** the PowerShell PATH refresh now appends registry entries rather than
|
|
79
|
+
replacing the whole PATH, so nvm, conda, and pyenv shims continue to work
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
3
83
|
## š v5.7.7 ā Party Mode Voice Restore + Polish
|
|
4
84
|
|
|
5
85
|
**Released:** 2026-05-17
|
package/mcp-server/server.py
CHANGED
|
@@ -71,6 +71,7 @@ class AgentVibesServer:
|
|
|
71
71
|
"""Initialize the AgentVibes MCP server"""
|
|
72
72
|
# Detect native Windows (not WSL)
|
|
73
73
|
self.is_windows = platform.system() == "Windows" and not os.environ.get("WSL_DISTRO_NAME")
|
|
74
|
+
self.is_darwin = platform.system() == "Darwin"
|
|
74
75
|
|
|
75
76
|
# Script name constants ā Windows uses .ps1, Unix uses .sh
|
|
76
77
|
if self.is_windows:
|
|
@@ -1167,15 +1168,24 @@ class AgentVibesServer:
|
|
|
1167
1168
|
if (cwd / ".claude").is_dir() and cwd != self.agentvibes_root:
|
|
1168
1169
|
env["CLAUDE_PROJECT_DIR"] = str(cwd)
|
|
1169
1170
|
|
|
1170
|
-
#
|
|
1171
|
+
# Augment PATH with platform-specific binary locations (Unix only).
|
|
1172
|
+
# MCP servers launched by Claude Desktop inherit a sanitized launchd/dbus PATH
|
|
1173
|
+
# that omits Homebrew (Mac) and pipx (all POSIX) locations.
|
|
1171
1174
|
if not self.is_windows:
|
|
1172
1175
|
home_dir = Path.home()
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1176
|
+
extra_paths = [
|
|
1177
|
+
str(home_dir / ".local" / "bin"),
|
|
1178
|
+
str(home_dir / ".local" / "share" / "pipx" / "venvs" / "piper-tts" / "bin"),
|
|
1179
|
+
]
|
|
1180
|
+
# Mac: add Homebrew prefix for both Apple Silicon (/opt/homebrew) and Intel (/usr/local)
|
|
1181
|
+
if self.is_darwin:
|
|
1182
|
+
extra_paths = ["/opt/homebrew/bin", "/usr/local/bin"] + extra_paths
|
|
1183
|
+
|
|
1184
|
+
current_path = env.get("PATH", "")
|
|
1185
|
+
path_parts = current_path.split(os.pathsep) if current_path else []
|
|
1186
|
+
new_dirs = [p for p in extra_paths if p not in path_parts]
|
|
1187
|
+
if new_dirs:
|
|
1188
|
+
env["PATH"] = os.pathsep.join(new_dirs) + os.pathsep + current_path
|
|
1179
1189
|
|
|
1180
1190
|
return env
|
|
1181
1191
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "agentvibes",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.10.1",
|
|
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": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"test:syntax": "node -c src/installer.js && node -c mcp-server/install-deps.js",
|
|
84
84
|
"test:bats": "AGENTVIBES_TEST_MODE=true bats test/unit/*.bats",
|
|
85
85
|
"test:node": "node --test test/unit/*.test.js",
|
|
86
|
-
"test:coverage": "c8 --reporter=lcov --reporter=text node --test test/unit/*.test.js",
|
|
86
|
+
"test:coverage": "c8 --reporter=lcov --reporter=text node --experimental-test-module-mocks --test-force-exit --test test/unit/*.test.js",
|
|
87
87
|
"test:verbose": "AGENTVIBES_TEST_MODE=true bats -t test/unit/*.bats"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
@@ -16,6 +16,99 @@ import ora from 'ora';
|
|
|
16
16
|
import boxen from 'boxen';
|
|
17
17
|
import { checkDependencies, displayMissingDependencies } from '../utils/dependency-checker.js';
|
|
18
18
|
|
|
19
|
+
// āāā Platform helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
20
|
+
|
|
21
|
+
function commandExists(cmd) {
|
|
22
|
+
try {
|
|
23
|
+
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
24
|
+
execFileSync(finder, [cmd], { stdio: 'pipe' });
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function brewExists() {
|
|
32
|
+
return commandExists('brew');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add Homebrew bin dirs to the current process's PATH so newly installed
|
|
36
|
+
// binaries are discoverable without restarting the shell.
|
|
37
|
+
function augmentPathForBrew() {
|
|
38
|
+
const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
|
|
39
|
+
const existing = new Set((process.env.PATH || '').split(path.delimiter));
|
|
40
|
+
const toAdd = brewPaths.filter(p => !existing.has(p));
|
|
41
|
+
if (toAdd.length > 0) {
|
|
42
|
+
process.env.PATH = [...toAdd, process.env.PATH].join(path.delimiter);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Install pipx via the platform package manager and augment PATH afterward.
|
|
47
|
+
async function ensurePipx(platform) {
|
|
48
|
+
if (platform === 'darwin') {
|
|
49
|
+
if (!brewExists()) {
|
|
50
|
+
console.log(chalk.yellow('Homebrew not found. Install it from https://brew.sh, then re-run.\n'));
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
console.log(chalk.cyan('š¦ Installing pipx via Homebrew...\n'));
|
|
54
|
+
try {
|
|
55
|
+
execFileSync('brew', ['install', 'pipx'], { stdio: 'inherit', env: process.env }); // NOSONAR
|
|
56
|
+
augmentPathForBrew();
|
|
57
|
+
try { execFileSync('pipx', ['ensurepath'], { stdio: 'pipe', env: process.env }); } catch { /* non-fatal */ } // NOSONAR
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
} else if (platform === 'linux') {
|
|
63
|
+
console.log(chalk.cyan('š¦ Installing pipx via apt-get...\n'));
|
|
64
|
+
try {
|
|
65
|
+
execFileSync('sudo', ['apt-get', 'install', '-y', 'pipx'], { stdio: 'inherit', env: process.env }); // NOSONAR
|
|
66
|
+
// Augment PATH with ~/.local/bin so pipx and piper are findable in this process
|
|
67
|
+
const localBin = path.join(os.homedir(), '.local', 'bin');
|
|
68
|
+
const existingParts = new Set((process.env.PATH || '').split(path.delimiter));
|
|
69
|
+
if (!existingParts.has(localBin)) {
|
|
70
|
+
process.env.PATH = localBin + path.delimiter + process.env.PATH;
|
|
71
|
+
}
|
|
72
|
+
try { execFileSync('pipx', ['ensurepath'], { stdio: 'pipe', env: process.env }); } catch { /* non-fatal */ } // NOSONAR
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Auto-install missing optional deps (ffmpeg, sox, pipx, etc.) via brew / apt.
|
|
82
|
+
async function autoInstallOptionalDeps(missing, platform) {
|
|
83
|
+
const installable = ['ffmpeg', 'sox', 'pipx', 'flock', 'curl', 'bc'];
|
|
84
|
+
const toInstall = installable.filter(dep => missing[dep]);
|
|
85
|
+
if (toInstall.length === 0) return;
|
|
86
|
+
|
|
87
|
+
if (platform === 'darwin') {
|
|
88
|
+
const brewMap = { ffmpeg: 'ffmpeg', sox: 'sox', pipx: 'pipx', flock: 'util-linux', curl: 'curl', bc: 'bc' };
|
|
89
|
+
const packages = toInstall.map(d => brewMap[d]).filter(Boolean);
|
|
90
|
+
if (packages.length === 0) return;
|
|
91
|
+
console.log(chalk.cyan(`\nš¦ Homebrew: brew install ${packages.join(' ')}\n`));
|
|
92
|
+
try {
|
|
93
|
+
execFileSync('brew', ['install', ...packages], { stdio: 'inherit', env: process.env }); // NOSONAR
|
|
94
|
+
augmentPathForBrew();
|
|
95
|
+
} catch {
|
|
96
|
+
console.log(chalk.yellow('ā ļø Some Homebrew packages may have failed ā continuing\n'));
|
|
97
|
+
}
|
|
98
|
+
} else if (platform === 'linux') {
|
|
99
|
+
const aptMap = { ffmpeg: ['ffmpeg'], sox: ['sox', 'libsox-fmt-mp3'], pipx: ['pipx'], flock: ['util-linux'], curl: ['curl'], bc: ['bc'] };
|
|
100
|
+
const packages = toInstall.flatMap(d => aptMap[d] || []);
|
|
101
|
+
if (packages.length === 0) return;
|
|
102
|
+
console.log(chalk.cyan(`\nš¦ apt-get: sudo apt-get install -y ${packages.join(' ')}\n`));
|
|
103
|
+
try {
|
|
104
|
+
execFileSync('sudo', ['apt-get', 'update', '-qq'], { stdio: 'pipe', env: process.env }); // NOSONAR
|
|
105
|
+
execFileSync('sudo', ['apt-get', 'install', '-y', ...packages], { stdio: 'inherit', env: process.env }); // NOSONAR
|
|
106
|
+
} catch {
|
|
107
|
+
console.log(chalk.yellow('ā ļø Some apt packages may have failed ā continuing\n'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
19
112
|
/**
|
|
20
113
|
* Check if WSL is installed on Windows
|
|
21
114
|
*/
|
|
@@ -163,26 +256,37 @@ function updateClaudeConfig(agentVibesPath, provider, apiKey = null) {
|
|
|
163
256
|
* Install Piper TTS
|
|
164
257
|
*/
|
|
165
258
|
async function installPiper(useWSL = false) {
|
|
166
|
-
const
|
|
259
|
+
const platform = os.platform();
|
|
167
260
|
|
|
261
|
+
// Ensure pipx is present before attempting piper-tts install
|
|
262
|
+
if (!useWSL && !commandExists('pipx')) {
|
|
263
|
+
console.log(chalk.yellow('\nā ļø pipx not found ā installing it first...\n'));
|
|
264
|
+
const ok = await ensurePipx(platform);
|
|
265
|
+
if (!ok) {
|
|
266
|
+
console.log(chalk.red('ā Could not install pipx automatically.'));
|
|
267
|
+
if (platform === 'darwin') {
|
|
268
|
+
console.log(chalk.cyan(' brew install pipx'));
|
|
269
|
+
} else {
|
|
270
|
+
console.log(chalk.cyan(' sudo apt install pipx'));
|
|
271
|
+
}
|
|
272
|
+
console.log(chalk.gray(' Then re-run this installer.\n'));
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
console.log(chalk.green('ā pipx ready\n'));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(chalk.cyan('\nš¦ Installing Piper TTS via pipx...\n'));
|
|
168
279
|
try {
|
|
169
|
-
// Security:
|
|
280
|
+
// Security: execFileSync with array args prevents command injection
|
|
170
281
|
if (useWSL) {
|
|
171
282
|
execFileSync('wsl', ['pipx', 'install', 'piper-tts'], { stdio: 'inherit' });
|
|
172
283
|
} else {
|
|
173
|
-
execFileSync('pipx', ['install', 'piper-tts'], { stdio: 'inherit' });
|
|
284
|
+
execFileSync('pipx', ['install', 'piper-tts'], { stdio: 'inherit', env: process.env }); // NOSONAR
|
|
174
285
|
}
|
|
175
|
-
|
|
286
|
+
console.log(chalk.green('\nā
Piper TTS installed!\n'));
|
|
176
287
|
return true;
|
|
177
288
|
} catch (error) {
|
|
178
|
-
|
|
179
|
-
console.error(chalk.yellow('\nā ļø You may need to install pipx first:'));
|
|
180
|
-
if (useWSL) {
|
|
181
|
-
console.log(chalk.cyan(' wsl sudo apt install pipx'));
|
|
182
|
-
} else {
|
|
183
|
-
console.log(chalk.cyan(' brew install pipx (macOS)'));
|
|
184
|
-
console.log(chalk.cyan(' sudo apt install pipx (Linux)'));
|
|
185
|
-
}
|
|
289
|
+
console.log(chalk.red(`\nā Failed to install Piper TTS: ${error.message}\n`));
|
|
186
290
|
return false;
|
|
187
291
|
}
|
|
188
292
|
}
|
|
@@ -227,6 +331,7 @@ async function checkSystemDependencies() {
|
|
|
227
331
|
return;
|
|
228
332
|
}
|
|
229
333
|
|
|
334
|
+
const platform = os.platform();
|
|
230
335
|
const hasCoreMissing = depResults.missing.node || depResults.missing.python || depResults.missing.bash;
|
|
231
336
|
|
|
232
337
|
if (hasCoreMissing) {
|
|
@@ -234,7 +339,35 @@ async function checkSystemDependencies() {
|
|
|
234
339
|
process.exit(1);
|
|
235
340
|
}
|
|
236
341
|
|
|
237
|
-
//
|
|
342
|
+
// Optional deps missing ā offer auto-install on Mac/Linux
|
|
343
|
+
const canAutoInstall = (platform === 'darwin' && brewExists()) || platform === 'linux';
|
|
344
|
+
|
|
345
|
+
if (canAutoInstall) {
|
|
346
|
+
const mgr = platform === 'darwin' ? 'Homebrew' : 'apt-get';
|
|
347
|
+
const { doInstall } = await inquirer.prompt([{
|
|
348
|
+
type: 'confirm',
|
|
349
|
+
name: 'doInstall',
|
|
350
|
+
message: `Install missing optional dependencies now using ${mgr}?`,
|
|
351
|
+
default: true
|
|
352
|
+
}]);
|
|
353
|
+
|
|
354
|
+
if (doInstall) {
|
|
355
|
+
await autoInstallOptionalDeps(depResults.missing, platform);
|
|
356
|
+
const recheckResults = checkDependencies();
|
|
357
|
+
const stillMissing = Object.values(recheckResults.missing).some(Boolean);
|
|
358
|
+
if (stillMissing) {
|
|
359
|
+
console.log(chalk.yellow('ā ļø Some dependencies could not be installed automatically.\n'));
|
|
360
|
+
displayMissingDependencies(recheckResults);
|
|
361
|
+
} else {
|
|
362
|
+
console.log(chalk.green('ā All optional dependencies installed\n'));
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
} else if (platform === 'darwin' && !brewExists()) {
|
|
367
|
+
console.log(chalk.yellow('ā¹ļø Homebrew not found ā install it from https://brew.sh to get ffmpeg and other tools automatically.\n'));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Fall through: ask to continue without the missing deps
|
|
238
371
|
const { continueAnyway } = await inquirer.prompt([{
|
|
239
372
|
type: 'confirm',
|
|
240
373
|
name: 'continueAnyway',
|
|
@@ -243,7 +376,7 @@ async function checkSystemDependencies() {
|
|
|
243
376
|
}]);
|
|
244
377
|
|
|
245
378
|
if (!continueAnyway) {
|
|
246
|
-
console.log(chalk.yellow('\nInstallation cancelled.
|
|
379
|
+
console.log(chalk.yellow('\nInstallation cancelled. Install the missing dependencies and try again.\n'));
|
|
247
380
|
process.exit(0);
|
|
248
381
|
}
|
|
249
382
|
}
|
|
@@ -389,6 +522,121 @@ async function setupPythonDependencies(isWindows) {
|
|
|
389
522
|
}
|
|
390
523
|
}
|
|
391
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Interactive voice download step shown after Piper installs.
|
|
527
|
+
* Offers starter, libritts-900, full pack, or skip.
|
|
528
|
+
* @param {string} agentVibesDir - Path to the AgentVibes installation directory
|
|
529
|
+
* @returns {Promise<void>}
|
|
530
|
+
*/
|
|
531
|
+
async function downloadPiperVoices(agentVibesDir) {
|
|
532
|
+
console.log(chalk.bold('\nšļø Step 4b: Download Voice Models\n'));
|
|
533
|
+
|
|
534
|
+
// Skip if voices are already present ā respect PIPER_VOICES_DIR override so we
|
|
535
|
+
// check the same directory the runtime will use.
|
|
536
|
+
const homeDir = os.homedir();
|
|
537
|
+
const voiceDir = process.env.PIPER_VOICES_DIR || path.join(homeDir, '.claude', 'piper-voices');
|
|
538
|
+
try {
|
|
539
|
+
if (fs.existsSync(voiceDir)) {
|
|
540
|
+
const onnxFiles = fs.readdirSync(voiceDir).filter(f => f.endsWith('.onnx'));
|
|
541
|
+
// Only count voices where both .onnx and .onnx.json exist and are non-empty
|
|
542
|
+
const completeVoices = onnxFiles.filter(f => {
|
|
543
|
+
try {
|
|
544
|
+
const onnxStat = fs.statSync(path.join(voiceDir, f));
|
|
545
|
+
const jsonStat = fs.statSync(path.join(voiceDir, f + '.json'));
|
|
546
|
+
return onnxStat.size > 0 && jsonStat.size > 0;
|
|
547
|
+
} catch { return false; }
|
|
548
|
+
});
|
|
549
|
+
if (completeVoices.length > 0) {
|
|
550
|
+
console.log(chalk.green(`ā ${completeVoices.length} voice model(s) already installed ā skipping download\n`));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch { /* ignore, fall through to download prompt */ }
|
|
555
|
+
|
|
556
|
+
console.log(chalk.gray('Piper needs at least one voice model to speak.\n'));
|
|
557
|
+
|
|
558
|
+
const { voiceChoice } = await inquirer.prompt([{
|
|
559
|
+
type: 'list',
|
|
560
|
+
name: 'voiceChoice',
|
|
561
|
+
message: 'Which voices would you like to download?',
|
|
562
|
+
choices: [
|
|
563
|
+
{
|
|
564
|
+
name: chalk.cyan('Starter voice') + chalk.gray(' ā en_US-lessac-medium (~13 MB, download in seconds)'),
|
|
565
|
+
value: 'starter',
|
|
566
|
+
short: 'Starter'
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: chalk.cyan('LibriTTS 900-speaker pack') + chalk.gray(' ā en_US-libritts-high (~57 MB, 900 unique named speakers)'),
|
|
570
|
+
value: 'libritts',
|
|
571
|
+
short: 'LibriTTS 900'
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: chalk.cyan('Full pack') + chalk.gray(' ā 11 voices including LibriTTS (~250 MB, all BMAD agent voices)'),
|
|
575
|
+
value: 'full',
|
|
576
|
+
short: 'Full pack'
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: chalk.gray('Skip ā download voices later with /agent-vibes:add'),
|
|
580
|
+
value: 'skip',
|
|
581
|
+
short: 'Skip'
|
|
582
|
+
}
|
|
583
|
+
]
|
|
584
|
+
}]);
|
|
585
|
+
|
|
586
|
+
if (voiceChoice === 'skip') {
|
|
587
|
+
console.log(chalk.yellow('\nā ļø No voices downloaded.'));
|
|
588
|
+
console.log(chalk.gray(' Download voices anytime with: ') + chalk.cyan('/agent-vibes:add\n'));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const voiceManagerScript = path.join(agentVibesDir, '.claude', 'hooks', 'piper-voice-manager.sh');
|
|
593
|
+
const fullPackScript = path.join(agentVibesDir, '.claude', 'hooks', 'piper-download-voices.sh');
|
|
594
|
+
|
|
595
|
+
if (voiceChoice === 'starter') {
|
|
596
|
+
console.log(chalk.cyan('\nš„ Downloading en_US-lessac-medium (~13 MB)...\n'));
|
|
597
|
+
try {
|
|
598
|
+
// Use positional params ($1/$2) so the script path is never string-interpolated
|
|
599
|
+
execFileSync('bash', ['-c', 'source "$1" && download_voice "$2"', '--', voiceManagerScript, 'en_US-lessac-medium'], { // NOSONAR
|
|
600
|
+
stdio: 'inherit',
|
|
601
|
+
env: process.env
|
|
602
|
+
});
|
|
603
|
+
console.log(chalk.green('\nā
Starter voice ready!\n'));
|
|
604
|
+
} catch {
|
|
605
|
+
console.log(chalk.yellow('\nā ļø Download failed ā try later with: /agent-vibes:add\n'));
|
|
606
|
+
}
|
|
607
|
+
} else if (voiceChoice === 'libritts') {
|
|
608
|
+
console.log(chalk.cyan('\nš„ Downloading LibriTTS 900-speaker pack (~57 MB)...\n'));
|
|
609
|
+
console.log(chalk.gray(' This gives you 900 individually named speakers to choose from.\n'));
|
|
610
|
+
try {
|
|
611
|
+
execFileSync('bash', ['-c', 'source "$1" && download_voice "$2"', '--', voiceManagerScript, 'en_US-libritts-high'], { // NOSONAR
|
|
612
|
+
stdio: 'inherit',
|
|
613
|
+
env: process.env
|
|
614
|
+
});
|
|
615
|
+
console.log(chalk.green('\nā
LibriTTS 900-speaker pack ready!'));
|
|
616
|
+
console.log(chalk.gray(' Browse speakers with: ') + chalk.cyan('/agent-vibes:list\n'));
|
|
617
|
+
} catch {
|
|
618
|
+
console.log(chalk.yellow('\nā ļø Download failed ā try later with: /agent-vibes:add\n'));
|
|
619
|
+
}
|
|
620
|
+
} else if (voiceChoice === 'full') {
|
|
621
|
+
console.log(chalk.cyan('\nš„ Downloading full voice pack (~250 MB)...\n'));
|
|
622
|
+
console.log(chalk.gray(' This includes all BMAD agent voices plus LibriTTS 900 speakers.\n'));
|
|
623
|
+
try {
|
|
624
|
+
if (fs.existsSync(fullPackScript)) {
|
|
625
|
+
execFileSync('bash', [fullPackScript, '--yes'], { // NOSONAR
|
|
626
|
+
stdio: 'inherit',
|
|
627
|
+
env: process.env
|
|
628
|
+
});
|
|
629
|
+
console.log(chalk.green('\nā
Full voice pack downloaded!\n'));
|
|
630
|
+
} else {
|
|
631
|
+
console.log(chalk.yellow(' Download script not found at: ' + fullPackScript));
|
|
632
|
+
console.log(chalk.yellow(' Try: /agent-vibes:add\n'));
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
console.log(chalk.yellow('\nā ļø Download failed ā try later with: /agent-vibes:add\n'));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
392
640
|
/**
|
|
393
641
|
* Display welcome banner
|
|
394
642
|
* @param {string} platform - Platform name
|
|
@@ -433,7 +681,8 @@ function showSuccessMessage(configPath, provider) {
|
|
|
433
681
|
console.log(chalk.gray(` ${configPath}\n`));
|
|
434
682
|
|
|
435
683
|
if (provider === 'piper') {
|
|
436
|
-
console.log(chalk.gray('
|
|
684
|
+
console.log(chalk.gray('Browse voices: ') + chalk.cyan('/agent-vibes:list') + '\n');
|
|
685
|
+
console.log(chalk.gray('Add more voices: ') + chalk.cyan('/agent-vibes:add') + '\n');
|
|
437
686
|
}
|
|
438
687
|
}
|
|
439
688
|
|
|
@@ -457,6 +706,11 @@ export async function installMCP() {
|
|
|
457
706
|
// Step 4: Choose TTS provider
|
|
458
707
|
const provider = await setupTTSProvider(isWindows);
|
|
459
708
|
|
|
709
|
+
// Step 4b: Download voices if Piper was selected
|
|
710
|
+
if (provider === 'piper') {
|
|
711
|
+
await downloadPiperVoices(agentVibesDir);
|
|
712
|
+
}
|
|
713
|
+
|
|
460
714
|
// Step 5: Install Python dependencies
|
|
461
715
|
await setupPythonDependencies(isWindows);
|
|
462
716
|
|
|
@@ -468,7 +722,7 @@ export async function installMCP() {
|
|
|
468
722
|
|
|
469
723
|
// Step 7: Update Claude Desktop config
|
|
470
724
|
console.log(chalk.bold('š Step 7: Updating Claude Desktop configuration...\n'));
|
|
471
|
-
const configPath = updateClaudeConfig(agentVibesDir, provider
|
|
725
|
+
const configPath = updateClaudeConfig(agentVibesDir, provider);
|
|
472
726
|
console.log(chalk.green(`ā Updated: ${configPath}\n`));
|
|
473
727
|
|
|
474
728
|
// Success!
|
package/src/console/app.js
CHANGED
|
@@ -238,9 +238,9 @@ export class AgentVibesConsole {
|
|
|
238
238
|
// Right-aligned: git remote + branch when available, else AgentVibes repo link
|
|
239
239
|
let topRightContent = `{${BRAND_PINK}-fg}github.com/preibisch/agentvibes{/${BRAND_PINK}-fg}`;
|
|
240
240
|
try {
|
|
241
|
-
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'],
|
|
241
|
+
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], // NOSONAR
|
|
242
242
|
{ encoding: 'utf8', timeout: 2000, cwd });
|
|
243
|
-
const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'],
|
|
243
|
+
const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'], // NOSONAR
|
|
244
244
|
{ encoding: 'utf8', timeout: 2000, cwd });
|
|
245
245
|
if (branchResult.status === 0 && remoteResult.status === 0) {
|
|
246
246
|
const branch = branchResult.stdout.trim();
|
|
@@ -627,7 +627,7 @@ export class AgentVibesConsole {
|
|
|
627
627
|
_createFooter() {
|
|
628
628
|
// Detect installed providers inline (same logic as ProviderService)
|
|
629
629
|
const _has = (bin) => {
|
|
630
|
-
try { execFileSync('which', [bin], { stdio: 'ignore', timeout: 2000 }); return true; }
|
|
630
|
+
try { execFileSync('which', [bin], { stdio: 'ignore', timeout: 2000 }); return true; } // NOSONAR
|
|
631
631
|
catch { return false; }
|
|
632
632
|
};
|
|
633
633
|
const detected = {
|
package/src/console/audio-env.js
CHANGED
|
@@ -121,7 +121,10 @@ function _detect(players, env) {
|
|
|
121
121
|
* @returns {string|null}
|
|
122
122
|
*/
|
|
123
123
|
export function detectRemoteLlm() {
|
|
124
|
-
|
|
124
|
+
// process.env.HOME takes priority over os.homedir() so tests can inject a fake home on all platforms.
|
|
125
|
+
// On Windows production use, HOME is typically unset, so os.homedir() (reads USERPROFILE) is the fallback.
|
|
126
|
+
const homeDir = process.env.HOME ?? os.homedir();
|
|
127
|
+
const cfgPath = path.join(homeDir, '.agentvibes', 'transport-config.json');
|
|
125
128
|
if (!fs.existsSync(cfgPath)) return null;
|
|
126
129
|
try {
|
|
127
130
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|