agentvibes 5.9.0 → 5.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/config.json +3 -12
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg +4 -5
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-enabled.txt +1 -1
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-cache-utils.sh +0 -0
- package/.claude/hooks/audio-processor.sh +0 -0
- package/.claude/hooks/background-music-manager.sh +0 -0
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +0 -0
- package/.claude/hooks/bmad-tts-injector.sh +0 -0
- package/.claude/hooks/bmad-voice-manager.sh +0 -0
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
- package/.claude/hooks/clawdbot-receiver.sh +0 -0
- package/.claude/hooks/clean-audio-cache.sh +0 -0
- package/.claude/hooks/cleanup-cache.sh +0 -0
- package/.claude/hooks/configure-rdp-mode.sh +0 -0
- package/.claude/hooks/download-extra-voices.sh +0 -0
- package/.claude/hooks/effects-manager.sh +0 -0
- package/.claude/hooks/github-star-reminder.sh +0 -0
- package/.claude/hooks/language-manager.sh +0 -0
- package/.claude/hooks/learn-manager.sh +0 -0
- package/.claude/hooks/macos-voice-manager.sh +0 -0
- package/.claude/hooks/migrate-background-music.sh +0 -0
- package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
- package/.claude/hooks/optimize-background-music.sh +0 -0
- package/.claude/hooks/path-resolver.sh +0 -0
- package/.claude/hooks/personality-manager.sh +0 -0
- package/.claude/hooks/piper-download-voices.sh +0 -0
- package/.claude/hooks/piper-installer.sh +0 -0
- package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
- package/.claude/hooks/piper-voice-manager.sh +0 -0
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
- package/.claude/hooks/play-tts-enhanced.sh +0 -0
- package/.claude/hooks/play-tts-macos.sh +0 -0
- package/.claude/hooks/play-tts-piper.sh +20 -13
- package/.claude/hooks/play-tts-soprano.sh +0 -0
- package/.claude/hooks/play-tts-ssh-remote.sh +0 -0
- package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
- package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
- package/.claude/hooks/play-tts.sh +0 -0
- package/.claude/hooks/prepare-release.sh +0 -0
- package/.claude/hooks/provider-commands.sh +0 -0
- package/.claude/hooks/provider-manager.sh +0 -0
- package/.claude/hooks/replay-target-audio.sh +0 -0
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +0 -0
- package/.claude/hooks/session-start-tts.sh +0 -0
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +0 -0
- package/.claude/hooks/stop-tts.sh +0 -0
- package/.claude/hooks/termux-installer.sh +0 -0
- package/.claude/hooks/translate-manager.sh +0 -0
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +0 -0
- package/.claude/hooks/tts-queue.sh +0 -0
- package/.claude/hooks/verbosity-manager.sh +0 -0
- package/.claude/hooks/voice-manager.sh +6 -0
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.mcp.json +19 -6
- package/README.md +1 -1
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +0 -0
- package/bin/agentvibes.js +0 -0
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +0 -0
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +0 -0
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +0 -0
- package/mcp-server/server.py +1807 -1797
- package/mcp-server/test_server.py +0 -0
- package/package.json +2 -2
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +730 -476
- package/src/console/app.js +3 -3
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/tabs/agents-tab.js +6 -6
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/music-tab.js +1 -1
- package/src/console/tabs/readme-tab.js +272 -272
- 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 +10 -10
- package/src/console/tabs/voices-tab.js +4 -4
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +2 -2
- package/src/console/widgets/reverb-picker.js +1 -1
- package/src/i18n/de.js +202 -202
- package/src/i18n/es.js +202 -202
- package/src/i18n/fr.js +202 -202
- package/src/i18n/hi.js +202 -202
- package/src/i18n/ja.js +202 -202
- package/src/i18n/ko.js +202 -202
- package/src/i18n/pt.js +202 -202
- package/src/i18n/strings.js +54 -54
- package/src/i18n/zh-CN.js +202 -202
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +32 -27
- package/src/services/config-service.js +264 -264
- package/src/services/language-service.js +47 -47
- package/src/services/provider-service.js +143 -143
- package/src/services/tts-engine-service.js +2 -2
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +200 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/platform-resolver.js +369 -0
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +9 -9
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +231 -231
- package/templates/audio/welcome-music.mp3 +0 -0
- package/.agentvibes/install-manifest.json +0 -330
- package/.claude/config/background-music-position.txt +0 -27
- package/.claude/config/background-music-volume.txt +0 -1
- package/.claude/config/background-music.cfg +0 -1
- package/.claude/config/background-music.txt +0 -1
- package/.claude/config/language.txt +0 -1
- package/.claude/config/reverb-level.txt +0 -1
- package/.claude/config/tts-speech-rate.txt +0 -1
- package/.claude/config/tts-verbosity.txt +0 -1
- package/.claude/hooks/play-tts-agentvibes-receiver.sh +0 -1
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
- package/.claude/piper-voices-dir.txt +0 -1
|
@@ -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
|
+
}
|
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom Inquirer List Prompt with Spacebar Preview
|
|
3
|
-
* Uses wrapper approach with readline keypress events
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import readline from 'node:readline';
|
|
7
|
-
import { execSync } from 'node:child_process';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Wrapper for inquirer list prompt that adds spacebar preview
|
|
11
|
-
* @param {Object} inquirer - Inquirer instance
|
|
12
|
-
* @param {Object} config - Prompt configuration
|
|
13
|
-
* @param {Function} config.onPreview - Callback for preview (receives selected value)
|
|
14
|
-
* @returns {Promise} Inquirer prompt promise
|
|
15
|
-
*/
|
|
16
|
-
export async function createPreviewListPrompt(inquirer, config) {
|
|
17
|
-
const { onPreview, ...promptConfig } = config;
|
|
18
|
-
|
|
19
|
-
// Set up keypress listener
|
|
20
|
-
let keypressListener = null;
|
|
21
|
-
|
|
22
|
-
// Track playing state
|
|
23
|
-
let currentlyPlaying = null;
|
|
24
|
-
let audioProcess = null;
|
|
25
|
-
|
|
26
|
-
// Initialize currentSelection to match the default value
|
|
27
|
-
let currentSelection = 0;
|
|
28
|
-
if (promptConfig.default) {
|
|
29
|
-
const defaultIndex = promptConfig.choices.findIndex(c => c.value === promptConfig.default);
|
|
30
|
-
if (defaultIndex !== -1) {
|
|
31
|
-
currentSelection = defaultIndex;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Function to stop currently playing audio
|
|
36
|
-
const stopAudio = () => {
|
|
37
|
-
// Kill the specific process if we have it
|
|
38
|
-
// SECURITY: Only kill our own process, never use pkill which affects all users
|
|
39
|
-
if (audioProcess) {
|
|
40
|
-
try {
|
|
41
|
-
audioProcess.kill('SIGKILL');
|
|
42
|
-
audioProcess = null;
|
|
43
|
-
} catch (e) {
|
|
44
|
-
// Process might have already finished or already killed
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
currentlyPlaying = null;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
if (onPreview && process.stdin.isTTY) {
|
|
52
|
-
readline.emitKeypressEvents(process.stdin);
|
|
53
|
-
// SECURITY: Wrap in try-catch to prevent terminal corruption on error
|
|
54
|
-
try {
|
|
55
|
-
if (process.stdin.setRawMode) {
|
|
56
|
-
process.stdin.setRawMode(true);
|
|
57
|
-
}
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// Failed to set raw mode, continue without it
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
keypressListener = async (str, key) => {
|
|
63
|
-
// Track current selection based on arrow keys
|
|
64
|
-
if (key && key.name === 'down') {
|
|
65
|
-
currentSelection = Math.min(currentSelection + 1, promptConfig.choices.length - 1);
|
|
66
|
-
} else if (key && key.name === 'up') {
|
|
67
|
-
currentSelection = Math.max(currentSelection - 1, 0);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (key && key.name === 'space') {
|
|
71
|
-
// Get the current item (don't filter - use actual index)
|
|
72
|
-
const currentChoice = promptConfig.choices[currentSelection];
|
|
73
|
-
|
|
74
|
-
// Only preview if it's a valid choice (not separator, not special item)
|
|
75
|
-
if (currentChoice && currentChoice.value && !currentChoice.value.startsWith('__')) {
|
|
76
|
-
|
|
77
|
-
// Toggle: if same voice pressed twice, stop it
|
|
78
|
-
if (currentlyPlaying === currentChoice.value) {
|
|
79
|
-
stopAudio();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// CRITICAL: Stop previous voice BEFORE starting new one
|
|
84
|
-
if (currentlyPlaying) {
|
|
85
|
-
stopAudio();
|
|
86
|
-
// Small delay to ensure kill takes effect
|
|
87
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
currentlyPlaying = currentChoice.value;
|
|
91
|
-
|
|
92
|
-
// Call onPreview and store process handle immediately
|
|
93
|
-
try {
|
|
94
|
-
const result = await onPreview(currentChoice.value);
|
|
95
|
-
// Store the process handle - onPreview should return the spawn() result
|
|
96
|
-
audioProcess = result;
|
|
97
|
-
} catch (err) {
|
|
98
|
-
console.error(`[Preview] Error playing sample:`, err.message);
|
|
99
|
-
currentlyPlaying = null;
|
|
100
|
-
audioProcess = null;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
process.stdin.on('keypress', keypressListener);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
// Run the standard list prompt
|
|
111
|
-
const result = await inquirer.prompt([{
|
|
112
|
-
...promptConfig,
|
|
113
|
-
type: 'list'
|
|
114
|
-
}]);
|
|
115
|
-
|
|
116
|
-
return result;
|
|
117
|
-
} finally {
|
|
118
|
-
// Stop any playing audio
|
|
119
|
-
stopAudio();
|
|
120
|
-
|
|
121
|
-
// Clean up keypress listener
|
|
122
|
-
if (keypressListener) {
|
|
123
|
-
process.stdin.removeListener('keypress', keypressListener);
|
|
124
|
-
// SECURITY: Wrap in try-catch to ensure cleanup always completes
|
|
125
|
-
try {
|
|
126
|
-
if (process.stdin.setRawMode) {
|
|
127
|
-
process.stdin.setRawMode(false);
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
// Failed to restore raw mode, but we're exiting anyway
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export default createPreviewListPrompt;
|
|
1
|
+
/**
|
|
2
|
+
* Custom Inquirer List Prompt with Spacebar Preview
|
|
3
|
+
* Uses wrapper approach with readline keypress events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import readline from 'node:readline';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wrapper for inquirer list prompt that adds spacebar preview
|
|
11
|
+
* @param {Object} inquirer - Inquirer instance
|
|
12
|
+
* @param {Object} config - Prompt configuration
|
|
13
|
+
* @param {Function} config.onPreview - Callback for preview (receives selected value)
|
|
14
|
+
* @returns {Promise} Inquirer prompt promise
|
|
15
|
+
*/
|
|
16
|
+
export async function createPreviewListPrompt(inquirer, config) {
|
|
17
|
+
const { onPreview, ...promptConfig } = config;
|
|
18
|
+
|
|
19
|
+
// Set up keypress listener
|
|
20
|
+
let keypressListener = null;
|
|
21
|
+
|
|
22
|
+
// Track playing state
|
|
23
|
+
let currentlyPlaying = null;
|
|
24
|
+
let audioProcess = null;
|
|
25
|
+
|
|
26
|
+
// Initialize currentSelection to match the default value
|
|
27
|
+
let currentSelection = 0;
|
|
28
|
+
if (promptConfig.default) {
|
|
29
|
+
const defaultIndex = promptConfig.choices.findIndex(c => c.value === promptConfig.default);
|
|
30
|
+
if (defaultIndex !== -1) {
|
|
31
|
+
currentSelection = defaultIndex;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Function to stop currently playing audio
|
|
36
|
+
const stopAudio = () => {
|
|
37
|
+
// Kill the specific process if we have it
|
|
38
|
+
// SECURITY: Only kill our own process, never use pkill which affects all users
|
|
39
|
+
if (audioProcess) {
|
|
40
|
+
try {
|
|
41
|
+
audioProcess.kill('SIGKILL');
|
|
42
|
+
audioProcess = null;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Process might have already finished or already killed
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
currentlyPlaying = null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (onPreview && process.stdin.isTTY) {
|
|
52
|
+
readline.emitKeypressEvents(process.stdin);
|
|
53
|
+
// SECURITY: Wrap in try-catch to prevent terminal corruption on error
|
|
54
|
+
try {
|
|
55
|
+
if (process.stdin.setRawMode) {
|
|
56
|
+
process.stdin.setRawMode(true);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Failed to set raw mode, continue without it
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
keypressListener = async (str, key) => {
|
|
63
|
+
// Track current selection based on arrow keys
|
|
64
|
+
if (key && key.name === 'down') {
|
|
65
|
+
currentSelection = Math.min(currentSelection + 1, promptConfig.choices.length - 1);
|
|
66
|
+
} else if (key && key.name === 'up') {
|
|
67
|
+
currentSelection = Math.max(currentSelection - 1, 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (key && key.name === 'space') {
|
|
71
|
+
// Get the current item (don't filter - use actual index)
|
|
72
|
+
const currentChoice = promptConfig.choices[currentSelection];
|
|
73
|
+
|
|
74
|
+
// Only preview if it's a valid choice (not separator, not special item)
|
|
75
|
+
if (currentChoice && currentChoice.value && !currentChoice.value.startsWith('__')) {
|
|
76
|
+
|
|
77
|
+
// Toggle: if same voice pressed twice, stop it
|
|
78
|
+
if (currentlyPlaying === currentChoice.value) {
|
|
79
|
+
stopAudio();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// CRITICAL: Stop previous voice BEFORE starting new one
|
|
84
|
+
if (currentlyPlaying) {
|
|
85
|
+
stopAudio();
|
|
86
|
+
// Small delay to ensure kill takes effect
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
currentlyPlaying = currentChoice.value;
|
|
91
|
+
|
|
92
|
+
// Call onPreview and store process handle immediately
|
|
93
|
+
try {
|
|
94
|
+
const result = await onPreview(currentChoice.value);
|
|
95
|
+
// Store the process handle - onPreview should return the spawn() result
|
|
96
|
+
audioProcess = result;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`[Preview] Error playing sample:`, err.message);
|
|
99
|
+
currentlyPlaying = null;
|
|
100
|
+
audioProcess = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
process.stdin.on('keypress', keypressListener);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Run the standard list prompt
|
|
111
|
+
const result = await inquirer.prompt([{
|
|
112
|
+
...promptConfig,
|
|
113
|
+
type: 'list'
|
|
114
|
+
}]);
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
} finally {
|
|
118
|
+
// Stop any playing audio
|
|
119
|
+
stopAudio();
|
|
120
|
+
|
|
121
|
+
// Clean up keypress listener
|
|
122
|
+
if (keypressListener) {
|
|
123
|
+
process.stdin.removeListener('keypress', keypressListener);
|
|
124
|
+
// SECURITY: Wrap in try-catch to ensure cleanup always completes
|
|
125
|
+
try {
|
|
126
|
+
if (process.stdin.setRawMode) {
|
|
127
|
+
process.stdin.setRawMode(false);
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Failed to restore raw mode, but we're exiting anyway
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default createPreviewListPrompt;
|