agentvibes 3.5.9 → 3.5.10-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/github-star-reminder.txt +1 -1
- package/README.md +11 -4
- package/package.json +3 -1
- package/src/installer.js +314 -37
- package/src/utils/provider-validator.js +144 -132
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20260214
|
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.10-alpha.0
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -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.10-alpha.0 (Alpha) with Soprano Detection Fixes + 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)
|
|
@@ -142,9 +142,16 @@ All 50+ Piper voices AgentVibes provides are sourced from Hugging Face's open-so
|
|
|
142
142
|
|
|
143
143
|
## 📰 Latest Release
|
|
144
144
|
|
|
145
|
-
**[v3.5.
|
|
145
|
+
**[v3.5.10-alpha.0 - Soprano Detection Fixes & Features](https://github.com/paulpreibisch/AgentVibes/releases/tag/master)** (Alpha) 🛡️
|
|
146
146
|
|
|
147
|
-
Critical
|
|
147
|
+
Critical fixes and new features:
|
|
148
|
+
- Fixed Soprano TTS detection when installed via pipx
|
|
149
|
+
- Fixed command injection vulnerabilities in provider validation
|
|
150
|
+
- Eliminated code duplication between Soprano and Piper validators
|
|
151
|
+
- Added custom music tracks support with preview functionality
|
|
152
|
+
- Added personality emoji mapping for better visual recognition
|
|
153
|
+
- Added pretext configuration for custom agent introductions
|
|
154
|
+
- HOME directory injection prevention and path traversal protection maintained
|
|
148
155
|
|
|
149
156
|
**Foundation Release:** [v3.5.5 - Native Windows Support](https://github.com/paulpreibisch/AgentVibes/releases/tag/v3.5.9) 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.9 history.
|
|
150
157
|
|
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.10-alpha.0",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
|
@@ -83,6 +83,8 @@
|
|
|
83
83
|
"test:verbose": "AGENTVIBES_TEST_MODE=true bats -t test/unit/*.bats"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
+
"@inquirer/search": "^3.1.3",
|
|
87
|
+
"agentvibes": "^3.5.9",
|
|
86
88
|
"boxen": "^7.0.0",
|
|
87
89
|
"chalk": "^5.0.0",
|
|
88
90
|
"commander": "^10.0.0",
|
package/src/installer.js
CHANGED
|
@@ -48,6 +48,7 @@ import fsSync from 'node:fs';
|
|
|
48
48
|
import { execSync, execFileSync, spawn } from 'node:child_process';
|
|
49
49
|
import chalk from 'chalk';
|
|
50
50
|
import inquirer from 'inquirer';
|
|
51
|
+
import search from '@inquirer/search';
|
|
51
52
|
import figlet from 'figlet';
|
|
52
53
|
import { detectBMAD } from './bmad-detector.js';
|
|
53
54
|
import boxen from 'boxen';
|
|
@@ -77,6 +78,31 @@ const packageJson = JSON.parse(
|
|
|
77
78
|
);
|
|
78
79
|
const VERSION = packageJson.version;
|
|
79
80
|
|
|
81
|
+
// Personality emoji mapping for quick visual recognition
|
|
82
|
+
const personalityEmojis = {
|
|
83
|
+
'angry': '😠',
|
|
84
|
+
'annoying': '😤',
|
|
85
|
+
'crass': '🤬',
|
|
86
|
+
'dramatic': '🎭',
|
|
87
|
+
'dry-humor': '😐',
|
|
88
|
+
'flirty': '😘',
|
|
89
|
+
'funny': '😂',
|
|
90
|
+
'grandpa': '👴',
|
|
91
|
+
'millennial': '🙄',
|
|
92
|
+
'moody': '😒',
|
|
93
|
+
'none': '😊',
|
|
94
|
+
'normal': '😊',
|
|
95
|
+
'pirate': '🏴☠️',
|
|
96
|
+
'poetic': '📜',
|
|
97
|
+
'professional': '👔',
|
|
98
|
+
'rapper': '🎤',
|
|
99
|
+
'robot': '🤖',
|
|
100
|
+
'sarcastic': '😏',
|
|
101
|
+
'sassy': '💁',
|
|
102
|
+
'surfer-dude': '🏄',
|
|
103
|
+
'zen': '🧘'
|
|
104
|
+
};
|
|
105
|
+
|
|
80
106
|
// Validate Node.js executable is available (CLAUDE.md - early validation)
|
|
81
107
|
if (!process.execPath) {
|
|
82
108
|
console.error('❌ Error: Node.js executable path not found');
|
|
@@ -519,6 +545,159 @@ async function handleSystemDependenciesPage() {
|
|
|
519
545
|
console.log(depsBoxen);
|
|
520
546
|
}
|
|
521
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Validate and copy custom music track to .claude/audio/tracks directory
|
|
550
|
+
* @param {string} userFilePath - Path provided by user
|
|
551
|
+
* @param {string} tracksDir - Target directory for audio tracks
|
|
552
|
+
* @returns {Promise<string|null>} Filename if successful, null if cancelled
|
|
553
|
+
*/
|
|
554
|
+
async function handleCustomMusicTrack(userFilePath, tracksDir) {
|
|
555
|
+
try {
|
|
556
|
+
// Validate file exists and resolve path securely
|
|
557
|
+
const resolvedPath = path.resolve(userFilePath.trim());
|
|
558
|
+
|
|
559
|
+
if (!fsSync.existsSync(resolvedPath)) {
|
|
560
|
+
console.error(chalk.red('✗ File not found. Please check the path.'));
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Validate file extension (whitelist approach per CLAUDE.md)
|
|
565
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
566
|
+
const supportedFormats = ['.mp3', '.wav', '.ogg', '.m4a'];
|
|
567
|
+
if (!supportedFormats.includes(ext)) {
|
|
568
|
+
console.error(chalk.red('✗ Unsupported format. Use: .mp3, .wav, .ogg, or .m4a'));
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Verify file is within expected directory (prevent path traversal)
|
|
573
|
+
if (!resolvedPath.startsWith(path.resolve(process.env.HOME || process.env.USERPROFILE))) {
|
|
574
|
+
console.error(chalk.red('✗ File must be in your home directory or subdirectories.'));
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Get original filename and sanitize it
|
|
579
|
+
let originalFilename = path.basename(resolvedPath);
|
|
580
|
+
const sanitizedFilename = originalFilename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
581
|
+
|
|
582
|
+
// Create tracks directory if needed
|
|
583
|
+
await fs.mkdir(tracksDir, { recursive: true });
|
|
584
|
+
|
|
585
|
+
// Copy file to tracks directory
|
|
586
|
+
const destPath = path.join(tracksDir, sanitizedFilename);
|
|
587
|
+
await fs.copyFile(resolvedPath, destPath);
|
|
588
|
+
|
|
589
|
+
return sanitizedFilename;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.error(chalk.red(`✗ Error: ${err.message}`));
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Load custom tracks from global registry
|
|
598
|
+
* @returns {Promise<Array>} Array of custom track objects {name, filename}
|
|
599
|
+
*/
|
|
600
|
+
async function loadCustomTracks() {
|
|
601
|
+
try {
|
|
602
|
+
const registryPath = path.join(process.env.HOME || process.env.USERPROFILE, '.agentvibes', 'custom-tracks.json');
|
|
603
|
+
if (fsSync.existsSync(registryPath)) {
|
|
604
|
+
const content = await fs.readFile(registryPath, 'utf-8');
|
|
605
|
+
return JSON.parse(content);
|
|
606
|
+
}
|
|
607
|
+
} catch (err) {
|
|
608
|
+
// Silently fail - registry may not exist yet
|
|
609
|
+
}
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Save custom tracks to global registry
|
|
615
|
+
* @param {Array} tracks - Array of custom track objects
|
|
616
|
+
*/
|
|
617
|
+
async function saveCustomTracks(tracks) {
|
|
618
|
+
try {
|
|
619
|
+
const registryDir = path.join(process.env.HOME || process.env.USERPROFILE, '.agentvibes');
|
|
620
|
+
await fs.mkdir(registryDir, { recursive: true });
|
|
621
|
+
const registryPath = path.join(registryDir, 'custom-tracks.json');
|
|
622
|
+
await fs.writeFile(registryPath, JSON.stringify(tracks, null, 2));
|
|
623
|
+
} catch (err) {
|
|
624
|
+
// Silently fail - non-critical
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Preview audio track using available audio player
|
|
630
|
+
* @param {string} trackName - Name of the track file to preview
|
|
631
|
+
* @param {string} tracksDir - Directory containing audio tracks
|
|
632
|
+
* @returns {Promise<boolean>} True if preview was attempted, false if no audio tools available
|
|
633
|
+
*/
|
|
634
|
+
async function previewAudioTrack(trackName, tracksDir) {
|
|
635
|
+
const trackPath = path.join(tracksDir, trackName);
|
|
636
|
+
|
|
637
|
+
// Verify track exists
|
|
638
|
+
if (!fsSync.existsSync(trackPath)) {
|
|
639
|
+
console.log(chalk.yellow('⚠️ Track file not found'));
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Try available audio players in order of preference
|
|
644
|
+
const audioPlayers = ['ffplay', 'play', 'mpv'];
|
|
645
|
+
let playerAvailable = false;
|
|
646
|
+
|
|
647
|
+
for (const player of audioPlayers) {
|
|
648
|
+
try {
|
|
649
|
+
execSync(`which ${player}`, { stdio: 'ignore' });
|
|
650
|
+
playerAvailable = true;
|
|
651
|
+
|
|
652
|
+
console.log(chalk.cyan('▶ Playing preview (10 seconds)...'));
|
|
653
|
+
|
|
654
|
+
// Build appropriate command for each player
|
|
655
|
+
let playerArgs = [];
|
|
656
|
+
if (player === 'ffplay') {
|
|
657
|
+
playerArgs = ['-nodisp', '-autoexit', '-t', '10', '-volume', '30', trackPath];
|
|
658
|
+
} else if (player === 'play') {
|
|
659
|
+
playerArgs = [trackPath, 'trim', '0', '10'];
|
|
660
|
+
} else if (player === 'mpv') {
|
|
661
|
+
playerArgs = ['--no-video', '--duration=10', '--volume=30', trackPath];
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Spawn player process with safety timeout
|
|
665
|
+
const audioProcess = spawn(player, playerArgs, {
|
|
666
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
667
|
+
timeout: 12000 // 12 second timeout for safety
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Handle process completion
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
const timeoutHandle = setTimeout(() => {
|
|
673
|
+
if (audioProcess && !audioProcess.killed) {
|
|
674
|
+
audioProcess.kill('SIGTERM');
|
|
675
|
+
}
|
|
676
|
+
resolve(true);
|
|
677
|
+
}, 11000); // 11 second timeout
|
|
678
|
+
|
|
679
|
+
audioProcess.on('close', () => {
|
|
680
|
+
clearTimeout(timeoutHandle);
|
|
681
|
+
resolve(true);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
audioProcess.on('error', () => {
|
|
685
|
+
clearTimeout(timeoutHandle);
|
|
686
|
+
resolve(true);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
} catch (err) {
|
|
690
|
+
// This player not available, try next
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (!playerAvailable) {
|
|
696
|
+
console.log(chalk.yellow('⚠️ Audio preview requires ffplay, sox (play), or mpv'));
|
|
697
|
+
}
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
522
701
|
/**
|
|
523
702
|
* Collect all configuration answers through paginated question flow
|
|
524
703
|
* @param {Object} options - Installation options (yes, pageOffset, totalPages)
|
|
@@ -530,6 +709,7 @@ async function collectConfiguration(options = {}) {
|
|
|
530
709
|
piperPath: null,
|
|
531
710
|
sshHost: null,
|
|
532
711
|
defaultVoice: null,
|
|
712
|
+
pretext: '',
|
|
533
713
|
personality: 'none',
|
|
534
714
|
reverb: 'light',
|
|
535
715
|
backgroundMusic: {
|
|
@@ -1314,7 +1494,37 @@ async function collectConfiguration(options = {}) {
|
|
|
1314
1494
|
}
|
|
1315
1495
|
|
|
1316
1496
|
} else if (currentPage === 3) {
|
|
1317
|
-
// Page 4: Personality Selection
|
|
1497
|
+
// Page 4: Pretext and Personality Selection
|
|
1498
|
+
console.log(boxen(
|
|
1499
|
+
chalk.white('Customize your Agent\'s introduction!\n\n') +
|
|
1500
|
+
chalk.gray('Add optional intro text that prefixes all TTS messages.\n') +
|
|
1501
|
+
chalk.gray('Examples: "FireBot: ", "Agent: ", "🤖 Assistant: "\n\n') +
|
|
1502
|
+
chalk.gray('You can change this anytime with: ') + chalk.cyan('/agent-vibes:set-pretext <text>'),
|
|
1503
|
+
{
|
|
1504
|
+
padding: 1,
|
|
1505
|
+
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
1506
|
+
borderStyle: 'round',
|
|
1507
|
+
borderColor: 'gray',
|
|
1508
|
+
width: 80
|
|
1509
|
+
}
|
|
1510
|
+
));
|
|
1511
|
+
|
|
1512
|
+
// Pretext input
|
|
1513
|
+
const { pretext } = await inquirer.prompt([{
|
|
1514
|
+
type: 'input',
|
|
1515
|
+
name: 'pretext',
|
|
1516
|
+
message: chalk.yellow('Enter intro text (optional, press Enter to skip):'),
|
|
1517
|
+
default: config.pretext || ''
|
|
1518
|
+
}]);
|
|
1519
|
+
|
|
1520
|
+
config.pretext = pretext.trim();
|
|
1521
|
+
if (config.pretext) {
|
|
1522
|
+
console.log(chalk.green(`✓ Intro text set: "${config.pretext}"\n`));
|
|
1523
|
+
} else {
|
|
1524
|
+
console.log(chalk.gray('→ No intro text (messages will speak normally)\n'));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Page 5: Personality Selection
|
|
1318
1528
|
console.log(boxen(
|
|
1319
1529
|
chalk.white('Give your Agent a personality!\n\n') +
|
|
1320
1530
|
chalk.gray('Personalities add character and style to TTS responses.\n') +
|
|
@@ -1359,16 +1569,18 @@ async function collectConfiguration(options = {}) {
|
|
|
1359
1569
|
personalities.sort((a, b) => a.name.localeCompare(b.name));
|
|
1360
1570
|
|
|
1361
1571
|
// Add "none" as first option (default)
|
|
1572
|
+
const noneEmoji = personalityEmojis['none'] || '✨';
|
|
1362
1573
|
personalityChoices.push(
|
|
1363
|
-
{ name: chalk.green('none') + chalk.gray(' (Professional, no personality) - Recommended'), value: 'none' },
|
|
1574
|
+
{ name: noneEmoji + ' ' + chalk.green('none') + chalk.gray(' (Professional, no personality) - Recommended'), value: 'none' },
|
|
1364
1575
|
new inquirer.Separator(chalk.gray('─'.repeat(60)))
|
|
1365
1576
|
);
|
|
1366
1577
|
|
|
1367
1578
|
// Add all other personalities
|
|
1368
1579
|
for (const p of personalities) {
|
|
1369
1580
|
if (p.name !== 'normal') { // Skip 'normal' as it's similar to 'none'
|
|
1581
|
+
const emoji = personalityEmojis[p.name] || '✨';
|
|
1370
1582
|
personalityChoices.push({
|
|
1371
|
-
name: chalk.cyan(p.name) + chalk.gray(` - ${p.description}`),
|
|
1583
|
+
name: emoji + ' ' + chalk.cyan(p.name) + chalk.gray(` - ${p.description}`),
|
|
1372
1584
|
value: p.name
|
|
1373
1585
|
});
|
|
1374
1586
|
}
|
|
@@ -1387,14 +1599,23 @@ async function collectConfiguration(options = {}) {
|
|
|
1387
1599
|
];
|
|
1388
1600
|
}
|
|
1389
1601
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1602
|
+
// Use search prompt for keyboard navigation (type to filter)
|
|
1603
|
+
const selectedPersonality = await search({
|
|
1604
|
+
message: chalk.yellow('Select your default personality (type to search):'),
|
|
1605
|
+
source: async (input) => {
|
|
1606
|
+
// Filter personalityChoices based on input
|
|
1607
|
+
if (!input) {
|
|
1608
|
+
return personalityChoices;
|
|
1609
|
+
}
|
|
1610
|
+
return personalityChoices.filter(choice => {
|
|
1611
|
+
// Check if choice is a Separator, if so skip it
|
|
1612
|
+
if (!choice.value) return false;
|
|
1613
|
+
return choice.value.toLowerCase().includes(input.toLowerCase()) ||
|
|
1614
|
+
(choice.name && choice.name.toLowerCase().includes(input.toLowerCase()));
|
|
1615
|
+
});
|
|
1616
|
+
},
|
|
1396
1617
|
pageSize: 15
|
|
1397
|
-
}
|
|
1618
|
+
});
|
|
1398
1619
|
|
|
1399
1620
|
if (selectedPersonality === '__back__') {
|
|
1400
1621
|
currentPage--; // Go back to voice selection
|
|
@@ -1514,6 +1735,13 @@ async function collectConfiguration(options = {}) {
|
|
|
1514
1735
|
console.log('');
|
|
1515
1736
|
console.log(chalk.gray('🎼 Choose your default background music genre (you can change this anytime).'));
|
|
1516
1737
|
|
|
1738
|
+
// Load custom tracks from registry
|
|
1739
|
+
const customTracks = await loadCustomTracks();
|
|
1740
|
+
const customTrackChoices = customTracks.map(track => ({
|
|
1741
|
+
name: `📁 ${track.name}`,
|
|
1742
|
+
value: track.filename
|
|
1743
|
+
}));
|
|
1744
|
+
|
|
1517
1745
|
const trackChoices = [
|
|
1518
1746
|
{ name: '🎻 Soft Flamenco (Spanish guitar)', value: 'agentvibes_soft_flamenco_loop.mp3' },
|
|
1519
1747
|
{ name: '🎺 Bachata (Latin - Romantic guitar & bongos)', value: 'agent_vibes_bachata_v1_loop.mp3' },
|
|
@@ -1533,20 +1761,85 @@ async function collectConfiguration(options = {}) {
|
|
|
1533
1761
|
{ name: '🥁 Tabla Dream Pop (Indian percussion)', value: 'agent_vibes_tabla_dream_pop_v1_loop.mp3' }
|
|
1534
1762
|
];
|
|
1535
1763
|
|
|
1764
|
+
// Add custom tracks separator and options if any exist
|
|
1765
|
+
if (customTrackChoices.length > 0) {
|
|
1766
|
+
trackChoices.push(
|
|
1767
|
+
new inquirer.Separator(chalk.gray('─'.repeat(50))),
|
|
1768
|
+
...customTrackChoices
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// Add custom track option
|
|
1773
|
+
trackChoices.push(
|
|
1774
|
+
new inquirer.Separator(chalk.gray('─'.repeat(50))),
|
|
1775
|
+
{ name: '➕ Add Custom Track...', value: '__custom__' }
|
|
1776
|
+
);
|
|
1777
|
+
|
|
1536
1778
|
const { selectedTrack } = await inquirer.prompt([{
|
|
1537
1779
|
type: 'list',
|
|
1538
1780
|
name: 'selectedTrack',
|
|
1539
1781
|
message: chalk.yellow('Choose default background music track:'),
|
|
1540
1782
|
choices: trackChoices,
|
|
1541
1783
|
default: config.backgroundMusic.track || 'agentvibes_soft_flamenco_loop.mp3',
|
|
1542
|
-
pageSize:
|
|
1784
|
+
pageSize: 18
|
|
1543
1785
|
}]);
|
|
1544
1786
|
|
|
1545
|
-
|
|
1787
|
+
// Handle custom track selection
|
|
1788
|
+
if (selectedTrack === '__custom__') {
|
|
1789
|
+
const tracksDir = path.join(__dirname, '..', '.claude', 'audio', 'tracks');
|
|
1790
|
+
const { customTrackPath } = await inquirer.prompt([{
|
|
1791
|
+
type: 'input',
|
|
1792
|
+
name: 'customTrackPath',
|
|
1793
|
+
message: chalk.yellow('Enter the full path to your audio file:'),
|
|
1794
|
+
validate: (input) => {
|
|
1795
|
+
const resolvedPath = path.resolve(input.trim());
|
|
1796
|
+
if (!fsSync.existsSync(resolvedPath)) return 'File not found';
|
|
1797
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
1798
|
+
if (!['.mp3', '.wav', '.ogg', '.m4a'].includes(ext))
|
|
1799
|
+
return 'Unsupported format (use .mp3, .wav, .ogg, or .m4a)';
|
|
1800
|
+
return true;
|
|
1801
|
+
}
|
|
1802
|
+
}]);
|
|
1803
|
+
|
|
1804
|
+
const copiedFilename = await handleCustomMusicTrack(customTrackPath, tracksDir);
|
|
1805
|
+
if (copiedFilename) {
|
|
1806
|
+
config.backgroundMusic.track = copiedFilename;
|
|
1807
|
+
console.log(chalk.green(`✓ Custom track added: ${copiedFilename}`));
|
|
1808
|
+
|
|
1809
|
+
// Update registry with new custom track
|
|
1810
|
+
const trackName = path.basename(customTrackPath, path.extname(customTrackPath));
|
|
1811
|
+
const allCustomTracks = await loadCustomTracks();
|
|
1812
|
+
if (!allCustomTracks.some(t => t.filename === copiedFilename)) {
|
|
1813
|
+
allCustomTracks.push({ name: trackName, filename: copiedFilename });
|
|
1814
|
+
await saveCustomTracks(allCustomTracks);
|
|
1815
|
+
}
|
|
1816
|
+
} else {
|
|
1817
|
+
// Fall back to default if custom track fails
|
|
1818
|
+
config.backgroundMusic.track = 'agentvibes_soft_flamenco_loop.mp3';
|
|
1819
|
+
console.log(chalk.yellow('⚠️ Using default track'));
|
|
1820
|
+
}
|
|
1821
|
+
} else {
|
|
1822
|
+
config.backgroundMusic.track = selectedTrack;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Offer preview of selected track
|
|
1826
|
+
const tracksDir = path.join(__dirname, '..', '.claude', 'audio', 'tracks');
|
|
1827
|
+
const { previewTrack } = await inquirer.prompt([{
|
|
1828
|
+
type: 'confirm',
|
|
1829
|
+
name: 'previewTrack',
|
|
1830
|
+
message: chalk.cyan('Preview this track before continuing?'),
|
|
1831
|
+
default: false
|
|
1832
|
+
}]);
|
|
1833
|
+
|
|
1834
|
+
if (previewTrack) {
|
|
1835
|
+
console.log('');
|
|
1836
|
+
await previewAudioTrack(config.backgroundMusic.track, tracksDir);
|
|
1837
|
+
console.log('');
|
|
1838
|
+
}
|
|
1546
1839
|
}
|
|
1547
1840
|
|
|
1548
1841
|
// Auto-advance to next page after audio settings
|
|
1549
|
-
console.log(chalk.green('
|
|
1842
|
+
console.log(chalk.green('✓ Audio settings configured\n'));
|
|
1550
1843
|
currentPage++;
|
|
1551
1844
|
continue;
|
|
1552
1845
|
|
|
@@ -2509,30 +2802,6 @@ async function copyPersonalityFiles(targetDir, spinner) {
|
|
|
2509
2802
|
content += chalk.gray('Personalities change how Claude speaks - adding humor, emotion, or style.\n');
|
|
2510
2803
|
content += chalk.gray('Change with: ') + chalk.yellow('/agent-vibes:personality <name>') + chalk.gray(' or say "change personality to sassy"\n\n');
|
|
2511
2804
|
|
|
2512
|
-
// Map personalities to emojis
|
|
2513
|
-
const personalityEmojis = {
|
|
2514
|
-
'angry': '😠',
|
|
2515
|
-
'annoying': '😤',
|
|
2516
|
-
'crass': '🤬',
|
|
2517
|
-
'dramatic': '🎭',
|
|
2518
|
-
'dry-humor': '😐',
|
|
2519
|
-
'flirty': '😘',
|
|
2520
|
-
'funny': '😂',
|
|
2521
|
-
'grandpa': '👴',
|
|
2522
|
-
'millennial': '🙄',
|
|
2523
|
-
'moody': '😒',
|
|
2524
|
-
'normal': '😊',
|
|
2525
|
-
'pirate': '🏴☠️',
|
|
2526
|
-
'poetic': '📜',
|
|
2527
|
-
'professional': '👔',
|
|
2528
|
-
'rapper': '🎤',
|
|
2529
|
-
'robot': '🤖',
|
|
2530
|
-
'sarcastic': '😏',
|
|
2531
|
-
'sassy': '💁',
|
|
2532
|
-
'surfer-dude': '🏄',
|
|
2533
|
-
'zen': '🧘'
|
|
2534
|
-
};
|
|
2535
|
-
|
|
2536
2805
|
// Display personalities in two columns
|
|
2537
2806
|
const personalities = installedPersonalities.map(file => {
|
|
2538
2807
|
const name = file.replace('.md', '');
|
|
@@ -4387,6 +4656,14 @@ Troubleshooting:
|
|
|
4387
4656
|
await fs.writeFile(personalityFile, userConfig.personality);
|
|
4388
4657
|
}
|
|
4389
4658
|
|
|
4659
|
+
// Apply pretext configuration from userConfig
|
|
4660
|
+
if (userConfig.pretext && userConfig.pretext.trim()) {
|
|
4661
|
+
const pretextFile = path.join(claudeDir, 'config', 'tts-pretext.txt');
|
|
4662
|
+
const configDir = path.join(claudeDir, 'config');
|
|
4663
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
4664
|
+
await fs.writeFile(pretextFile, userConfig.pretext.trim());
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4390
4667
|
// Initialize piperVoicesBoxen outside the conditional for proper scoping
|
|
4391
4668
|
let piperVoicesBoxen = null;
|
|
4392
4669
|
|
|
@@ -9,10 +9,50 @@ import path from 'node:path'; // For safe path operations and traversal preventi
|
|
|
9
9
|
import fs from 'node:fs'; // For checking file/directory existence
|
|
10
10
|
import os from 'node:os'; // For os.homedir() to prevent HOME injection attacks
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Helper: Check if command exists in PATH
|
|
14
|
+
* @param {string} command - Command name to check
|
|
15
|
+
* @returns {boolean} True if command exists in PATH
|
|
16
|
+
*/
|
|
17
|
+
function commandExistsInPath(command) {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`which "${command}" 2>/dev/null`, {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
shell: true,
|
|
22
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Helper: Check if file/directory exists and is within home directory
|
|
32
|
+
* @param {string} targetPath - Full path to check
|
|
33
|
+
* @returns {boolean} True if path exists and is safe
|
|
34
|
+
*/
|
|
35
|
+
function isSafePathExists(targetPath) {
|
|
36
|
+
try {
|
|
37
|
+
const homeDir = os.homedir();
|
|
38
|
+
const resolvedPath = path.resolve(targetPath);
|
|
39
|
+
const resolvedHome = path.resolve(homeDir);
|
|
40
|
+
|
|
41
|
+
// Validate path is within home directory (prevent path traversal)
|
|
42
|
+
if (!resolvedPath.startsWith(resolvedHome)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return fs.existsSync(targetPath);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
12
52
|
/**
|
|
13
53
|
* Validate a TTS provider's installation status
|
|
14
54
|
* @param {string} providerName - Provider name (soprano, piper, macos, windows-sapi, etc.)
|
|
15
|
-
* @returns {Promise<{installed: boolean, message: string,
|
|
55
|
+
* @returns {Promise<{installed: boolean, message: string, checkedLocations?: string[], error?: string}>}
|
|
16
56
|
*/
|
|
17
57
|
export async function validateProvider(providerName) {
|
|
18
58
|
switch (providerName) {
|
|
@@ -39,57 +79,54 @@ export async function validateProvider(providerName) {
|
|
|
39
79
|
}
|
|
40
80
|
|
|
41
81
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
82
|
+
* Helper: Check if pipx provider is installed (Soprano, Piper)
|
|
83
|
+
* @param {string} providerName - Provider name (soprano or piper)
|
|
84
|
+
* @param {string} packageName - Package name (soprano-tts or piper-tts)
|
|
85
|
+
* @returns {Promise<{installed: boolean, message: string, checkedLocations: string[]}>}
|
|
45
86
|
*/
|
|
46
|
-
|
|
87
|
+
async function validatePipxProvider(providerName, packageName) {
|
|
47
88
|
const checkedLocations = [];
|
|
89
|
+
const binName = providerName === 'soprano' ? 'soprano' : 'piper';
|
|
90
|
+
const venvName = providerName === 'soprano' ? 'soprano-tts' : 'piper-tts';
|
|
48
91
|
|
|
49
|
-
// Check for
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
// Check for binary in PATH first (most reliable for pipx installations)
|
|
93
|
+
if (commandExistsInPath(binName)) {
|
|
94
|
+
return { installed: true, message: `${providerName} TTS detected (binary in PATH)`, checkedLocations: ['PATH'] };
|
|
95
|
+
}
|
|
96
|
+
checkedLocations.push('PATH');
|
|
54
97
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
// Check for pipx bin directory directly
|
|
99
|
+
const homeDir = os.homedir();
|
|
100
|
+
const binPath = path.join(homeDir, '.local', 'bin', binName);
|
|
101
|
+
if (isSafePathExists(binPath)) {
|
|
102
|
+
return { installed: true, message: `${providerName} TTS detected (via pipx bin)`, checkedLocations: ['~/.local/bin'] };
|
|
103
|
+
}
|
|
104
|
+
checkedLocations.push('~/.local/bin');
|
|
61
105
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
}
|
|
106
|
+
// Check for pipx venv installation directory
|
|
107
|
+
const venvPath = path.join(homeDir, '.local', 'share', 'pipx', 'venvs', venvName);
|
|
108
|
+
if (isSafePathExists(venvPath)) {
|
|
109
|
+
return { installed: true, message: `${providerName} TTS detected (via pipx venv)`, checkedLocations: ['pipx venv'] };
|
|
71
110
|
}
|
|
72
|
-
checkedLocations.push('pipx');
|
|
111
|
+
checkedLocations.push('pipx venv');
|
|
73
112
|
|
|
74
|
-
//
|
|
113
|
+
// Check Python package installations (comprehensive version detection)
|
|
75
114
|
const pythonCommands = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10', 'python3.9', 'python3.8'];
|
|
76
115
|
|
|
77
116
|
for (const pythonCmd of pythonCommands) {
|
|
78
117
|
try {
|
|
79
|
-
// Use
|
|
80
|
-
const result =
|
|
118
|
+
// Use spawnSync with array args (security: correct API usage per CLAUDE.md)
|
|
119
|
+
const result = spawnSync(pythonCmd, ['-m', 'pip', 'show', packageName], {
|
|
81
120
|
encoding: 'utf8',
|
|
82
121
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
122
|
timeout: 10000
|
|
84
123
|
});
|
|
85
124
|
|
|
86
|
-
if (result && result.trim()) {
|
|
87
|
-
checkedLocations.push(pythonCmd); // Track which Python found it
|
|
125
|
+
if (result.status === 0 && result.stdout && result.stdout.trim()) {
|
|
88
126
|
return {
|
|
89
127
|
installed: true,
|
|
90
|
-
message:
|
|
91
|
-
|
|
92
|
-
checkedCount: checkedLocations.length + pythonCommands.indexOf(pythonCmd)
|
|
128
|
+
message: `${providerName} TTS detected (Python package via ${pythonCmd})`,
|
|
129
|
+
checkedLocations: [...checkedLocations, pythonCmd]
|
|
93
130
|
};
|
|
94
131
|
}
|
|
95
132
|
} catch (error) {
|
|
@@ -97,81 +134,32 @@ export async function validateSopranoInstallation() {
|
|
|
97
134
|
}
|
|
98
135
|
}
|
|
99
136
|
|
|
100
|
-
// Build list of Python versions checked
|
|
101
137
|
checkedLocations.push(...pythonCommands);
|
|
102
138
|
|
|
103
139
|
return {
|
|
104
140
|
installed: false,
|
|
105
|
-
message:
|
|
106
|
-
error:
|
|
107
|
-
|
|
141
|
+
message: `${providerName} TTS is not installed on your system (checked: ${checkedLocations.join(', ')})`,
|
|
142
|
+
error: `${providerName.toUpperCase()}_NOT_FOUND`,
|
|
143
|
+
checkedLocations
|
|
108
144
|
};
|
|
109
145
|
}
|
|
110
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Validate Soprano TTS installation
|
|
149
|
+
* Checks multiple locations: PATH, pipx bin, pipx venv, Python packages
|
|
150
|
+
* @returns {Promise<{installed: boolean, message: string, checkedLocations?: string[], error?: string}>}
|
|
151
|
+
*/
|
|
152
|
+
export async function validateSopranoInstallation() {
|
|
153
|
+
return await validatePipxProvider('soprano', 'soprano-tts');
|
|
154
|
+
}
|
|
155
|
+
|
|
111
156
|
/**
|
|
112
157
|
* Validate Piper TTS installation
|
|
113
|
-
* Checks
|
|
114
|
-
*
|
|
115
|
-
* @returns {Promise<{installed: boolean, message: string}>}
|
|
158
|
+
* Checks multiple locations: PATH, pipx bin, pipx venv, Python packages
|
|
159
|
+
* @returns {Promise<{installed: boolean, message: string, checkedLocations?: string[], error?: string}>}
|
|
116
160
|
*/
|
|
117
161
|
export async function validatePiperInstallation() {
|
|
118
|
-
|
|
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
|
-
};
|
|
162
|
+
return await validatePipxProvider('piper', 'piper-tts');
|
|
175
163
|
}
|
|
176
164
|
|
|
177
165
|
/**
|
|
@@ -273,11 +261,17 @@ async function testSopranoRuntime() {
|
|
|
273
261
|
*/
|
|
274
262
|
async function testPiperRuntime() {
|
|
275
263
|
try {
|
|
276
|
-
|
|
264
|
+
const result = spawnSync('piper', ['--help'], {
|
|
277
265
|
encoding: 'utf8',
|
|
278
266
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
279
267
|
timeout: 5000
|
|
280
268
|
});
|
|
269
|
+
if (result.error || result.status !== 0) {
|
|
270
|
+
return {
|
|
271
|
+
working: false,
|
|
272
|
+
error: 'Piper command execution failed'
|
|
273
|
+
};
|
|
274
|
+
}
|
|
281
275
|
return { working: true };
|
|
282
276
|
} catch (e) {
|
|
283
277
|
return {
|
|
@@ -292,11 +286,17 @@ async function testPiperRuntime() {
|
|
|
292
286
|
*/
|
|
293
287
|
async function testMacOSRuntime() {
|
|
294
288
|
try {
|
|
295
|
-
|
|
289
|
+
const result = spawnSync('say', ['-f', '/dev/null'], {
|
|
296
290
|
encoding: 'utf8',
|
|
297
291
|
timeout: 5000,
|
|
298
292
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
299
293
|
});
|
|
294
|
+
if (result.error || result.status !== 0) {
|
|
295
|
+
return {
|
|
296
|
+
working: false,
|
|
297
|
+
error: 'macOS Say command execution failed'
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
300
|
return { working: true };
|
|
301
301
|
} catch (e) {
|
|
302
302
|
return {
|
|
@@ -339,52 +339,59 @@ export async function attemptProviderInstallation(providerName) {
|
|
|
339
339
|
return { success: false, message: `Unknown provider: ${providerName}` };
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
// Strategy 1: Try regular pip install (using
|
|
342
|
+
// Strategy 1: Try regular pip install (using spawnSync for correct API usage)
|
|
343
343
|
try {
|
|
344
344
|
// Show installation in progress
|
|
345
345
|
console.log(` Attempting: pip install ${pkgName}...`);
|
|
346
|
-
|
|
346
|
+
const result = spawnSync('pip', ['install', pkgName], {
|
|
347
347
|
stdio: 'inherit',
|
|
348
|
-
timeout: 60000
|
|
348
|
+
timeout: 60000
|
|
349
349
|
});
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
351
|
+
if (result.error || result.status !== 0) {
|
|
352
|
+
// Strategy 1 failed - continue to Strategy 2
|
|
353
|
+
} else {
|
|
354
|
+
// Verify installation actually worked (proves it's installed)
|
|
355
|
+
const validation = await validateProvider(providerName);
|
|
356
|
+
if (validation.installed) {
|
|
357
|
+
return {
|
|
358
|
+
success: true,
|
|
359
|
+
message: `Successfully installed via pip`,
|
|
360
|
+
command: `pip install ${pkgName}`,
|
|
361
|
+
verified: true
|
|
362
|
+
};
|
|
363
|
+
}
|
|
361
364
|
|
|
362
|
-
|
|
365
|
+
return { success: true, message: `Successfully installed via pip`, command: `pip install ${pkgName}` };
|
|
366
|
+
}
|
|
363
367
|
} catch (error) {
|
|
364
368
|
// Strategy 1 failed - continue to Strategy 2
|
|
365
|
-
// Don't check specific error messages - just try next strategy (MEDIUM #3 fix)
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
// Strategy 2: Try pipx install (recommended for PEP 668 protection)
|
|
369
372
|
try {
|
|
370
373
|
console.log(` Attempting: pipx install ${pkgName}...`);
|
|
371
|
-
|
|
374
|
+
const result = spawnSync('pipx', ['install', pkgName], {
|
|
372
375
|
stdio: 'inherit',
|
|
373
|
-
timeout: 60000
|
|
376
|
+
timeout: 60000
|
|
374
377
|
});
|
|
375
378
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
if (result.error || result.status !== 0) {
|
|
380
|
+
// Both strategies failed
|
|
381
|
+
} else {
|
|
382
|
+
// Verify installation actually worked (proves it's installed)
|
|
383
|
+
const validation = await validateProvider(providerName);
|
|
384
|
+
if (validation.installed) {
|
|
385
|
+
return {
|
|
386
|
+
success: true,
|
|
387
|
+
message: `Successfully installed via pipx`,
|
|
388
|
+
command: `pipx install ${pkgName}`,
|
|
389
|
+
verified: true
|
|
390
|
+
};
|
|
391
|
+
}
|
|
386
392
|
|
|
387
|
-
|
|
393
|
+
return { success: true, message: `Successfully installed via pipx`, command: `pipx install ${pkgName}` };
|
|
394
|
+
}
|
|
388
395
|
} catch (error) {
|
|
389
396
|
// Both strategies failed
|
|
390
397
|
}
|
|
@@ -400,14 +407,19 @@ export async function attemptProviderInstallation(providerName) {
|
|
|
400
407
|
*/
|
|
401
408
|
function getPackageInfo(pkgName) {
|
|
402
409
|
try {
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
const output = execSync('pip', ['show', pkgName], {
|
|
410
|
+
// Use spawnSync with array args (security: correct API usage per CLAUDE.md)
|
|
411
|
+
const result = spawnSync('pip', ['show', pkgName], {
|
|
406
412
|
encoding: 'utf8',
|
|
407
413
|
timeout: 10000,
|
|
408
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
414
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
409
415
|
});
|
|
410
416
|
|
|
417
|
+
if (result.error || result.status !== 0) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const output = result.stdout;
|
|
422
|
+
|
|
411
423
|
// Parse pip show output
|
|
412
424
|
const info = {};
|
|
413
425
|
const lines = output.split('\n');
|