agentvibes 5.9.0 → 5.11.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/.agentvibes/config.json +17 -12
- package/.agentvibes/install-manifest.json +98 -86
- package/.claude/audio/ui/CREDITS.txt +16 -0
- package/.claude/audio/ui/bling-success.wav +0 -0
- package/.claude/commands/agent-vibes/receiver.md +64 -0
- package/.claude/config/audio-effects.cfg +3 -3
- package/.claude/config/personality.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -1
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-processor.sh +57 -18
- package/.claude/hooks/kokoro-installer.sh +117 -0
- package/.claude/hooks/kokoro-server.py +219 -0
- package/.claude/hooks/kokoro-tts.py +141 -0
- package/.claude/hooks/piper-download-voices.sh +39 -16
- package/.claude/hooks/piper-installer.sh +11 -6
- package/.claude/hooks/piper-voice-manager.sh +6 -6
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
- package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
- package/.claude/hooks/play-tts-kokoro.sh +127 -0
- package/.claude/hooks/play-tts-piper.sh +20 -13
- package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
- package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
- package/.claude/hooks/play-tts.sh +78 -2
- package/.claude/hooks/provider-commands.sh +71 -22
- package/.claude/hooks/provider-manager.sh +15 -7
- package/.claude/hooks/voice-manager.sh +6 -0
- package/.claude/hooks-windows/kokoro-server.py +219 -0
- package/.claude/hooks-windows/kokoro-tts.py +107 -0
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
- package/.claude/hooks-windows/play-tts.ps1 +29 -1
- package/README.md +12 -86
- package/RELEASE_NOTES.md +60 -0
- package/mcp-server/server.py +17 -7
- package/package.json +4 -3
- package/src/commands/install-mcp.js +730 -476
- package/src/console/app.js +28 -12
- package/src/console/audio-env.js +85 -1
- package/src/console/navigation.js +4 -0
- package/src/console/tabs/agents-tab.js +18 -12
- package/src/console/tabs/music-tab.js +7 -25
- 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 +1708 -124
- package/src/console/tabs/voices-tab.js +76 -92
- package/src/console/widgets/format-utils.js +14 -2
- package/src/console/widgets/help-bar.js +55 -0
- package/src/console/widgets/personality-picker.js +2 -2
- package/src/console/widgets/reverb-picker.js +429 -41
- package/src/console/widgets/track-picker.js +60 -51
- package/src/i18n/de.js +204 -203
- package/src/i18n/en.js +1 -0
- package/src/i18n/es.js +204 -203
- package/src/i18n/fr.js +204 -203
- package/src/i18n/hi.js +204 -203
- package/src/i18n/ja.js +204 -203
- package/src/i18n/ko.js +204 -203
- package/src/i18n/pt.js +204 -203
- package/src/i18n/strings.js +54 -54
- package/src/i18n/zh-CN.js +204 -203
- package/src/installer.js +127 -34
- package/src/services/provider-service.js +178 -143
- package/src/services/provider-voice-catalog.js +126 -0
- package/src/services/tts-engine-service.js +53 -4
- package/src/utils/audio-duration-validator.js +341 -298
- package/src/utils/list-formatter.js +200 -194
- package/src/utils/platform-resolver.js +369 -0
- package/src/utils/preview-list-prompt.js +8 -0
- package/src/utils/provider-validator.js +79 -9
- package/templates/agentvibes-receiver.sh +6 -2
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes Cross-Platform Resolver
|
|
3
|
+
*
|
|
4
|
+
* Implements the Agent Vibes Cross-Platform Contract v1.0.
|
|
5
|
+
* Single source of truth for binary resolution, path conventions, and env var interface.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order (authoritative):
|
|
8
|
+
* 1. ENV_OVERRIDE — AGENTVIBES_*_PATH env var; if invalid, fail HARD (no fallthrough)
|
|
9
|
+
* 2. which/where — first result that passes validation
|
|
10
|
+
* 3. HINT_PATHS — platform hint list in order; first valid wins
|
|
11
|
+
* 4. FAIL — structured error, exit code 2
|
|
12
|
+
*
|
|
13
|
+
* Supported platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64
|
|
14
|
+
* WSL2 is treated as linux-x64.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from 'child_process';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
// ─── Platform Detection ───────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function isWSL() {
|
|
25
|
+
try {
|
|
26
|
+
return /microsoft|wsl/i.test(fs.readFileSync('/proc/version', 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect the current platform ID.
|
|
34
|
+
* Set AGENTVIBES_PLATFORM to override (CI / testing only).
|
|
35
|
+
* @returns {string} One of: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64, unknown
|
|
36
|
+
*/
|
|
37
|
+
export function detectPlatform() {
|
|
38
|
+
const forced = process.env.AGENTVIBES_PLATFORM;
|
|
39
|
+
if (forced) return forced;
|
|
40
|
+
|
|
41
|
+
const p = process.platform;
|
|
42
|
+
const arch = process.arch;
|
|
43
|
+
|
|
44
|
+
if (p === 'linux' && isWSL()) return arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
|
|
45
|
+
if (p === 'darwin') return arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
46
|
+
if (p === 'linux') return arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
|
|
47
|
+
if (p === 'win32') return 'win32-x64';
|
|
48
|
+
return 'unknown';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Path Hint Lists (only hardcoded paths in the entire codebase) ────────────
|
|
52
|
+
|
|
53
|
+
const PIPER_HINTS = {
|
|
54
|
+
'darwin-arm64': () => [
|
|
55
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'piper'),
|
|
56
|
+
path.join(os.homedir(), '.local', 'bin', 'piper'),
|
|
57
|
+
path.join(os.homedir(), '.local', 'share', 'pipx', 'venvs', 'piper-tts', 'bin', 'piper'),
|
|
58
|
+
'/opt/homebrew/bin/piper',
|
|
59
|
+
'/usr/local/bin/piper',
|
|
60
|
+
],
|
|
61
|
+
'darwin-x64': () => [
|
|
62
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'piper'),
|
|
63
|
+
path.join(os.homedir(), '.local', 'bin', 'piper'),
|
|
64
|
+
path.join(os.homedir(), '.local', 'share', 'pipx', 'venvs', 'piper-tts', 'bin', 'piper'),
|
|
65
|
+
'/usr/local/bin/piper',
|
|
66
|
+
'/opt/homebrew/bin/piper',
|
|
67
|
+
],
|
|
68
|
+
'linux-x64': () => [
|
|
69
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'piper'),
|
|
70
|
+
path.join(os.homedir(), '.local', 'bin', 'piper'),
|
|
71
|
+
path.join(os.homedir(), '.local', 'share', 'pipx', 'venvs', 'piper-tts', 'bin', 'piper'),
|
|
72
|
+
'/usr/bin/piper',
|
|
73
|
+
'/usr/local/bin/piper',
|
|
74
|
+
'/snap/bin/piper',
|
|
75
|
+
],
|
|
76
|
+
'linux-arm64': () => [
|
|
77
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'piper'),
|
|
78
|
+
path.join(os.homedir(), '.local', 'bin', 'piper'),
|
|
79
|
+
path.join(os.homedir(), '.local', 'share', 'pipx', 'venvs', 'piper-tts', 'bin', 'piper'),
|
|
80
|
+
'/usr/bin/piper',
|
|
81
|
+
'/usr/local/bin/piper',
|
|
82
|
+
],
|
|
83
|
+
'win32-x64': () => {
|
|
84
|
+
const appdata = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
85
|
+
const localappdata = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
86
|
+
const programfiles = process.env.PROGRAMFILES || path.join('C:', 'Program Files');
|
|
87
|
+
return [
|
|
88
|
+
path.join(appdata, 'AgentVibes', 'bin', 'piper.exe'),
|
|
89
|
+
path.join(localappdata, 'AgentVibes', 'bin', 'piper.exe'),
|
|
90
|
+
path.join(programfiles, 'AgentVibes', 'bin', 'piper.exe'),
|
|
91
|
+
];
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const FFMPEG_HINTS = {
|
|
96
|
+
'darwin-arm64': () => [
|
|
97
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'ffmpeg'),
|
|
98
|
+
'/opt/homebrew/bin/ffmpeg',
|
|
99
|
+
'/usr/local/bin/ffmpeg',
|
|
100
|
+
],
|
|
101
|
+
'darwin-x64': () => [
|
|
102
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'ffmpeg'),
|
|
103
|
+
'/usr/local/bin/ffmpeg',
|
|
104
|
+
'/opt/homebrew/bin/ffmpeg',
|
|
105
|
+
],
|
|
106
|
+
'linux-x64': () => [
|
|
107
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'ffmpeg'),
|
|
108
|
+
'/usr/bin/ffmpeg',
|
|
109
|
+
'/usr/local/bin/ffmpeg',
|
|
110
|
+
],
|
|
111
|
+
'linux-arm64': () => [
|
|
112
|
+
path.join(os.homedir(), '.agentvibes', 'bin', 'ffmpeg'),
|
|
113
|
+
'/usr/bin/ffmpeg',
|
|
114
|
+
'/usr/local/bin/ffmpeg',
|
|
115
|
+
],
|
|
116
|
+
'win32-x64': () => {
|
|
117
|
+
const appdata = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
118
|
+
const localappdata = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
119
|
+
const programfiles = process.env.PROGRAMFILES || path.join('C:', 'Program Files');
|
|
120
|
+
return [
|
|
121
|
+
path.join(appdata, 'AgentVibes', 'bin', 'ffmpeg.exe'),
|
|
122
|
+
path.join(localappdata, 'AgentVibes', 'bin', 'ffmpeg.exe'),
|
|
123
|
+
path.join(programfiles, 'ffmpeg', 'bin', 'ffmpeg.exe'),
|
|
124
|
+
];
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Derive ffprobe hints by substituting the binary name in every ffmpeg path.
|
|
129
|
+
// ffprobe is always co-located with ffmpeg so sharing the directory list is correct.
|
|
130
|
+
function deriveBinaryHints(templateHints, binaryName) {
|
|
131
|
+
const result = {};
|
|
132
|
+
for (const [plat, fn] of Object.entries(templateHints)) {
|
|
133
|
+
result[plat] = () => fn().map(p => {
|
|
134
|
+
const dir = path.dirname(p);
|
|
135
|
+
const ext = path.extname(p);
|
|
136
|
+
return path.join(dir, binaryName + ext);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const FFPROBE_HINTS = deriveBinaryHints(FFMPEG_HINTS, 'ffprobe');
|
|
143
|
+
|
|
144
|
+
const BINARY_HINTS = { piper: PIPER_HINTS, ffmpeg: FFMPEG_HINTS, ffprobe: FFPROBE_HINTS };
|
|
145
|
+
|
|
146
|
+
/** Canonical env var names — only override surface permitted by the contract */
|
|
147
|
+
export const ENV_VARS = {
|
|
148
|
+
piper: 'AGENTVIBES_PIPER_PATH',
|
|
149
|
+
ffmpeg: 'AGENTVIBES_FFMPEG_PATH',
|
|
150
|
+
ffprobe: 'AGENTVIBES_FFPROBE_PATH',
|
|
151
|
+
config_dir: 'AGENTVIBES_CONFIG_DIR',
|
|
152
|
+
data_dir: 'AGENTVIBES_DATA_DIR',
|
|
153
|
+
cache_dir: 'AGENTVIBES_CACHE_DIR',
|
|
154
|
+
voice_dir: 'AGENTVIBES_VOICE_DIR',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ─── Binary Validation ────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate that a candidate binary path is a real, executable, working binary.
|
|
161
|
+
* @returns {{ valid: boolean, reason?: string }}
|
|
162
|
+
*/
|
|
163
|
+
export function validateBinary(binaryPath, binaryName) {
|
|
164
|
+
let stat;
|
|
165
|
+
try {
|
|
166
|
+
stat = fs.statSync(binaryPath);
|
|
167
|
+
} catch {
|
|
168
|
+
return { valid: false, reason: 'not_found' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!stat.isFile()) return { valid: false, reason: 'not_a_file' };
|
|
172
|
+
|
|
173
|
+
if (process.platform !== 'win32') {
|
|
174
|
+
try {
|
|
175
|
+
fs.accessSync(binaryPath, fs.constants.X_OK);
|
|
176
|
+
} catch {
|
|
177
|
+
return { valid: false, reason: 'not_executable' };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Smoke test: binary must respond to --version (or -version for ffmpeg/ffprobe)
|
|
182
|
+
const versionFlag = (binaryName === 'ffmpeg' || binaryName === 'ffprobe') ? '-version' : '--version';
|
|
183
|
+
try {
|
|
184
|
+
execFileSync(binaryPath, [versionFlag], { stdio: 'pipe', timeout: 3000 });
|
|
185
|
+
return { valid: true };
|
|
186
|
+
} catch {
|
|
187
|
+
return { valid: false, reason: 'version_check_failed' };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Find binary using which (POSIX) or where (Windows).
|
|
193
|
+
* Returns the realpath of the first result, or null.
|
|
194
|
+
*/
|
|
195
|
+
export function whichBinary(name) {
|
|
196
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
197
|
+
try {
|
|
198
|
+
const result = execFileSync(cmd, [name], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
|
|
199
|
+
const first = result.trim().split('\n')[0].trim();
|
|
200
|
+
if (!first) return null;
|
|
201
|
+
// Resolve symlinks to get the real binary path
|
|
202
|
+
return fs.realpathSync(first);
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── Binary Resolution (4-step contract) ─────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve a binary following the contract resolution order.
|
|
212
|
+
* Throws a structured error if resolution fails.
|
|
213
|
+
*
|
|
214
|
+
* @param {'piper'|'ffmpeg'|'ffprobe'} binaryName
|
|
215
|
+
* @returns {{ path: string, source: string }}
|
|
216
|
+
*/
|
|
217
|
+
export function resolveBinary(binaryName) {
|
|
218
|
+
const platformId = detectPlatform();
|
|
219
|
+
const envVar = ENV_VARS[binaryName];
|
|
220
|
+
const tried = [];
|
|
221
|
+
|
|
222
|
+
// Step 1: ENV_OVERRIDE — if set, it's absolute. Invalid = hard fail, no fallthrough.
|
|
223
|
+
if (envVar && process.env[envVar]) {
|
|
224
|
+
const overridePath = process.env[envVar];
|
|
225
|
+
const validation = validateBinary(overridePath, binaryName);
|
|
226
|
+
tried.push({ step: 'ENV_OVERRIDE', path: overridePath, ...validation });
|
|
227
|
+
if (!validation.valid) {
|
|
228
|
+
const err = new Error(
|
|
229
|
+
`[AgentVibes] RESOLUTION_FAILURE\n` +
|
|
230
|
+
` binary: ${binaryName}\n` +
|
|
231
|
+
` error: ENV_OVERRIDE_INVALID\n` +
|
|
232
|
+
` platform: ${platformId}\n` +
|
|
233
|
+
` ${envVar}=${overridePath} (${validation.reason})\n` +
|
|
234
|
+
` fix: Correct the ${envVar} environment variable or unset it`
|
|
235
|
+
);
|
|
236
|
+
err.code = 'ENV_OVERRIDE_INVALID';
|
|
237
|
+
err.tried = tried;
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
return { path: overridePath, source: 'env_override' };
|
|
241
|
+
}
|
|
242
|
+
tried.push({ step: 'ENV_OVERRIDE', path: 'not set' });
|
|
243
|
+
|
|
244
|
+
// Step 2: which/where — respect what the user already has configured
|
|
245
|
+
const whichResult = whichBinary(binaryName);
|
|
246
|
+
if (whichResult) {
|
|
247
|
+
const validation = validateBinary(whichResult, binaryName);
|
|
248
|
+
tried.push({ step: 'WHICH', path: whichResult, ...validation });
|
|
249
|
+
if (validation.valid) {
|
|
250
|
+
return { path: whichResult, source: 'which' };
|
|
251
|
+
}
|
|
252
|
+
// Exists in PATH but failed validation — log and continue to hints
|
|
253
|
+
} else {
|
|
254
|
+
tried.push({ step: 'WHICH', path: 'not found' });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 3: Platform hint list — last resort before failure
|
|
258
|
+
const hintFn = BINARY_HINTS[binaryName]?.[platformId];
|
|
259
|
+
if (hintFn) {
|
|
260
|
+
const hints = hintFn();
|
|
261
|
+
for (let i = 0; i < hints.length; i++) {
|
|
262
|
+
const hintPath = hints[i];
|
|
263
|
+
const validation = validateBinary(hintPath, binaryName);
|
|
264
|
+
tried.push({ step: `HINT[${i}]`, path: hintPath, ...validation });
|
|
265
|
+
if (validation.valid) {
|
|
266
|
+
return { path: hintPath, source: `hint[${i}]` };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Step 4: FAIL — structured error with full audit trail
|
|
272
|
+
const triedFormatted = tried
|
|
273
|
+
.map(t => ` - ${t.step.padEnd(12)}: ${t.path}${t.reason ? ` → ${t.reason}` : ''}`)
|
|
274
|
+
.join('\n');
|
|
275
|
+
const err = new Error(
|
|
276
|
+
`[AgentVibes] RESOLUTION_FAILURE\n` +
|
|
277
|
+
` binary: ${binaryName}\n` +
|
|
278
|
+
` error: BINARY_NOT_FOUND\n` +
|
|
279
|
+
` platform: ${platformId}\n` +
|
|
280
|
+
` tried:\n${triedFormatted}\n` +
|
|
281
|
+
` fix: Install ${binaryName} or set ${envVar} to the binary path`
|
|
282
|
+
);
|
|
283
|
+
err.code = 'BINARY_NOT_FOUND';
|
|
284
|
+
err.binary = binaryName;
|
|
285
|
+
err.platform = platformId;
|
|
286
|
+
err.tried = tried;
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Directory Resolution ─────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Resolve the voice model directory (never contains tilde on return).
|
|
294
|
+
* Windows: %LOCALAPPDATA%\AgentVibes\voices
|
|
295
|
+
* POSIX: $XDG_DATA_HOME/agentvibes/voices or ~/.local/share/agentvibes/voices
|
|
296
|
+
*/
|
|
297
|
+
export function resolveVoiceDir() {
|
|
298
|
+
const override = process.env[ENV_VARS.voice_dir];
|
|
299
|
+
if (override) return path.resolve(override);
|
|
300
|
+
return path.join(resolveDataDir(), 'voices');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resolve the data directory.
|
|
305
|
+
* Uses LOCALAPPDATA on Windows, XDG_DATA_HOME or ~/.local/share on POSIX.
|
|
306
|
+
*/
|
|
307
|
+
export function resolveDataDir() {
|
|
308
|
+
const override = process.env[ENV_VARS.data_dir];
|
|
309
|
+
if (override) return path.resolve(override);
|
|
310
|
+
|
|
311
|
+
const platformId = detectPlatform();
|
|
312
|
+
if (platformId === 'win32-x64') {
|
|
313
|
+
const localappdata = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
314
|
+
return path.join(localappdata, 'AgentVibes');
|
|
315
|
+
}
|
|
316
|
+
const xdgData = process.env.XDG_DATA_HOME;
|
|
317
|
+
if (xdgData) return path.join(xdgData, 'agentvibes');
|
|
318
|
+
return path.join(os.homedir(), '.local', 'share', 'agentvibes');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Resolve the config directory.
|
|
323
|
+
* Uses APPDATA on Windows, XDG_CONFIG_HOME or ~/.config on POSIX.
|
|
324
|
+
*/
|
|
325
|
+
export function resolveConfigDir() {
|
|
326
|
+
const override = process.env[ENV_VARS.config_dir];
|
|
327
|
+
if (override) return path.resolve(override);
|
|
328
|
+
|
|
329
|
+
const platformId = detectPlatform();
|
|
330
|
+
if (platformId === 'win32-x64') {
|
|
331
|
+
const appdata = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
332
|
+
return path.join(appdata, 'AgentVibes');
|
|
333
|
+
}
|
|
334
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
335
|
+
if (xdgConfig) return path.join(xdgConfig, 'agentvibes');
|
|
336
|
+
return path.join(os.homedir(), '.config', 'agentvibes');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─── PATH Augmentation Helper ─────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Return extra PATH directories for the current platform.
|
|
343
|
+
* MCP servers launched by Claude Desktop inherit a sanitized PATH that omits
|
|
344
|
+
* Homebrew (Mac) and pipx (POSIX) locations — this list covers those gaps.
|
|
345
|
+
* Never includes directories already on PATH.
|
|
346
|
+
* @returns {string[]} List of absolute directory paths
|
|
347
|
+
*/
|
|
348
|
+
export function getPathAugmentation() {
|
|
349
|
+
const platformId = detectPlatform();
|
|
350
|
+
const extra = [];
|
|
351
|
+
|
|
352
|
+
if (platformId === 'darwin-arm64') {
|
|
353
|
+
extra.push('/opt/homebrew/bin', '/usr/local/bin');
|
|
354
|
+
} else if (platformId === 'darwin-x64') {
|
|
355
|
+
extra.push('/usr/local/bin', '/opt/homebrew/bin');
|
|
356
|
+
}
|
|
357
|
+
// Linux/WSL: ~/.local/bin is the only reliable extra location
|
|
358
|
+
if (platformId === 'linux-x64' || platformId === 'linux-arm64') {
|
|
359
|
+
extra.push(path.join(os.homedir(), '.local', 'bin'));
|
|
360
|
+
}
|
|
361
|
+
// pipx venv — all POSIX platforms
|
|
362
|
+
if (platformId !== 'win32-x64') {
|
|
363
|
+
extra.push(path.join(os.homedir(), '.local', 'share', 'pipx', 'venvs', 'piper-tts', 'bin'));
|
|
364
|
+
extra.push(path.join(os.homedir(), '.local', 'bin'));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Deduplicate, preserving order
|
|
368
|
+
return [...new Set(extra)];
|
|
369
|
+
}
|
|
@@ -129,6 +129,14 @@ export async function createPreviewListPrompt(inquirer, config) {
|
|
|
129
129
|
} catch (e) {
|
|
130
130
|
// Failed to restore raw mode, but we're exiting anyway
|
|
131
131
|
}
|
|
132
|
+
// readline.emitKeypressEvents() put stdin into flowing mode; pause it so the
|
|
133
|
+
// open stdin handle no longer keeps the event loop alive after the prompt
|
|
134
|
+
// resolves (otherwise the process lingers until stdin is otherwise closed).
|
|
135
|
+
try {
|
|
136
|
+
process.stdin.pause();
|
|
137
|
+
} catch (e) {
|
|
138
|
+
// stdin may already be closed; nothing more to do
|
|
139
|
+
}
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
}
|
|
@@ -15,7 +15,7 @@ import os from 'node:os'; // For os.homedir() to prevent HOME injection attacks
|
|
|
15
15
|
*/
|
|
16
16
|
function commandExistsInPath(command) {
|
|
17
17
|
// SECURITY: Use spawnSync instead of execSync+shell to prevent command injection (#126)
|
|
18
|
-
const result = spawnSync('which', [command], {
|
|
18
|
+
const result = spawnSync('which', [command], { // NOSONAR
|
|
19
19
|
encoding: 'utf8',
|
|
20
20
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
21
21
|
});
|
|
@@ -55,6 +55,10 @@ export async function validateProvider(providerName) {
|
|
|
55
55
|
return await validateSopranoInstallation();
|
|
56
56
|
case 'piper':
|
|
57
57
|
return await validatePiperInstallation();
|
|
58
|
+
case 'kokoro':
|
|
59
|
+
return await validateKokoroInstallation();
|
|
60
|
+
case 'elevenlabs':
|
|
61
|
+
return await validateElevenLabsInstallation();
|
|
58
62
|
case 'macos':
|
|
59
63
|
return await validateMacOSProvider();
|
|
60
64
|
case 'sapi':
|
|
@@ -143,6 +147,66 @@ async function validatePipxProvider(providerName, packageName) {
|
|
|
143
147
|
};
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Validate Kokoro TTS installation (kokoro-onnx Python package)
|
|
152
|
+
* @returns {Promise<{installed: boolean, message: string, checkedLocations?: string[], error?: string}>}
|
|
153
|
+
*/
|
|
154
|
+
export async function validateKokoroInstallation() {
|
|
155
|
+
const pythonCommands = process.platform === 'win32'
|
|
156
|
+
? ['py', 'python', 'python3']
|
|
157
|
+
: ['python3', 'python'];
|
|
158
|
+
|
|
159
|
+
for (const pythonCmd of pythonCommands) {
|
|
160
|
+
try {
|
|
161
|
+
const result = spawnSync(pythonCmd, ['-c', 'import kokoro; import soundfile; import numpy'], { // NOSONAR
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
164
|
+
timeout: 8000,
|
|
165
|
+
});
|
|
166
|
+
if (result.status === 0) {
|
|
167
|
+
return { installed: true, message: `Kokoro TTS detected (via ${pythonCmd})`, checkedLocations: [pythonCmd] };
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// try next
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
installed: false,
|
|
176
|
+
message: 'Kokoro TTS not installed. Run: pip install kokoro-onnx soundfile numpy',
|
|
177
|
+
error: 'KOKORO_NOT_FOUND',
|
|
178
|
+
checkedLocations: pythonCommands,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate ElevenLabs configuration (API key presence)
|
|
184
|
+
* @returns {Promise<{installed: boolean, message: string, error?: string}>}
|
|
185
|
+
*/
|
|
186
|
+
export async function validateElevenLabsInstallation() {
|
|
187
|
+
// Check environment variable
|
|
188
|
+
if (process.env.ELEVENLABS_API_KEY && process.env.ELEVENLABS_API_KEY.length > 0) {
|
|
189
|
+
return { installed: true, message: 'ElevenLabs API key found in environment' };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check key file
|
|
193
|
+
const keyFile = path.join(os.homedir(), '.agentvibes', 'elevenlabs-key.txt');
|
|
194
|
+
if (isSafePathExists(keyFile)) {
|
|
195
|
+
try {
|
|
196
|
+
const key = fs.readFileSync(keyFile, 'utf8').trim();
|
|
197
|
+
if (key.length > 0) {
|
|
198
|
+
return { installed: true, message: 'ElevenLabs API key found in ~/.agentvibes/elevenlabs-key.txt' };
|
|
199
|
+
}
|
|
200
|
+
} catch { /* fall through */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
installed: false,
|
|
205
|
+
message: 'ElevenLabs API key not set. Set ELEVENLABS_API_KEY env var or save key to ~/.agentvibes/elevenlabs-key.txt',
|
|
206
|
+
error: 'ELEVENLABS_NO_KEY',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
146
210
|
/**
|
|
147
211
|
* Validate Soprano TTS installation
|
|
148
212
|
* Checks multiple locations: PATH, pipx bin, pipx venv, Python packages
|
|
@@ -176,7 +240,7 @@ export async function validateMacOSProvider() {
|
|
|
176
240
|
|
|
177
241
|
try {
|
|
178
242
|
// SECURITY: Use spawnSync instead of execSync+shell to prevent command injection (#126)
|
|
179
|
-
const result = spawnSync('which', ['say'], {
|
|
243
|
+
const result = spawnSync('which', ['say'], { // NOSONAR
|
|
180
244
|
encoding: 'utf8',
|
|
181
245
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
182
246
|
});
|
|
@@ -272,7 +336,7 @@ export async function testProviderRuntime(providerName) {
|
|
|
272
336
|
async function testSopranoRuntime() {
|
|
273
337
|
try {
|
|
274
338
|
// Try a quick soprano import check
|
|
275
|
-
const result = spawnSync('python3', ['-c', 'import soprano; print("OK")'], {
|
|
339
|
+
const result = spawnSync('python3', ['-c', 'import soprano; print("OK")'], { // NOSONAR
|
|
276
340
|
timeout: 5000,
|
|
277
341
|
encoding: 'utf8'
|
|
278
342
|
});
|
|
@@ -298,7 +362,7 @@ async function testSopranoRuntime() {
|
|
|
298
362
|
*/
|
|
299
363
|
async function testPiperRuntime() {
|
|
300
364
|
try {
|
|
301
|
-
const result = spawnSync('piper', ['--help'], {
|
|
365
|
+
const result = spawnSync('piper', ['--help'], { // NOSONAR
|
|
302
366
|
encoding: 'utf8',
|
|
303
367
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
304
368
|
timeout: 5000
|
|
@@ -323,7 +387,7 @@ async function testPiperRuntime() {
|
|
|
323
387
|
*/
|
|
324
388
|
async function testMacOSRuntime() {
|
|
325
389
|
try {
|
|
326
|
-
const result = spawnSync('say', ['-f', '/dev/null'], {
|
|
390
|
+
const result = spawnSync('say', ['-f', '/dev/null'], { // NOSONAR
|
|
327
391
|
encoding: 'utf8',
|
|
328
392
|
timeout: 5000,
|
|
329
393
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
@@ -352,6 +416,8 @@ export function getProviderInstallCommand(providerName) {
|
|
|
352
416
|
const commands = {
|
|
353
417
|
soprano: 'pip install soprano-tts',
|
|
354
418
|
piper: 'pip install piper-tts',
|
|
419
|
+
kokoro: 'pip install kokoro-onnx soundfile numpy',
|
|
420
|
+
elevenlabs: 'export ELEVENLABS_API_KEY=your_key # get free key at elevenlabs.io',
|
|
355
421
|
// macOS Say and Windows SAPI are built-in, no install needed
|
|
356
422
|
};
|
|
357
423
|
|
|
@@ -369,7 +435,9 @@ export async function attemptProviderInstallation(providerName) {
|
|
|
369
435
|
const providers = {
|
|
370
436
|
soprano: 'soprano-tts',
|
|
371
437
|
piper: 'piper-tts',
|
|
438
|
+
kokoro: 'kokoro-onnx',
|
|
372
439
|
'windows-piper': 'piper-windows-exe'
|
|
440
|
+
// elevenlabs requires API key, not pip install — handled separately
|
|
373
441
|
};
|
|
374
442
|
|
|
375
443
|
const pkgName = providers[providerName];
|
|
@@ -394,7 +462,7 @@ export async function attemptProviderInstallation(providerName) {
|
|
|
394
462
|
// Strategy 1: Try regular pip install (using spawnSync for correct API usage)
|
|
395
463
|
try {
|
|
396
464
|
console.log(` Attempting: pip install ${pkgName}...`);
|
|
397
|
-
const result = spawnSync('pip', ['install', pkgName], {
|
|
465
|
+
const result = spawnSync('pip', ['install', pkgName], { // NOSONAR
|
|
398
466
|
stdio: 'inherit',
|
|
399
467
|
timeout: 60000
|
|
400
468
|
});
|
|
@@ -421,7 +489,7 @@ export async function attemptProviderInstallation(providerName) {
|
|
|
421
489
|
// Strategy 2: Try pipx install (recommended for PEP 668 protection)
|
|
422
490
|
try {
|
|
423
491
|
console.log(` Attempting: pipx install ${pkgName}...`);
|
|
424
|
-
const result = spawnSync('pipx', ['install', pkgName], {
|
|
492
|
+
const result = spawnSync('pipx', ['install', pkgName], { // NOSONAR
|
|
425
493
|
stdio: 'inherit',
|
|
426
494
|
timeout: 60000
|
|
427
495
|
});
|
|
@@ -449,7 +517,7 @@ export async function attemptProviderInstallation(providerName) {
|
|
|
449
517
|
if (isWindows) {
|
|
450
518
|
try {
|
|
451
519
|
console.log(` Attempting: py -m pip install ${pkgName}...`);
|
|
452
|
-
const result = spawnSync('py', ['-m', 'pip', 'install', pkgName], {
|
|
520
|
+
const result = spawnSync('py', ['-m', 'pip', 'install', pkgName], { // NOSONAR
|
|
453
521
|
stdio: 'inherit',
|
|
454
522
|
timeout: 60000
|
|
455
523
|
});
|
|
@@ -487,7 +555,7 @@ export async function attemptProviderInstallation(providerName) {
|
|
|
487
555
|
function getPackageInfo(pkgName) {
|
|
488
556
|
try {
|
|
489
557
|
// Use spawnSync with array args (security: correct API usage per CLAUDE.md)
|
|
490
|
-
const result = spawnSync('pip', ['show', pkgName], {
|
|
558
|
+
const result = spawnSync('pip', ['show', pkgName], { // NOSONAR
|
|
491
559
|
encoding: 'utf8',
|
|
492
560
|
timeout: 10000,
|
|
493
561
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
@@ -538,6 +606,8 @@ export function getProviderDisplayName(providerName) {
|
|
|
538
606
|
const names = {
|
|
539
607
|
soprano: 'Soprano TTS',
|
|
540
608
|
piper: 'Piper TTS',
|
|
609
|
+
kokoro: 'Kokoro TTS',
|
|
610
|
+
elevenlabs: 'ElevenLabs',
|
|
541
611
|
macos: 'macOS Say',
|
|
542
612
|
sapi: 'Windows SAPI',
|
|
543
613
|
'windows-sapi': 'Windows SAPI',
|
|
@@ -119,7 +119,7 @@ fi
|
|
|
119
119
|
|
|
120
120
|
# Validate provider
|
|
121
121
|
case "$PROVIDER" in
|
|
122
|
-
piper|soprano|macos|windows-sapi) ;;
|
|
122
|
+
piper|soprano|macos|windows-sapi|kokoro|elevenlabs) ;;
|
|
123
123
|
*) PROVIDER="piper" ;;
|
|
124
124
|
esac
|
|
125
125
|
|
|
@@ -226,6 +226,10 @@ if [[ "${AGENTVIBES_DEBUG:-0}" == "1" ]]; then
|
|
|
226
226
|
fi
|
|
227
227
|
|
|
228
228
|
echo "🎵 Playing via AgentVibes: ${TEXT:0:50}..." >&2
|
|
229
|
-
|
|
229
|
+
# AGENTVIBES_FORCE_PROVIDER lets the sender specify which engine the receiver
|
|
230
|
+
# should use (e.g. kokoro, elevenlabs). The receiver's tts-provider.txt takes
|
|
231
|
+
# precedence if already set to a non-default value; FORCE_PROVIDER is the
|
|
232
|
+
# fallback when the receiver has no local preference configured.
|
|
233
|
+
AGENTVIBES_FORCE_PROVIDER="$PROVIDER" bash "$PLAY_TTS" "$TEXT" "$VOICE"
|
|
230
234
|
|
|
231
235
|
exit 0
|