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.
Files changed (145) hide show
  1. package/.agentvibes/config.json +3 -12
  2. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  3. package/.claude/commands/agent-vibes-rdp.md +24 -24
  4. package/.claude/config/audio-effects.cfg +4 -5
  5. package/.claude/config/audio-effects.cfg.sample +52 -52
  6. package/.claude/config/background-music-enabled.txt +1 -1
  7. package/.claude/docs/TERMUX_SETUP.md +408 -408
  8. package/.claude/github-star-reminder.txt +1 -1
  9. package/.claude/hooks/audio-cache-utils.sh +0 -0
  10. package/.claude/hooks/audio-processor.sh +0 -0
  11. package/.claude/hooks/background-music-manager.sh +0 -0
  12. package/.claude/hooks/bmad-party-speak.sh +0 -0
  13. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  14. package/.claude/hooks/bmad-speak.sh +0 -0
  15. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  16. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  17. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  18. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  19. package/.claude/hooks/clean-audio-cache.sh +0 -0
  20. package/.claude/hooks/cleanup-cache.sh +0 -0
  21. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  22. package/.claude/hooks/download-extra-voices.sh +0 -0
  23. package/.claude/hooks/effects-manager.sh +0 -0
  24. package/.claude/hooks/github-star-reminder.sh +0 -0
  25. package/.claude/hooks/language-manager.sh +0 -0
  26. package/.claude/hooks/learn-manager.sh +0 -0
  27. package/.claude/hooks/macos-voice-manager.sh +0 -0
  28. package/.claude/hooks/migrate-background-music.sh +0 -0
  29. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  30. package/.claude/hooks/optimize-background-music.sh +0 -0
  31. package/.claude/hooks/path-resolver.sh +0 -0
  32. package/.claude/hooks/personality-manager.sh +0 -0
  33. package/.claude/hooks/piper-download-voices.sh +0 -0
  34. package/.claude/hooks/piper-installer.sh +0 -0
  35. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  36. package/.claude/hooks/piper-voice-manager.sh +0 -0
  37. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
  38. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  39. package/.claude/hooks/play-tts-macos.sh +0 -0
  40. package/.claude/hooks/play-tts-piper.sh +20 -13
  41. package/.claude/hooks/play-tts-soprano.sh +0 -0
  42. package/.claude/hooks/play-tts-ssh-remote.sh +0 -0
  43. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  44. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  45. package/.claude/hooks/play-tts.sh +0 -0
  46. package/.claude/hooks/prepare-release.sh +0 -0
  47. package/.claude/hooks/provider-commands.sh +0 -0
  48. package/.claude/hooks/provider-manager.sh +0 -0
  49. package/.claude/hooks/replay-target-audio.sh +0 -0
  50. package/.claude/hooks/requirements.txt +6 -6
  51. package/.claude/hooks/sentiment-manager.sh +0 -0
  52. package/.claude/hooks/session-start-tts.sh +0 -0
  53. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  54. package/.claude/hooks/speed-manager.sh +0 -0
  55. package/.claude/hooks/stop-tts.sh +0 -0
  56. package/.claude/hooks/termux-installer.sh +0 -0
  57. package/.claude/hooks/translate-manager.sh +0 -0
  58. package/.claude/hooks/translator.py +237 -237
  59. package/.claude/hooks/tts-queue-worker.sh +0 -0
  60. package/.claude/hooks/tts-queue.sh +0 -0
  61. package/.claude/hooks/verbosity-manager.sh +0 -0
  62. package/.claude/hooks/voice-manager.sh +6 -0
  63. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
  64. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  65. package/.claude/verbosity.txt +1 -1
  66. package/.clawdbot/README.md +105 -105
  67. package/.mcp.json +19 -6
  68. package/README.md +1 -1
  69. package/WINDOWS-SETUP.md +208 -208
  70. package/bin/agent-vibes +39 -39
  71. package/bin/agentvibes-voice-browser.js +0 -0
  72. package/bin/agentvibes.js +0 -0
  73. package/bin/mcp-server.js +121 -121
  74. package/bin/mcp-server.sh +0 -0
  75. package/bin/test-bmad-pr +78 -78
  76. package/mcp-server/QUICK_START.md +203 -203
  77. package/mcp-server/README.md +345 -345
  78. package/mcp-server/WINDOWS_SETUP.md +0 -0
  79. package/mcp-server/examples/claude_desktop_config.json +11 -11
  80. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  81. package/mcp-server/examples/custom_instructions.md +169 -169
  82. package/mcp-server/install-deps.js +0 -0
  83. package/mcp-server/server.py +1807 -1797
  84. package/mcp-server/test_server.py +0 -0
  85. package/package.json +2 -2
  86. package/src/cli/list-personalities.js +110 -110
  87. package/src/cli/list-voices.js +114 -114
  88. package/src/commands/bmad-voices.js +394 -394
  89. package/src/commands/install-mcp.js +730 -476
  90. package/src/console/app.js +3 -3
  91. package/src/console/brand-colors.js +13 -13
  92. package/src/console/constants/personalities.js +44 -44
  93. package/src/console/tabs/agents-tab.js +6 -6
  94. package/src/console/tabs/help-tab.js +314 -314
  95. package/src/console/tabs/music-tab.js +1 -1
  96. package/src/console/tabs/readme-tab.js +272 -272
  97. package/src/console/tabs/receiver-tab.js +13 -13
  98. package/src/console/tabs/settings-tab.js +2 -2
  99. package/src/console/tabs/setup-tab.js +10 -10
  100. package/src/console/tabs/voices-tab.js +4 -4
  101. package/src/console/widgets/destroy-list.js +25 -25
  102. package/src/console/widgets/notice.js +55 -55
  103. package/src/console/widgets/personality-picker.js +2 -2
  104. package/src/console/widgets/reverb-picker.js +1 -1
  105. package/src/i18n/de.js +202 -202
  106. package/src/i18n/es.js +202 -202
  107. package/src/i18n/fr.js +202 -202
  108. package/src/i18n/hi.js +202 -202
  109. package/src/i18n/ja.js +202 -202
  110. package/src/i18n/ko.js +202 -202
  111. package/src/i18n/pt.js +202 -202
  112. package/src/i18n/strings.js +54 -54
  113. package/src/i18n/zh-CN.js +202 -202
  114. package/src/installer/language-screen.js +31 -31
  115. package/src/installer/music-file-input.js +304 -304
  116. package/src/installer.js +32 -27
  117. package/src/services/config-service.js +264 -264
  118. package/src/services/language-service.js +47 -47
  119. package/src/services/provider-service.js +143 -143
  120. package/src/services/tts-engine-service.js +2 -2
  121. package/src/utils/audio-duration-validator.js +298 -298
  122. package/src/utils/audio-format-validator.js +277 -277
  123. package/src/utils/dependency-checker.js +469 -469
  124. package/src/utils/file-ownership-verifier.js +358 -358
  125. package/src/utils/list-formatter.js +200 -194
  126. package/src/utils/music-file-validator.js +285 -285
  127. package/src/utils/platform-resolver.js +369 -0
  128. package/src/utils/preview-list-prompt.js +136 -136
  129. package/src/utils/provider-validator.js +9 -9
  130. package/src/utils/secure-music-storage.js +412 -412
  131. package/templates/agentvibes-receiver.sh +231 -231
  132. package/templates/audio/welcome-music.mp3 +0 -0
  133. package/.agentvibes/install-manifest.json +0 -330
  134. package/.claude/config/background-music-position.txt +0 -27
  135. package/.claude/config/background-music-volume.txt +0 -1
  136. package/.claude/config/background-music.cfg +0 -1
  137. package/.claude/config/background-music.txt +0 -1
  138. package/.claude/config/language.txt +0 -1
  139. package/.claude/config/reverb-level.txt +0 -1
  140. package/.claude/config/tts-speech-rate.txt +0 -1
  141. package/.claude/config/tts-verbosity.txt +0 -1
  142. package/.claude/hooks/play-tts-agentvibes-receiver.sh +0 -1
  143. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
  144. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
  145. 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;