agentvibes 3.5.6 โ 3.5.8
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/background-music-position.txt +1 -0
- package/.claude/github-star-reminder.txt +1 -1
- package/README.md +11 -9
- package/RELEASE_NOTES.md +70 -0
- package/bin/agent-vibes +21 -26
- package/package.json +1 -1
- package/setup-windows.ps1 +61 -13
- package/src/installer.js +87 -12
- package/src/utils/provider-validator.js +456 -0
|
@@ -24,3 +24,4 @@ agent_vibes_arabic_v2_loop.mp3:.00000000000000000006132724
|
|
|
24
24
|
agent_vibes_chillwave_v2_loop.mp3:14.628390
|
|
25
25
|
agent_vibes_bachata_v1_loop.mp3:.00000000000000000005344000
|
|
26
26
|
agentvibes_soft_flamenco_loop.mp3:.00000000000000000006934441
|
|
27
|
+
agent_vibes_goa_trance_v2_loop.mp3:.00000000000000000002499918
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20260212
|
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**: v3.5.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v3.5.8
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -41,11 +41,11 @@ Whether you're coding in Claude Code, chatting in Claude Desktop, using Warp Ter
|
|
|
41
41
|
### ๐ฏ Key Features
|
|
42
42
|
|
|
43
43
|
**๐ช NEW IN v3.5.5 โ Native Windows Support:**
|
|
44
|
-
- ๐ฅ๏ธ **Windows Native TTS** -
|
|
45
|
-
- ๐ต **Background Music** - 16 genre tracks
|
|
46
|
-
- ๐๏ธ **Reverb & Audio Effects** - 5 reverb levels via ffmpeg
|
|
47
|
-
- ๐ **Verbosity Control** -
|
|
48
|
-
- ๐จ **Beautiful Installer** - `npx agentvibes install`
|
|
44
|
+
- ๐ฅ๏ธ **Windows Native TTS** - Soprano, Piper, and Windows SAPI providers. No WSL required!
|
|
45
|
+
- ๐ต **Background Music** - 16 genre tracks mixed under voice
|
|
46
|
+
- ๐๏ธ **Reverb & Audio Effects** - 5 reverb levels via ffmpeg
|
|
47
|
+
- ๐ **Verbosity Control** - High, Medium, or Low settings
|
|
48
|
+
- ๐จ **Beautiful Installer** - `npx agentvibes install` or `.\setup-windows.ps1`
|
|
49
49
|
|
|
50
50
|
**โก v3.4.0 Highlights:**
|
|
51
51
|
- ๐ค **Soprano TTS Provider** - Ultra-fast neural TTS with 20x CPU, 2000x GPU acceleration (thanks [@nathanchase](https://github.com/nathanchase)!)
|
|
@@ -97,7 +97,7 @@ All 50+ Piper voices AgentVibes provides are sourced from Hugging Face's open-so
|
|
|
97
97
|
- [๐ฑ Android/Termux](#-quick-setup-android--termux-claude-code-on-your-phone) - Run Claude Code on your phone
|
|
98
98
|
- [๐ Prerequisites](#-prerequisites) - What you actually need (Node.js + optional tools)
|
|
99
99
|
- [โจ What is AgentVibes?](#-what-is-agentvibes) - Overview & key features
|
|
100
|
-
- [๐ฐ Latest Release](#-latest-release) - v3.5.
|
|
100
|
+
- [๐ฐ Latest Release](#-latest-release) - v3.5.8 Security & UX + v3.5.5 Native Windows Support
|
|
101
101
|
- [๐ช Windows Setup Guide for Claude Desktop](mcp-server/WINDOWS_SETUP.md) - Complete Windows installation with WSL & Python
|
|
102
102
|
|
|
103
103
|
### AgentVibes MCP (Natural Language Control)
|
|
@@ -141,9 +141,11 @@ All 50+ Piper voices AgentVibes provides are sourced from Hugging Face's open-so
|
|
|
141
141
|
|
|
142
142
|
## ๐ฐ Latest Release
|
|
143
143
|
|
|
144
|
-
**[v3.5.
|
|
144
|
+
**[v3.5.8 - Security & Provider Validation](https://github.com/paulpreibisch/AgentVibes/releases/tag/v3.5.8)** ๐ก๏ธ
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
Critical security update: Fixed command injection vulnerabilities, HOME directory injection prevention, and path traversal protection. Soprano TTS installed via pipx now correctly detected. Enhanced provider detection messaging and debug logging.
|
|
147
|
+
|
|
148
|
+
**Foundation Release:** [v3.5.5 - Native Windows Support](https://github.com/paulpreibisch/AgentVibes/releases/tag/v3.5.5) brings Windows support (Soprano, Piper, SAPI), background music (16 genre tracks), reverb/audio effects, and verbosity control. [See release notes](RELEASE_NOTES.md) for complete v3.5.5-3.5.8 history.
|
|
147
149
|
|
|
148
150
|
๐ก **Tip:** If `npx agentvibes` shows an older version or missing commands, clear your npm cache: `npm cache clean --force && npx agentvibes@latest --help`
|
|
149
151
|
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## ๐ก๏ธ v3.5.8 - Provider Validation Security & UX Improvements
|
|
4
|
+
|
|
5
|
+
**Release Date:** February 12, 2026
|
|
6
|
+
|
|
7
|
+
### ๐ฏ Summary
|
|
8
|
+
|
|
9
|
+
Critical security and reliability update for provider detection. Fixes command injection vulnerabilities in validation code, prevents HOME directory injection attacks, and improves UX with explicit provider detection messaging. Soprano TTS installed via pipx is now correctly detected (previously showed "not installed" due to ES module import error). All 8 critical code review issues resolved with comprehensive security hardening and enhanced error reporting.
|
|
10
|
+
|
|
11
|
+
### โจ Key Improvements
|
|
12
|
+
|
|
13
|
+
- **๐ Security Fixes:** Fixed command injection vulnerability (template strings โ array form), prevented HOME injection attacks, added path traversal protection
|
|
14
|
+
- **โ
Provider Detection:** Soprano via pipx now correctly detected; added checkedLocations tracking for transparency
|
|
15
|
+
- **๐ฌ Better Messaging:** Explicit "Detected and selected!" confirmation; detailed error messages showing what was checked
|
|
16
|
+
- **๐งช Test Coverage:** Enhanced tests verify actual detection values, not just types
|
|
17
|
+
- **๐ Debugging:** Added [DEBUG] logging for troubleshooting provider issues
|
|
18
|
+
|
|
19
|
+
### ๐ด Critical Fixes
|
|
20
|
+
|
|
21
|
+
1. **Command Injection Prevention** - All execSync calls now use array form (security: CLAUDE.md)
|
|
22
|
+
2. **HOME Directory Injection** - Switched to os.homedir() instead of process.env.HOME
|
|
23
|
+
3. **Path Traversal Protection** - Added path.resolve() validation for pipx venv directories
|
|
24
|
+
|
|
25
|
+
### ๐ก Medium Fixes
|
|
26
|
+
|
|
27
|
+
4. **Pipx Logic Improved** - Tracks checked locations even on success (transparency)
|
|
28
|
+
5. **Silent Failures Eliminated** - Added [DEBUG] error logging for diagnostics
|
|
29
|
+
6. **Test Quality Enhanced** - Verify message content, not just types
|
|
30
|
+
7. **Documentation** - Added JSDoc comments explaining security-critical imports
|
|
31
|
+
8. **Error Differentiation** - Better distinction between different failure types
|
|
32
|
+
|
|
33
|
+
### ๐ Technical Impact
|
|
34
|
+
|
|
35
|
+
- Soprano detection now works reliably for both pip and pipx installations
|
|
36
|
+
- Reduced false negatives in provider validation
|
|
37
|
+
- Enhanced security posture aligned with CLAUDE.md security mandates
|
|
38
|
+
- Improved debuggability with explicit error messages
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ๐ง v3.5.7 - CLI Fix: npx Command Output & Startup Hooks
|
|
43
|
+
|
|
44
|
+
**Release Date:** February 12, 2026
|
|
45
|
+
|
|
46
|
+
Fixes critical bug where `npx agent-vibes install` and other commands produced no output, making CLI unusable. Root cause: bin/agent-vibes used dynamic import without passing arguments to installer.js on local execution. Also removed broken hook configurations (pre_compact.py, notification.ts) that didn't exist and caused startup errors in Claude Code settings.
|
|
47
|
+
|
|
48
|
+
### ๐ฏ What's Fixed
|
|
49
|
+
|
|
50
|
+
- **npx agent-vibes now works** - `npx agent-vibes install`, `npx agent-vibes --help`, all commands produce proper output
|
|
51
|
+
- **Startup hook errors gone** - Removed non-existent hook references from settings.json (pre_compact.py, notification.ts)
|
|
52
|
+
- **CLI execution proper** - Both npx and local execution now use execFileSync with proper argument passing
|
|
53
|
+
|
|
54
|
+
### ๐ Technical Details
|
|
55
|
+
|
|
56
|
+
**Before v3.5.7:**
|
|
57
|
+
```javascript
|
|
58
|
+
// bin/agent-vibes (local execution path)
|
|
59
|
+
import('../src/installer.js'); // โ No args, doesn't await
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**After v3.5.7:**
|
|
63
|
+
```javascript
|
|
64
|
+
// bin/agent-vibes (all execution paths)
|
|
65
|
+
execFileSync('node', [installerPath, ...arguments_], {
|
|
66
|
+
stdio: 'inherit',
|
|
67
|
+
cwd: isNpxExecution ? path.dirname(__dirname) : process.cwd(),
|
|
68
|
+
}); // โ
Passes args, proper I/O
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
3
73
|
## ๐ง v3.5.6 - Bug Fix: Bash Hook Parameter Handling
|
|
4
74
|
|
|
5
75
|
**Release Date:** February 11, 2026
|
package/bin/agent-vibes
CHANGED
|
@@ -16,30 +16,25 @@ const __dirname = path.dirname(__filename);
|
|
|
16
16
|
// Check if we're running in an npx temporary directory
|
|
17
17
|
const isNpxExecution = __dirname.includes('_npx') || __dirname.includes('.npm');
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
process.exit(error.status || 1);
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
// Local execution - use installer directly
|
|
44
|
-
import('../src/installer.js');
|
|
19
|
+
// Get CLI arguments
|
|
20
|
+
const arguments_ = process.argv.slice(2);
|
|
21
|
+
|
|
22
|
+
// Use the installer for all commands
|
|
23
|
+
const installerPath = path.join(__dirname, '..', 'src', 'installer.js');
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(installerPath)) {
|
|
26
|
+
console.error('Error: Could not find installer.js at', installerPath);
|
|
27
|
+
console.error('Current directory:', __dirname);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Security: Use execFileSync with array args to prevent command injection
|
|
33
|
+
// Arguments are passed as array elements, not string interpolation
|
|
34
|
+
execFileSync('node', [installerPath, ...arguments_], {
|
|
35
|
+
stdio: 'inherit',
|
|
36
|
+
cwd: isNpxExecution ? path.dirname(__dirname) : process.cwd(),
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
process.exit(error.status || 1);
|
|
45
40
|
}
|
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": "3.5.
|
|
4
|
+
"version": "3.5.8",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
package/setup-windows.ps1
CHANGED
|
@@ -330,6 +330,13 @@ Write-Ok "Installed $CopiedCount TTS scripts"
|
|
|
330
330
|
|
|
331
331
|
Write-Section "Choose Your TTS Provider"
|
|
332
332
|
|
|
333
|
+
# Check if pip is available
|
|
334
|
+
$PipAvailable = $false
|
|
335
|
+
try {
|
|
336
|
+
$pipTest = & pip --version 2>$null
|
|
337
|
+
if ($pipTest) { $PipAvailable = $true }
|
|
338
|
+
} catch {}
|
|
339
|
+
|
|
333
340
|
# Check Soprano availability
|
|
334
341
|
$SopranoAvailable = $false
|
|
335
342
|
try {
|
|
@@ -341,12 +348,14 @@ try {
|
|
|
341
348
|
if ($response.StatusCode -eq 200) { $SopranoAvailable = $true }
|
|
342
349
|
} catch {}
|
|
343
350
|
|
|
344
|
-
# Check if pip has soprano-tts
|
|
351
|
+
# Check if pip has soprano-tts (only if pip is available)
|
|
345
352
|
$SopranoInstalled = $false
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
353
|
+
if ($PipAvailable) {
|
|
354
|
+
try {
|
|
355
|
+
$pipResult = & pip show soprano-tts 2>$null
|
|
356
|
+
if ($pipResult) { $SopranoInstalled = $true }
|
|
357
|
+
} catch {}
|
|
358
|
+
}
|
|
350
359
|
|
|
351
360
|
if (-not $Provider) {
|
|
352
361
|
Write-Host " [1] Soprano (Best Quality)" -ForegroundColor White
|
|
@@ -407,15 +416,54 @@ if ($Provider -eq "soprano") {
|
|
|
407
416
|
Write-Info "Start it with: soprano-tts --share"
|
|
408
417
|
Write-Info "Or run it in WSL and forward port 7860"
|
|
409
418
|
} else {
|
|
410
|
-
Write-Warn "Soprano not detected"
|
|
411
|
-
Write-Host ""
|
|
412
|
-
Write-Host " To install Soprano:" -ForegroundColor White
|
|
413
|
-
Write-Host " pip install soprano-tts" -ForegroundColor Cyan
|
|
419
|
+
Write-Warn "Soprano TTS not detected"
|
|
414
420
|
Write-Host ""
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
421
|
+
|
|
422
|
+
if (-not $PipAvailable) {
|
|
423
|
+
Write-Error "pip is not available on this system"
|
|
424
|
+
Write-Info "Please install Python 3 with pip, then run:"
|
|
425
|
+
Write-Host " pip install soprano-tts" -ForegroundColor Cyan
|
|
426
|
+
Write-Host ""
|
|
427
|
+
} else {
|
|
428
|
+
# Offer to install Soprano
|
|
429
|
+
$installChoice = Read-Host "Would you like to install Soprano now? (y/n, default: y)"
|
|
430
|
+
|
|
431
|
+
if ($installChoice -eq "" -or $installChoice -eq "y" -or $installChoice -eq "Y") {
|
|
432
|
+
Write-Info "Installing Soprano TTS..."
|
|
433
|
+
Write-Host ""
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
& pip install soprano-tts 2>&1 | Tee-Object -Variable pipOutput | Write-Host
|
|
437
|
+
|
|
438
|
+
# Re-check if installation succeeded
|
|
439
|
+
$SopranoInstalled = $false
|
|
440
|
+
try {
|
|
441
|
+
$pipResult = & pip show soprano-tts 2>$null
|
|
442
|
+
if ($pipResult) { $SopranoInstalled = $true }
|
|
443
|
+
} catch {}
|
|
444
|
+
|
|
445
|
+
if ($SopranoInstalled) {
|
|
446
|
+
Write-Ok "Soprano TTS installed successfully!"
|
|
447
|
+
Write-Info "Start it with: soprano-tts --share"
|
|
448
|
+
} else {
|
|
449
|
+
Write-Error "Installation may have failed. Please check the output above."
|
|
450
|
+
Write-Info "You can try installing manually: pip install soprano-tts"
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
Write-Error "Installation failed: $_"
|
|
454
|
+
Write-Info "Please install manually: pip install soprano-tts"
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
Write-Host ""
|
|
458
|
+
Write-Host " To install Soprano manually:" -ForegroundColor White
|
|
459
|
+
Write-Host " pip install soprano-tts" -ForegroundColor Cyan
|
|
460
|
+
Write-Host ""
|
|
461
|
+
Write-Host " Or use Soprano in WSL with port forwarding:" -ForegroundColor White
|
|
462
|
+
Write-Host " ssh -L 7860:localhost:7860 your-wsl-host" -ForegroundColor Cyan
|
|
463
|
+
Write-Host ""
|
|
464
|
+
Write-Info "AgentVibes will work once Soprano is accessible on port 7860"
|
|
465
|
+
}
|
|
466
|
+
}
|
|
419
467
|
}
|
|
420
468
|
}
|
|
421
469
|
|
package/src/installer.js
CHANGED
|
@@ -61,6 +61,12 @@ import {
|
|
|
61
61
|
assignVoice,
|
|
62
62
|
resetBmadVoices,
|
|
63
63
|
} from './commands/bmad-voices.js';
|
|
64
|
+
import {
|
|
65
|
+
validateProvider,
|
|
66
|
+
getProviderInstallCommand,
|
|
67
|
+
getProviderDisplayName,
|
|
68
|
+
attemptProviderInstallation,
|
|
69
|
+
} from './utils/provider-validator.js';
|
|
64
70
|
|
|
65
71
|
const __filename = fileURLToPath(import.meta.url);
|
|
66
72
|
const __dirname = path.dirname(__filename);
|
|
@@ -869,6 +875,78 @@ async function collectConfiguration(options = {}) {
|
|
|
869
875
|
return null;
|
|
870
876
|
}
|
|
871
877
|
|
|
878
|
+
// Validate provider installation before accepting selection
|
|
879
|
+
console.log(chalk.gray(`\n Checking for ${getProviderDisplayName(provider)}...`));
|
|
880
|
+
const validation = await validateProvider(provider);
|
|
881
|
+
|
|
882
|
+
if (!validation.installed) {
|
|
883
|
+
const displayName = getProviderDisplayName(provider);
|
|
884
|
+
console.log(chalk.yellow(`\nโ ๏ธ ${validation.message}`));
|
|
885
|
+
|
|
886
|
+
const { action } = await inquirer.prompt([{
|
|
887
|
+
type: 'list',
|
|
888
|
+
name: 'action',
|
|
889
|
+
message: 'What would you like to do?',
|
|
890
|
+
choices: [
|
|
891
|
+
{ name: chalk.green('Install now (recommended)'), value: 'install' },
|
|
892
|
+
{ name: 'Choose a different provider', value: 'back' },
|
|
893
|
+
{ name: 'I\'ll install it myself later', value: 'skip' }
|
|
894
|
+
]
|
|
895
|
+
}]);
|
|
896
|
+
|
|
897
|
+
if (action === 'install') {
|
|
898
|
+
console.log(chalk.cyan(`\n๐ฆ Installing ${displayName}...\n`));
|
|
899
|
+
|
|
900
|
+
// Use smart installation with fallbacks
|
|
901
|
+
const installResult = await attemptProviderInstallation(provider);
|
|
902
|
+
|
|
903
|
+
if (installResult.success && installResult.verified) {
|
|
904
|
+
// Installation succeeded AND verified
|
|
905
|
+
console.log(chalk.green(`\nโ ${displayName} installed and verified!\n`));
|
|
906
|
+
console.log(chalk.gray(` Method: ${installResult.command}`));
|
|
907
|
+
console.log(chalk.green(` Status: Ready to use\n`));
|
|
908
|
+
} else if (installResult.success) {
|
|
909
|
+
// Installation command ran but verification failed
|
|
910
|
+
console.log(chalk.yellow(`\nโ ๏ธ Installation command completed, but verification failed\n`));
|
|
911
|
+
console.log(chalk.gray(` The installation may have been blocked by system protection (PEP 668).\n`));
|
|
912
|
+
console.log(chalk.cyan(` Try one of these solutions:\n`));
|
|
913
|
+
console.log(chalk.gray(` 1. Use pipx (avoids system protection):\n pipx install soprano-tts\n`));
|
|
914
|
+
console.log(chalk.gray(` 2. Create a virtual environment:\n python3 -m venv ~/my-env\n ~/my-env/bin/pip install soprano-tts\n`));
|
|
915
|
+
|
|
916
|
+
// Pause before returning to provider selection
|
|
917
|
+
await inquirer.prompt([{
|
|
918
|
+
type: 'confirm',
|
|
919
|
+
name: 'continue',
|
|
920
|
+
message: 'Press Enter to go back to provider selection',
|
|
921
|
+
default: true
|
|
922
|
+
}]);
|
|
923
|
+
|
|
924
|
+
return null; // Go back to provider selection
|
|
925
|
+
} else {
|
|
926
|
+
console.log(chalk.red(`\nโ ${installResult.message}\n`));
|
|
927
|
+
|
|
928
|
+
// Pause before returning to provider selection
|
|
929
|
+
await inquirer.prompt([{
|
|
930
|
+
type: 'confirm',
|
|
931
|
+
name: 'continue',
|
|
932
|
+
message: 'Press Enter to go back to provider selection',
|
|
933
|
+
default: true
|
|
934
|
+
}]);
|
|
935
|
+
|
|
936
|
+
return null; // Go back to provider selection
|
|
937
|
+
}
|
|
938
|
+
} else if (action === 'back') {
|
|
939
|
+
// Go back to provider selection
|
|
940
|
+
return null;
|
|
941
|
+
} else if (action === 'skip') {
|
|
942
|
+
console.log(chalk.yellow(`\nโ ๏ธ No problem! You can set it up anytime with:\n ${getProviderInstallCommand(provider)}\n`));
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
// Provider detected and ready to use
|
|
946
|
+
const displayName = getProviderDisplayName(provider);
|
|
947
|
+
console.log(chalk.green(`\nโ ${displayName} Detected and selected!\n`));
|
|
948
|
+
}
|
|
949
|
+
|
|
872
950
|
config.provider = provider;
|
|
873
951
|
|
|
874
952
|
// Handle special receiver mode for Termux
|
|
@@ -1602,20 +1680,17 @@ function showWelcome() {
|
|
|
1602
1680
|
* Shown during install and update commands
|
|
1603
1681
|
*/
|
|
1604
1682
|
function getReleaseInfoBoxen() {
|
|
1605
|
-
return chalk.cyan.bold('๐ฆ AgentVibes v3.5.
|
|
1683
|
+
return chalk.cyan.bold('๐ฆ AgentVibes v3.5.8 - Provider Validation Security & UX Improvements\n\n') +
|
|
1606
1684
|
chalk.green.bold('๐๏ธ WHAT\'S NEW:\n\n') +
|
|
1607
|
-
chalk.cyan('
|
|
1608
|
-
chalk.cyan('
|
|
1609
|
-
chalk.cyan('
|
|
1610
|
-
chalk.cyan('installer with figlet banner, and 46 new Windows-specific unit tests.\n\n') +
|
|
1611
|
-
chalk.yellow('๐ Thanks to @nathanchase (Soprano), @alexeyv (Windows SAPI), @bmadcode (BMAD Method)!\n\n') +
|
|
1685
|
+
chalk.cyan('Critical security and reliability update for provider detection. Fixes command injection\n') +
|
|
1686
|
+
chalk.cyan('vulnerabilities, prevents HOME directory injection attacks, and improves UX with explicit\n') +
|
|
1687
|
+
chalk.cyan('provider detection messaging. Soprano TTS installed via pipx now correctly detected.\n\n') +
|
|
1612
1688
|
chalk.green.bold('โจ KEY HIGHLIGHTS:\n\n') +
|
|
1613
|
-
chalk.gray('
|
|
1614
|
-
chalk.gray('
|
|
1615
|
-
chalk.gray('
|
|
1616
|
-
chalk.gray('
|
|
1617
|
-
chalk.gray('
|
|
1618
|
-
chalk.gray(' ๐งช 93/93 Tests Passing - 46 Windows + 47 cross-platform\n\n') +
|
|
1689
|
+
chalk.gray(' ๐ Security Fixes - Fixed command injection, HOME injection prevention, path traversal\n') +
|
|
1690
|
+
chalk.gray(' โ
Provider Detection - Soprano via pipx now correctly detected\n') +
|
|
1691
|
+
chalk.gray(' ๐ฌ Better Messaging - Explicit detection confirmation, detailed error messages\n') +
|
|
1692
|
+
chalk.gray(' ๐งช Enhanced Tests - Verification of actual detection values\n') +
|
|
1693
|
+
chalk.gray(' ๐ Debug Support - Added logging for troubleshooting\n\n') +
|
|
1619
1694
|
chalk.gray('๐ Full Release Notes: RELEASE_NOTES.md\n') +
|
|
1620
1695
|
chalk.gray('๐ Website: https://agentvibes.org\n') +
|
|
1621
1696
|
chalk.gray('๐ฆ Repository: https://github.com/paulpreibisch/AgentVibes\n\n') +
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provider validation utility for AgentVibes
|
|
3
|
+
* Validates TTS provider availability at installation, switch, and runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import path from 'node:path'; // For safe path operations and traversal prevention
|
|
9
|
+
import fs from 'node:fs'; // For checking file/directory existence
|
|
10
|
+
import os from 'node:os'; // For os.homedir() to prevent HOME injection attacks
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a TTS provider's installation status
|
|
14
|
+
* @param {string} providerName - Provider name (soprano, piper, macos, windows-sapi, etc.)
|
|
15
|
+
* @returns {Promise<{installed: boolean, message: string, pythonVersion?: string, error?: string}>}
|
|
16
|
+
*/
|
|
17
|
+
export async function validateProvider(providerName) {
|
|
18
|
+
switch (providerName) {
|
|
19
|
+
case 'soprano':
|
|
20
|
+
return await validateSopranoInstallation();
|
|
21
|
+
case 'piper':
|
|
22
|
+
return await validatePiperInstallation();
|
|
23
|
+
case 'macos':
|
|
24
|
+
return await validateMacOSProvider();
|
|
25
|
+
case 'windows-sapi':
|
|
26
|
+
return await validateWindowsSAPI();
|
|
27
|
+
case 'windows-piper':
|
|
28
|
+
return await validatePiperInstallation();
|
|
29
|
+
case 'termux-ssh':
|
|
30
|
+
case 'ssh-remote':
|
|
31
|
+
case 'ssh-pulseaudio':
|
|
32
|
+
case 'piper-receiver':
|
|
33
|
+
case 'silent':
|
|
34
|
+
// These don't require local provider installation
|
|
35
|
+
return { installed: true, message: 'Remote/Special provider - no local installation needed' };
|
|
36
|
+
default:
|
|
37
|
+
return { installed: false, message: `Unknown provider: ${providerName}`, error: 'UNKNOWN_PROVIDER' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate Soprano TTS installation
|
|
43
|
+
* Checks multiple Python versions since Soprano might be in non-default Python
|
|
44
|
+
* @returns {Promise<{installed: boolean, message: string, pythonVersion?: string, checkedCount?: number}>}
|
|
45
|
+
*/
|
|
46
|
+
export async function validateSopranoInstallation() {
|
|
47
|
+
const checkedLocations = [];
|
|
48
|
+
|
|
49
|
+
// Check for pipx installation first (common for CLI tools)
|
|
50
|
+
// Use os.homedir() (not env var) to prevent HOME injection attacks
|
|
51
|
+
try {
|
|
52
|
+
const homeDir = os.homedir();
|
|
53
|
+
const sopranoVenvPath = path.join(homeDir, '.local', 'share', 'pipx', 'venvs', 'soprano-tts');
|
|
54
|
+
|
|
55
|
+
// Validate path is within home directory (prevent path traversal)
|
|
56
|
+
const resolvedPath = path.resolve(sopranoVenvPath);
|
|
57
|
+
const resolvedHome = path.resolve(homeDir);
|
|
58
|
+
if (!resolvedPath.startsWith(resolvedHome)) {
|
|
59
|
+
throw new Error('Path traversal detected');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (fs.existsSync(sopranoVenvPath)) {
|
|
63
|
+
checkedLocations.push('pipx'); // Always track what was checked
|
|
64
|
+
return { installed: true, message: 'Soprano TTS detected (via pipx)', checkedLocations };
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// If home directory check fails, fall through to Python checks
|
|
68
|
+
if (error.code !== 'ENOENT') {
|
|
69
|
+
console.error('[DEBUG] Pipx check error:', error.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
checkedLocations.push('pipx');
|
|
73
|
+
|
|
74
|
+
// Comprehensive Python version detection
|
|
75
|
+
const pythonCommands = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8'];
|
|
76
|
+
|
|
77
|
+
for (const pythonCmd of pythonCommands) {
|
|
78
|
+
try {
|
|
79
|
+
// Use array form to prevent command injection (security: CLAUDE.md)
|
|
80
|
+
const result = execSync(pythonCmd, ['-m', 'pip', 'show', 'soprano-tts'], {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
|
+
timeout: 10000
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (result && result.trim()) {
|
|
87
|
+
checkedLocations.push(pythonCmd); // Track which Python found it
|
|
88
|
+
return {
|
|
89
|
+
installed: true,
|
|
90
|
+
message: `Soprano TTS detected via ${pythonCmd}`,
|
|
91
|
+
pythonVersion: pythonCmd,
|
|
92
|
+
checkedCount: checkedLocations.length + pythonCommands.indexOf(pythonCmd)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// Continue to next Python version - errors expected when Python not in PATH or package missing
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Build list of Python versions checked
|
|
101
|
+
checkedLocations.push(...pythonCommands);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
installed: false,
|
|
105
|
+
message: `Soprano TTS is not installed on your system (checked: ${checkedLocations.join(', ')})`,
|
|
106
|
+
error: 'SOPRANO_NOT_FOUND',
|
|
107
|
+
checkedCount: checkedLocations.length
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validate Piper TTS installation
|
|
113
|
+
* Checks if Piper is available via pipx or direct installation
|
|
114
|
+
* Suppresses all error output for clean UX
|
|
115
|
+
* @returns {Promise<{installed: boolean, message: string}>}
|
|
116
|
+
*/
|
|
117
|
+
export async function validatePiperInstallation() {
|
|
118
|
+
const checkedLocations = [];
|
|
119
|
+
|
|
120
|
+
// Check for piper binary with error suppression
|
|
121
|
+
try {
|
|
122
|
+
execSync('which piper 2>/dev/null', {
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
shell: true,
|
|
125
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
126
|
+
});
|
|
127
|
+
return { installed: true, message: 'Piper TTS detected (binary in PATH)' };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
checkedLocations.push('PATH (piper binary)');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for pipx installation (use venv directory check - more reliable than pipx list)
|
|
133
|
+
try {
|
|
134
|
+
const homeDir = os.homedir(); // Use os.homedir() not env var (security: prevent HOME injection)
|
|
135
|
+
const piperVenvPath = path.join(homeDir, '.local', 'share', 'pipx', 'venvs', 'piper-tts');
|
|
136
|
+
|
|
137
|
+
// Validate path is within home directory (prevent path traversal)
|
|
138
|
+
const resolvedPath = path.resolve(piperVenvPath);
|
|
139
|
+
const resolvedHome = path.resolve(homeDir);
|
|
140
|
+
if (!resolvedPath.startsWith(resolvedHome)) {
|
|
141
|
+
throw new Error('Path traversal detected');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (fs.existsSync(piperVenvPath)) {
|
|
145
|
+
checkedLocations.push('pipx'); // Always track
|
|
146
|
+
return { installed: true, message: 'Piper TTS detected (via pipx)', checkedLocations };
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.code !== 'ENOENT') {
|
|
150
|
+
console.error('[DEBUG] Pipx check error:', error.message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
checkedLocations.push('pipx');
|
|
154
|
+
|
|
155
|
+
// Check if Python + piper-tts package installed using array form (security: prevent injection)
|
|
156
|
+
try {
|
|
157
|
+
const result = execSync('python3', ['-m', 'pip', 'show', 'piper-tts'], {
|
|
158
|
+
encoding: 'utf8',
|
|
159
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
160
|
+
timeout: 10000
|
|
161
|
+
});
|
|
162
|
+
if (result && result.trim()) {
|
|
163
|
+
checkedLocations.push('python3 pip'); // Track what found it
|
|
164
|
+
return { installed: true, message: 'Piper TTS detected (Python package)', checkedLocations };
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
checkedLocations.push('python3 pip');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
installed: false,
|
|
172
|
+
message: `Piper TTS is not installed on your system (checked: ${checkedLocations.join(', ')})`,
|
|
173
|
+
error: 'PIPER_NOT_FOUND'
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate macOS Say (built-in)
|
|
179
|
+
* @returns {Promise<{installed: boolean, message: string}>}
|
|
180
|
+
*/
|
|
181
|
+
export async function validateMacOSProvider() {
|
|
182
|
+
if (process.platform !== 'darwin') {
|
|
183
|
+
return {
|
|
184
|
+
installed: false,
|
|
185
|
+
message: 'macOS Say is only available on macOS (this is not a Mac)',
|
|
186
|
+
error: 'NOT_MACOS'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
execSync('which say 2>/dev/null', {
|
|
192
|
+
encoding: 'utf8',
|
|
193
|
+
shell: true,
|
|
194
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
195
|
+
});
|
|
196
|
+
return { installed: true, message: 'macOS Say detected' };
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// say command not found
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
installed: false,
|
|
203
|
+
message: 'macOS Say is not installed on your system (checked: /usr/bin/say)',
|
|
204
|
+
error: 'MACOS_SAY_NOT_FOUND'
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Validate Windows SAPI (built-in)
|
|
210
|
+
* @returns {Promise<{installed: boolean, message: string}>}
|
|
211
|
+
*/
|
|
212
|
+
export async function validateWindowsSAPI() {
|
|
213
|
+
if (process.platform !== 'win32') {
|
|
214
|
+
return {
|
|
215
|
+
installed: false,
|
|
216
|
+
message: 'Windows SAPI only available on Windows',
|
|
217
|
+
error: 'NOT_WINDOWS'
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Windows SAPI is built-in, always available
|
|
222
|
+
return { installed: true, message: 'Windows SAPI available' };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Test if a provider works at runtime (more thorough check)
|
|
227
|
+
* Attempts to actually use the provider for a brief moment
|
|
228
|
+
* @param {string} providerName
|
|
229
|
+
* @returns {Promise<{working: boolean, error?: string}>}
|
|
230
|
+
*/
|
|
231
|
+
export async function testProviderRuntime(providerName) {
|
|
232
|
+
switch (providerName) {
|
|
233
|
+
case 'soprano':
|
|
234
|
+
return await testSopranoRuntime();
|
|
235
|
+
case 'piper':
|
|
236
|
+
return await testPiperRuntime();
|
|
237
|
+
case 'macos':
|
|
238
|
+
return await testMacOSRuntime();
|
|
239
|
+
default:
|
|
240
|
+
return { working: true }; // Assume other providers work if installed
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Test Soprano runtime (webui connectivity)
|
|
246
|
+
*/
|
|
247
|
+
async function testSopranoRuntime() {
|
|
248
|
+
try {
|
|
249
|
+
// Try a quick soprano import check
|
|
250
|
+
const result = spawnSync('python3', ['-c', 'import soprano; print("OK")'], {
|
|
251
|
+
timeout: 5000,
|
|
252
|
+
encoding: 'utf8'
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (result.error || result.status !== 0) {
|
|
256
|
+
return {
|
|
257
|
+
working: false,
|
|
258
|
+
error: 'Soprano Python module import failed'
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { working: true };
|
|
263
|
+
} catch (e) {
|
|
264
|
+
return {
|
|
265
|
+
working: false,
|
|
266
|
+
error: e.message
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Test Piper runtime
|
|
273
|
+
*/
|
|
274
|
+
async function testPiperRuntime() {
|
|
275
|
+
try {
|
|
276
|
+
execSync('piper', ['--help'], {
|
|
277
|
+
encoding: 'utf8',
|
|
278
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
279
|
+
timeout: 5000
|
|
280
|
+
});
|
|
281
|
+
return { working: true };
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return {
|
|
284
|
+
working: false,
|
|
285
|
+
error: 'Piper command execution failed'
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Test macOS Say runtime
|
|
292
|
+
*/
|
|
293
|
+
async function testMacOSRuntime() {
|
|
294
|
+
try {
|
|
295
|
+
execSync('say', ['-f', '/dev/null'], {
|
|
296
|
+
encoding: 'utf8',
|
|
297
|
+
timeout: 5000,
|
|
298
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
299
|
+
});
|
|
300
|
+
return { working: true };
|
|
301
|
+
} catch (e) {
|
|
302
|
+
return {
|
|
303
|
+
working: false,
|
|
304
|
+
error: 'macOS Say command execution failed'
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get the appropriate pip/install command for a provider
|
|
311
|
+
* @param {string} providerName
|
|
312
|
+
* @returns {string} Installation command or empty string if N/A
|
|
313
|
+
*/
|
|
314
|
+
export function getProviderInstallCommand(providerName) {
|
|
315
|
+
const commands = {
|
|
316
|
+
soprano: 'pip install soprano-tts',
|
|
317
|
+
piper: 'pip install piper-tts',
|
|
318
|
+
// macOS Say and Windows SAPI are built-in, no install needed
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return commands[providerName] || '';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Attempt to install a provider with fallback strategies
|
|
326
|
+
* Handles PEP 668 protection by trying multiple installation methods
|
|
327
|
+
* @param {string} providerName - Provider name (soprano, piper)
|
|
328
|
+
* @returns {object} {success: boolean, message: string, command?: string, verified?: boolean}
|
|
329
|
+
*/
|
|
330
|
+
export async function attemptProviderInstallation(providerName) {
|
|
331
|
+
// Whitelist approach - only allow known providers (MEDIUM #1 fix)
|
|
332
|
+
const providers = {
|
|
333
|
+
soprano: 'soprano-tts',
|
|
334
|
+
piper: 'piper-tts'
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const pkgName = providers[providerName];
|
|
338
|
+
if (!pkgName) {
|
|
339
|
+
return { success: false, message: `Unknown provider: ${providerName}` };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Strategy 1: Try regular pip install (using array form for security - CRITICAL #1 fix)
|
|
343
|
+
try {
|
|
344
|
+
// Show installation in progress
|
|
345
|
+
console.log(` Attempting: pip install ${pkgName}...`);
|
|
346
|
+
execSync('pip', ['install', pkgName], {
|
|
347
|
+
stdio: 'inherit',
|
|
348
|
+
timeout: 60000 // HIGH #1 fix - 60 second timeout
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Verify installation actually worked (proves it's installed)
|
|
352
|
+
const validation = await validateProvider(providerName);
|
|
353
|
+
if (validation.installed) {
|
|
354
|
+
return {
|
|
355
|
+
success: true,
|
|
356
|
+
message: `Successfully installed via pip`,
|
|
357
|
+
command: `pip install ${pkgName}`,
|
|
358
|
+
verified: true
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { success: true, message: `Successfully installed via pip`, command: `pip install ${pkgName}` };
|
|
363
|
+
} catch (error) {
|
|
364
|
+
// Strategy 1 failed - continue to Strategy 2
|
|
365
|
+
// Don't check specific error messages - just try next strategy (MEDIUM #3 fix)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Strategy 2: Try pipx install (recommended for PEP 668 protection)
|
|
369
|
+
try {
|
|
370
|
+
console.log(` Attempting: pipx install ${pkgName}...`);
|
|
371
|
+
execSync('pipx', ['install', pkgName], {
|
|
372
|
+
stdio: 'inherit',
|
|
373
|
+
timeout: 60000 // HIGH #1 fix - 60 second timeout
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Verify installation actually worked (proves it's installed)
|
|
377
|
+
const validation = await validateProvider(providerName);
|
|
378
|
+
if (validation.installed) {
|
|
379
|
+
return {
|
|
380
|
+
success: true,
|
|
381
|
+
message: `Successfully installed via pipx`,
|
|
382
|
+
command: `pipx install ${pkgName}`,
|
|
383
|
+
verified: true
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { success: true, message: `Successfully installed via pipx`, command: `pipx install ${pkgName}` };
|
|
388
|
+
} catch (error) {
|
|
389
|
+
// Both strategies failed
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Both strategies failed - consistent error message (MEDIUM #2 fix)
|
|
393
|
+
return { success: false, message: `Installation failed. Please install manually:\n pip install ${pkgName}\n or\n pipx install ${pkgName}` };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get package installation information (location and version)
|
|
398
|
+
* @param {string} pkgName - Package name to check
|
|
399
|
+
* @returns {object|null} {location, version} or null if not found
|
|
400
|
+
*/
|
|
401
|
+
function getPackageInfo(pkgName) {
|
|
402
|
+
try {
|
|
403
|
+
// Small delay to ensure package is registered in pip
|
|
404
|
+
// (sometimes pip needs a moment to update its cache)
|
|
405
|
+
const output = execSync('pip', ['show', pkgName], {
|
|
406
|
+
encoding: 'utf8',
|
|
407
|
+
timeout: 10000,
|
|
408
|
+
stdio: ['pipe', 'pipe', 'pipe'] // Capture both stdout and stderr
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Parse pip show output
|
|
412
|
+
const info = {};
|
|
413
|
+
const lines = output.split('\n');
|
|
414
|
+
|
|
415
|
+
for (const line of lines) {
|
|
416
|
+
if (!line.trim()) continue;
|
|
417
|
+
|
|
418
|
+
const colonIndex = line.indexOf(':');
|
|
419
|
+
if (colonIndex === -1) continue;
|
|
420
|
+
|
|
421
|
+
const key = line.substring(0, colonIndex).trim();
|
|
422
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
423
|
+
|
|
424
|
+
if (key === 'Location') {
|
|
425
|
+
info.location = value;
|
|
426
|
+
} else if (key === 'Version') {
|
|
427
|
+
info.version = value;
|
|
428
|
+
} else if (key === 'Name') {
|
|
429
|
+
info.name = value;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Return info if we found at least version or location
|
|
434
|
+
return (info.version || info.location) ? info : null;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
// Silently fail - package info is optional, not critical
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get user-friendly provider names
|
|
443
|
+
* @param {string} providerName
|
|
444
|
+
* @returns {string} Display name
|
|
445
|
+
*/
|
|
446
|
+
export function getProviderDisplayName(providerName) {
|
|
447
|
+
const names = {
|
|
448
|
+
soprano: 'Soprano TTS',
|
|
449
|
+
piper: 'Piper TTS',
|
|
450
|
+
macos: 'macOS Say',
|
|
451
|
+
'windows-sapi': 'Windows SAPI',
|
|
452
|
+
'windows-piper': 'Windows Piper TTS'
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return names[providerName] || providerName;
|
|
456
|
+
}
|