agentvibes 3.5.9 ā 4.0.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/bmad/bmad-voices-enabled.flag +0 -0
- package/.agentvibes/bmad/bmad-voices.md +69 -0
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/background-music-position.txt +1 -27
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-processor.sh +32 -17
- package/.claude/hooks/bmad-speak-enhanced.sh +5 -5
- package/.claude/hooks/bmad-speak.sh +4 -4
- package/.claude/hooks/bmad-voice-manager.sh +8 -8
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +23 -25
- package/.claude/hooks/clawdbot-receiver.sh +28 -4
- package/.claude/hooks/language-manager.sh +1 -1
- package/.claude/hooks/path-resolver.sh +60 -0
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -0
- package/.claude/hooks/play-tts-piper.sh +82 -24
- package/.claude/hooks/play-tts-ssh-remote.sh +13 -15
- package/.claude/hooks/play-tts.sh +16 -5
- package/.claude/hooks/session-start-tts.sh +26 -56
- package/.claude/hooks/soprano-gradio-synth.py +1 -1
- package/.claude/hooks/verbosity-manager.sh +10 -4
- package/.claude/settings.json +1 -1
- package/CLAUDE.md +129 -104
- package/README.md +418 -10
- package/RELEASE_NOTES.md +60 -1036
- package/bin/agentvibes-voice-browser.js +1827 -0
- package/bin/agentvibes.js +100 -0
- package/mcp-server/server.py +67 -3
- package/package.json +11 -2
- package/src/console/app.js +806 -0
- package/src/console/audio-env.js +123 -0
- package/src/console/brand-colors.js +13 -0
- package/src/console/footer-config.js +42 -0
- package/src/console/modals/.gitkeep +0 -0
- package/src/console/modals/modal-overlay.js +247 -0
- package/src/console/navigation.js +60 -0
- package/src/console/tabs/.gitkeep +0 -0
- package/src/console/tabs/agents-tab.js +369 -0
- package/src/console/tabs/help-tab.js +261 -0
- package/src/console/tabs/install-tab.js +990 -0
- package/src/console/tabs/music-tab.js +997 -0
- package/src/console/tabs/placeholder-tab.js +45 -0
- package/src/console/tabs/readme-tab.js +267 -0
- package/src/console/tabs/settings-tab.js +3949 -0
- package/src/console/tabs/voices-tab.js +1574 -0
- package/src/installer/music-file-input.js +304 -0
- package/src/installer.js +1353 -676
- package/src/services/.gitkeep +0 -0
- package/src/services/agent-voice-store.js +163 -0
- package/src/services/config-service.js +240 -0
- package/src/services/navigation-service.js +123 -0
- package/src/services/provider-service.js +132 -0
- package/src/services/verbosity-service.js +157 -0
- package/src/utils/audio-duration-validator.js +298 -0
- package/src/utils/audio-format-validator.js +277 -0
- package/src/utils/dependency-checker.js +3 -3
- package/src/utils/file-ownership-verifier.js +358 -0
- package/src/utils/music-file-validator.js +275 -0
- package/src/utils/preview-list-prompt.js +136 -0
- package/src/utils/provider-validator.js +144 -132
- package/src/utils/secure-music-storage.js +412 -0
- package/templates/agentvibes-receiver.sh +11 -7
- package/voice-assignments.json +8245 -0
- 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/tts-speech-rate.txt +0 -1
- package/.claude/config/tts-verbosity.txt +0 -1
- package/.claude/hooks/bmad-party-manager.sh +0 -225
- package/.claude/hooks/stop.sh +0 -38
- package/.claude/piper-voices-dir.txt +0 -1
- package/.mcp.json +0 -34
package/src/installer.js
CHANGED
|
@@ -45,9 +45,12 @@ import { program } from 'commander';
|
|
|
45
45
|
import path from 'node:path';
|
|
46
46
|
import fs from 'node:fs/promises';
|
|
47
47
|
import fsSync from 'node:fs';
|
|
48
|
-
import { execSync, execFileSync, spawn } from 'node:child_process';
|
|
48
|
+
import { execSync, execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
49
|
+
import os from 'node:os';
|
|
50
|
+
import crypto from 'node:crypto';
|
|
49
51
|
import chalk from 'chalk';
|
|
50
52
|
import inquirer from 'inquirer';
|
|
53
|
+
import search from '@inquirer/search';
|
|
51
54
|
import figlet from 'figlet';
|
|
52
55
|
import { detectBMAD } from './bmad-detector.js';
|
|
53
56
|
import boxen from 'boxen';
|
|
@@ -67,6 +70,8 @@ import {
|
|
|
67
70
|
getProviderDisplayName,
|
|
68
71
|
attemptProviderInstallation,
|
|
69
72
|
} from './utils/provider-validator.js';
|
|
73
|
+
import { promptForCustomMusic } from './installer/music-file-input.js';
|
|
74
|
+
import { createPreviewListPrompt } from './utils/preview-list-prompt.js';
|
|
70
75
|
|
|
71
76
|
const __filename = fileURLToPath(import.meta.url);
|
|
72
77
|
const __dirname = path.dirname(__filename);
|
|
@@ -77,6 +82,31 @@ const packageJson = JSON.parse(
|
|
|
77
82
|
);
|
|
78
83
|
const VERSION = packageJson.version;
|
|
79
84
|
|
|
85
|
+
// Personality emoji mapping for quick visual recognition
|
|
86
|
+
const personalityEmojis = {
|
|
87
|
+
'angry': 'š ',
|
|
88
|
+
'annoying': 'š¤',
|
|
89
|
+
'crass': 'š¤¬',
|
|
90
|
+
'dramatic': 'š',
|
|
91
|
+
'dry-humor': 'š',
|
|
92
|
+
'flirty': 'š',
|
|
93
|
+
'funny': 'š',
|
|
94
|
+
'grandpa': 'š“',
|
|
95
|
+
'millennial': 'š',
|
|
96
|
+
'moody': 'š',
|
|
97
|
+
'none': 'š',
|
|
98
|
+
'normal': 'š',
|
|
99
|
+
'pirate': 'š“āā ļø',
|
|
100
|
+
'poetic': 'š',
|
|
101
|
+
'professional': 'š',
|
|
102
|
+
'rapper': 'š¤',
|
|
103
|
+
'robot': 'š¤',
|
|
104
|
+
'sarcastic': 'š',
|
|
105
|
+
'sassy': 'š',
|
|
106
|
+
'surfer-dude': 'š',
|
|
107
|
+
'zen': 'š§'
|
|
108
|
+
};
|
|
109
|
+
|
|
80
110
|
// Validate Node.js executable is available (CLAUDE.md - early validation)
|
|
81
111
|
if (!process.execPath) {
|
|
82
112
|
console.error('ā Error: Node.js executable path not found');
|
|
@@ -122,15 +152,244 @@ function hasPulseAudioTunnel() {
|
|
|
122
152
|
process.env.PULSE_SERVER.toLowerCase().startsWith('tcp:');
|
|
123
153
|
}
|
|
124
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Story 2.3: Detect terminal emoji support
|
|
157
|
+
* Checks $TERM, locale, and platform to determine emoji capability
|
|
158
|
+
* @returns {boolean} True if terminal supports emoji
|
|
159
|
+
*/
|
|
160
|
+
function supportsEmoji() {
|
|
161
|
+
// Check TERM environment variable
|
|
162
|
+
const term = process.env.TERM || '';
|
|
163
|
+
const lang = process.env.LANG || '';
|
|
164
|
+
const lcAll = process.env.LC_ALL || '';
|
|
165
|
+
|
|
166
|
+
// Explicitly unsupported terminals
|
|
167
|
+
const unsupportedTerminals = ['dumb', 'emacs', 'ansi'];
|
|
168
|
+
if (unsupportedTerminals.includes(term.toLowerCase())) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check for UTF-8 locale (required for emoji)
|
|
173
|
+
const isUtf8 = lang.includes('utf8') || lang.includes('UTF-8') ||
|
|
174
|
+
lcAll.includes('utf8') || lcAll.includes('UTF-8');
|
|
175
|
+
|
|
176
|
+
// Modern terminals (check common ones)
|
|
177
|
+
const modernTerminals = [
|
|
178
|
+
'xterm-256color', 'screen-256color', 'tmux-256color',
|
|
179
|
+
'iterm2', 'iterm', 'vscode', 'alacritty', 'kitty',
|
|
180
|
+
'wezterm', 'windows-terminal', 'conemu'
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const isModernTerminal = modernTerminals.some(t => term.toLowerCase().includes(t));
|
|
184
|
+
|
|
185
|
+
// Windows Terminal always supports emoji
|
|
186
|
+
const isWindowsTerminal = process.platform === 'win32' &&
|
|
187
|
+
(process.env.WT_SESSION || process.env.WT_PROFILE_ID);
|
|
188
|
+
|
|
189
|
+
// macOS Terminal and iTerm2
|
|
190
|
+
const isMacOS = process.platform === 'darwin';
|
|
191
|
+
|
|
192
|
+
// Linux with proper UTF-8
|
|
193
|
+
const isLinuxWithUtf8 = process.platform === 'linux' && isUtf8;
|
|
194
|
+
|
|
195
|
+
// Unknown terminal with UTF-8: Only enable emoji if TERM is not explicitly unsupported AND has UTF-8
|
|
196
|
+
// This prevents false positives like "vt100" with UTF-8 reporting emoji support
|
|
197
|
+
const unknownTerminalWithUtf8 = term &&
|
|
198
|
+
!unsupportedTerminals.includes(term.toLowerCase()) &&
|
|
199
|
+
isUtf8;
|
|
200
|
+
|
|
201
|
+
// Default to true for: modern terms, Windows Terminal, macOS, Linux with UTF-8, or unknown term with UTF-8
|
|
202
|
+
// Default to false for: dumb/emacs/ansi terminals or environments without UTF-8
|
|
203
|
+
return isModernTerminal || isWindowsTerminal || isMacOS || isLinuxWithUtf8 || unknownTerminalWithUtf8;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Story 2.4: Get personality display with emoji or text fallback
|
|
208
|
+
* Returns emoji if supported, otherwise returns text label like "[personality]"
|
|
209
|
+
* @param {string} personality - Personality name
|
|
210
|
+
* @param {boolean} emojiSupported - Pre-computed emoji support (avoids redundant env var reads)
|
|
211
|
+
* @returns {string} Either emoji or "[personality-name]" fallback
|
|
212
|
+
*/
|
|
213
|
+
function getPersonalityIcon(personality, emojiSupported) {
|
|
214
|
+
const emoji = personalityEmojis[personality] || 'āØ';
|
|
215
|
+
|
|
216
|
+
// Use provided emojiSupported to avoid recalculating for every personality (performance)
|
|
217
|
+
if (emojiSupported) {
|
|
218
|
+
return emoji;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Text fallback for unsupported terminals: [personality-name]
|
|
222
|
+
return `[${personality}]`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if Piper TTS is installed
|
|
227
|
+
* @returns {boolean} True if piper command exists
|
|
228
|
+
*/
|
|
229
|
+
function isPiperInstalled() {
|
|
230
|
+
try {
|
|
231
|
+
execSync('which piper', {
|
|
232
|
+
stdio: 'pipe',
|
|
233
|
+
timeout: 3000
|
|
234
|
+
});
|
|
235
|
+
return true;
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if Soprano TTS is installed
|
|
243
|
+
* @returns {boolean} True if soprano-tts or soprano-webui command exists
|
|
244
|
+
*/
|
|
245
|
+
function isSopranoInstalled() {
|
|
246
|
+
try {
|
|
247
|
+
execSync('which soprano-tts || which soprano-webui', {
|
|
248
|
+
stdio: 'pipe',
|
|
249
|
+
timeout: 3000
|
|
250
|
+
});
|
|
251
|
+
return true;
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Play voice sample for preview during voice selection
|
|
259
|
+
* @param {string} voiceName - Name of the voice (e.g., 'en_US-lessac-medium', 'soprano-default')
|
|
260
|
+
* @param {string} provider - TTS provider ('piper' or 'soprano')
|
|
261
|
+
* @returns {Promise<boolean>} True if sample played successfully
|
|
262
|
+
*/
|
|
263
|
+
async function playVoiceSample(voiceName, provider) {
|
|
264
|
+
try {
|
|
265
|
+
const samplesDir = path.join(__dirname, '..', '.claude', 'audio', 'voice-samples', provider);
|
|
266
|
+
|
|
267
|
+
// Try friendly name first (e.g., "ryan.wav")
|
|
268
|
+
let sampleFile = path.join(samplesDir, `${voiceName}.wav`);
|
|
269
|
+
|
|
270
|
+
// If not found and looks like a Piper ID, try that too
|
|
271
|
+
if (!fsSync.existsSync(sampleFile) && voiceName.includes('-')) {
|
|
272
|
+
sampleFile = path.join(samplesDir, `${voiceName}.wav`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if pre-recorded sample exists
|
|
276
|
+
if (fsSync.existsSync(sampleFile)) {
|
|
277
|
+
console.log(chalk.cyan(' š Playing voice sample...'));
|
|
278
|
+
|
|
279
|
+
// Play using sox/aplay - use spawn for non-blocking playback
|
|
280
|
+
try {
|
|
281
|
+
// Play using aplay directly (no shell interpolation ā prevents command injection)
|
|
282
|
+
const player = spawn('aplay', [sampleFile], {
|
|
283
|
+
detached: false,
|
|
284
|
+
stdio: 'ignore'
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Return the process so it can be killed
|
|
288
|
+
return player;
|
|
289
|
+
} catch (e) {
|
|
290
|
+
// Fallback: generate on-the-fly if provider is available
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Generate sample on-the-fly if provider is running
|
|
295
|
+
if (provider === 'piper' && isPiperInstalled()) {
|
|
296
|
+
const text = `Hi, I'm ${voiceName.split('-')[1] || 'Piper'}`;
|
|
297
|
+
// Use bash -c with positional args to prevent command injection via text/voiceName
|
|
298
|
+
spawnSync('bash', ['-c', 'echo "$1" | piper --model "$2" --output_raw | aplay -r 22050 -f S16_LE -t raw -', '_', text, voiceName], {
|
|
299
|
+
stdio: 'inherit',
|
|
300
|
+
timeout: 15000
|
|
301
|
+
});
|
|
302
|
+
return true;
|
|
303
|
+
} else if (provider === 'soprano' && await isSopranoRunning()) {
|
|
304
|
+
// Generate via Soprano API
|
|
305
|
+
const text = "Hi, I'm Soprano";
|
|
306
|
+
const response = await fetch('http://localhost:7860/api/tts', {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
body: JSON.stringify({ text, voice: 'soprano-default' }),
|
|
310
|
+
signal: AbortSignal.timeout(5000)
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (response.ok) {
|
|
314
|
+
const audio = await response.arrayBuffer();
|
|
315
|
+
// Save temporarily and play
|
|
316
|
+
const tempFile = path.join(os.tmpdir(), `soprano-sample-${crypto.randomBytes(8).toString('hex')}.wav`);
|
|
317
|
+
fsSync.writeFileSync(tempFile, Buffer.from(audio));
|
|
318
|
+
try {
|
|
319
|
+
// Use spawnSync with argument array to prevent command injection
|
|
320
|
+
spawnSync('aplay', [tempFile], { stdio: 'pipe', timeout: 5000 });
|
|
321
|
+
} finally {
|
|
322
|
+
fsSync.unlinkSync(tempFile);
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return false;
|
|
329
|
+
} catch (e) {
|
|
330
|
+
console.log(chalk.gray(' (Preview not available)'));
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check if Soprano server is running on port 7860
|
|
337
|
+
* @returns {Promise<boolean>} True if server responds to health check
|
|
338
|
+
*/
|
|
339
|
+
async function isSopranoRunning() {
|
|
340
|
+
try {
|
|
341
|
+
const response = await fetch('http://localhost:7860/health', {
|
|
342
|
+
signal: AbortSignal.timeout(2000)
|
|
343
|
+
});
|
|
344
|
+
return response.ok;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Start Soprano TTS server in background
|
|
352
|
+
* @returns {Promise<boolean>} True if successfully started
|
|
353
|
+
*/
|
|
354
|
+
async function startSopranoServer() {
|
|
355
|
+
try {
|
|
356
|
+
console.log(chalk.gray('š Starting Soprano TTS server...'));
|
|
357
|
+
|
|
358
|
+
// Start soprano-webui in background
|
|
359
|
+
const sopranoProcess = spawn('soprano-webui', ['--port', '7860'], {
|
|
360
|
+
detached: true,
|
|
361
|
+
stdio: 'ignore'
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
sopranoProcess.unref(); // Allow parent to exit independently
|
|
365
|
+
|
|
366
|
+
// Wait up to 10 seconds for server to be ready
|
|
367
|
+
for (let i = 0; i < 20; i++) {
|
|
368
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
369
|
+
if (await isSopranoRunning()) {
|
|
370
|
+
console.log(chalk.green('ā Soprano TTS server started successfully\n'));
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log(chalk.yellow('ā ļø Soprano server started but not responding yet\n'));
|
|
376
|
+
return false;
|
|
377
|
+
} catch (e) {
|
|
378
|
+
console.log(chalk.yellow('ā ļø Failed to start Soprano server:', e.message, '\n'));
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
125
383
|
/**
|
|
126
384
|
* Detect system capabilities for smart provider recommendations
|
|
127
|
-
* @returns {Promise<Object>} System info including GPU, memory, platform
|
|
385
|
+
* @returns {Promise<Object>} System info including GPU, memory, platform, Soprano availability
|
|
128
386
|
*/
|
|
129
387
|
async function detectSystemCapabilities() {
|
|
130
388
|
const isMacOS = process.platform === 'darwin';
|
|
131
389
|
const isAndroid = isTermux();
|
|
132
390
|
let hasGPU = false;
|
|
133
391
|
let totalRAM = 0;
|
|
392
|
+
let sopranoAvailable = false;
|
|
134
393
|
|
|
135
394
|
try {
|
|
136
395
|
// Detect NVIDIA GPU
|
|
@@ -183,12 +442,24 @@ async function detectSystemCapabilities() {
|
|
|
183
442
|
totalRAM = 4096;
|
|
184
443
|
}
|
|
185
444
|
|
|
445
|
+
// Detect and auto-start Soprano if installed
|
|
446
|
+
if (isSopranoInstalled()) {
|
|
447
|
+
const isRunning = await isSopranoRunning();
|
|
448
|
+
if (isRunning) {
|
|
449
|
+
sopranoAvailable = true;
|
|
450
|
+
} else {
|
|
451
|
+
// Soprano installed but not running - try to start it
|
|
452
|
+
sopranoAvailable = await startSopranoServer();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
186
456
|
return {
|
|
187
457
|
hasGPU,
|
|
188
458
|
lowMemory: totalRAM < 4096,
|
|
189
459
|
totalRAM,
|
|
190
460
|
isMacOS,
|
|
191
|
-
isAndroid
|
|
461
|
+
isAndroid,
|
|
462
|
+
sopranoAvailable
|
|
192
463
|
};
|
|
193
464
|
}
|
|
194
465
|
|
|
@@ -397,7 +668,8 @@ async function showPaginatedContent(pages, options = {}) {
|
|
|
397
668
|
const { action } = await inquirer.prompt([{
|
|
398
669
|
type: 'list',
|
|
399
670
|
name: 'action',
|
|
400
|
-
message: '
|
|
671
|
+
message: chalk.cyan('š” Try these AgentVibes commands in Claude Code terminal'),
|
|
672
|
+
prefix: '',
|
|
401
673
|
choices,
|
|
402
674
|
default: currentPage < pages.length - 1 ? 'next' : 'continue'
|
|
403
675
|
}]);
|
|
@@ -425,11 +697,12 @@ async function showPaginatedContent(pages, options = {}) {
|
|
|
425
697
|
function getPageTitle(pageNum) {
|
|
426
698
|
const titles = {
|
|
427
699
|
0: 'š§ System Dependencies',
|
|
428
|
-
1: '
|
|
700
|
+
1: 'š TTS Provider Configuration',
|
|
429
701
|
2: 'š¤ Voice Selection',
|
|
430
702
|
3: 'š Personality Selection',
|
|
431
|
-
4: '
|
|
432
|
-
5: '
|
|
703
|
+
4: 'šļø Reverb Settings',
|
|
704
|
+
5: 'šµ Background Music',
|
|
705
|
+
6: 'š Verbosity Settings'
|
|
433
706
|
};
|
|
434
707
|
return titles[pageNum] || 'Configuration';
|
|
435
708
|
}
|
|
@@ -442,8 +715,8 @@ async function handleSystemDependenciesPage() {
|
|
|
442
715
|
const { checkDependencies, getInstallCommands } = await import('./utils/dependency-checker.js');
|
|
443
716
|
const depResults = checkDependencies();
|
|
444
717
|
|
|
445
|
-
let depContent = chalk.gray('System dependencies
|
|
446
|
-
depContent += chalk.gray('
|
|
718
|
+
let depContent = chalk.gray('System dependencies detected and already installed.\n');
|
|
719
|
+
depContent += chalk.gray('These tools enable AgentVibes features and functionality.\n\n');
|
|
447
720
|
|
|
448
721
|
// Satisfied dependencies
|
|
449
722
|
if (depResults.core.node?.isCompatible) {
|
|
@@ -477,6 +750,39 @@ async function handleSystemDependenciesPage() {
|
|
|
477
750
|
depContent += chalk.green('ā audio player (paplay/aplay/mpv)\n');
|
|
478
751
|
}
|
|
479
752
|
|
|
753
|
+
// Check TTS providers
|
|
754
|
+
const piperInstalled = isPiperInstalled();
|
|
755
|
+
const sopranoInstalled = isSopranoInstalled();
|
|
756
|
+
|
|
757
|
+
if (piperInstalled || sopranoInstalled) {
|
|
758
|
+
depContent += '\n' + chalk.gray('ā'.repeat(50)) + '\n\n';
|
|
759
|
+
depContent += chalk.cyan.bold('TTS Providers Already Installed:\n\n');
|
|
760
|
+
|
|
761
|
+
if (piperInstalled) {
|
|
762
|
+
try {
|
|
763
|
+
const piperPath = execSync('which piper 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
764
|
+
depContent += chalk.green('ā Piper TTS (offline voice synthesis)\n');
|
|
765
|
+
depContent += chalk.gray(` ${piperPath}\n`);
|
|
766
|
+
} catch (e) {
|
|
767
|
+
depContent += chalk.green('ā Piper TTS (offline voice synthesis)\n');
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (sopranoInstalled) {
|
|
771
|
+
try {
|
|
772
|
+
let sopranoPath = '';
|
|
773
|
+
try {
|
|
774
|
+
sopranoPath = execSync('which soprano-tts 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
775
|
+
} catch (e) {
|
|
776
|
+
sopranoPath = execSync('which soprano-webui 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
777
|
+
}
|
|
778
|
+
depContent += chalk.green('ā Soprano TTS (premium quality)\n');
|
|
779
|
+
depContent += chalk.gray(` ${sopranoPath}\n`);
|
|
780
|
+
} catch (e) {
|
|
781
|
+
depContent += chalk.green('ā Soprano TTS (premium quality)\n');
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
480
786
|
// Missing dependencies
|
|
481
787
|
if (Object.keys(depResults.missing).length > 0) {
|
|
482
788
|
depContent += '\n' + chalk.gray('ā'.repeat(50)) + '\n\n';
|
|
@@ -492,8 +798,7 @@ async function handleSystemDependenciesPage() {
|
|
|
492
798
|
|
|
493
799
|
depContent += '\n' + chalk.gray('TTS will still work without optional tools');
|
|
494
800
|
|
|
495
|
-
// Add install commands
|
|
496
|
-
const os = await import('os');
|
|
801
|
+
// Add install commands (os imported at top level)
|
|
497
802
|
const platform = os.platform();
|
|
498
803
|
const installCmds = getInstallCommands(depResults.missing, platform);
|
|
499
804
|
|
|
@@ -517,6 +822,185 @@ async function handleSystemDependenciesPage() {
|
|
|
517
822
|
});
|
|
518
823
|
|
|
519
824
|
console.log(depsBoxen);
|
|
825
|
+
|
|
826
|
+
// Return status for navigation message
|
|
827
|
+
return {
|
|
828
|
+
allMet: Object.keys(depResults.missing).length === 0,
|
|
829
|
+
missingCount: Object.keys(depResults.missing).length
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Validate and copy custom music track to .claude/audio/tracks directory
|
|
835
|
+
* @param {string} userFilePath - Path provided by user
|
|
836
|
+
* @param {string} tracksDir - Target directory for audio tracks
|
|
837
|
+
* @returns {Promise<string|null>} Filename if successful, null if cancelled
|
|
838
|
+
*/
|
|
839
|
+
async function handleCustomMusicTrack(userFilePath, tracksDir) {
|
|
840
|
+
try {
|
|
841
|
+
// Validate file exists and resolve path securely
|
|
842
|
+
const resolvedPath = path.resolve(userFilePath.trim());
|
|
843
|
+
|
|
844
|
+
if (!fsSync.existsSync(resolvedPath)) {
|
|
845
|
+
console.error(chalk.red('ā File not found. Please check the path.'));
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Validate file extension (whitelist approach per CLAUDE.md)
|
|
850
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
851
|
+
const supportedFormats = ['.mp3', '.wav', '.ogg', '.m4a'];
|
|
852
|
+
if (!supportedFormats.includes(ext)) {
|
|
853
|
+
console.error(chalk.red('ā Unsupported format. Use: .mp3, .wav, .ogg, or .m4a'));
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Verify file is within expected directory (prevent path traversal)
|
|
858
|
+
if (!resolvedPath.startsWith(path.resolve(os.homedir()))) {
|
|
859
|
+
console.error(chalk.red('ā File must be in your home directory or subdirectories.'));
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Get original filename and sanitize it
|
|
864
|
+
let originalFilename = path.basename(resolvedPath);
|
|
865
|
+
const sanitizedFilename = originalFilename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
866
|
+
|
|
867
|
+
// Create tracks directory if needed
|
|
868
|
+
await fs.mkdir(tracksDir, { recursive: true });
|
|
869
|
+
|
|
870
|
+
// Copy file to tracks directory
|
|
871
|
+
const destPath = path.join(tracksDir, sanitizedFilename);
|
|
872
|
+
await fs.copyFile(resolvedPath, destPath);
|
|
873
|
+
|
|
874
|
+
return sanitizedFilename;
|
|
875
|
+
} catch (err) {
|
|
876
|
+
console.error(chalk.red(`ā Error: ${err.message}`));
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Load custom tracks from global registry
|
|
883
|
+
* @returns {Promise<Array>} Array of custom track objects {name, filename}
|
|
884
|
+
*/
|
|
885
|
+
async function loadCustomTracks() {
|
|
886
|
+
try {
|
|
887
|
+
const registryPath = path.join(process.env.HOME || process.env.USERPROFILE, '.agentvibes', 'custom-tracks.json');
|
|
888
|
+
if (fsSync.existsSync(registryPath)) {
|
|
889
|
+
const content = await fs.readFile(registryPath, 'utf-8');
|
|
890
|
+
return JSON.parse(content);
|
|
891
|
+
}
|
|
892
|
+
} catch (err) {
|
|
893
|
+
// Silently fail - registry may not exist yet
|
|
894
|
+
}
|
|
895
|
+
return [];
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Save custom tracks to global registry
|
|
900
|
+
* @param {Array} tracks - Array of custom track objects
|
|
901
|
+
*/
|
|
902
|
+
async function saveCustomTracks(tracks) {
|
|
903
|
+
try {
|
|
904
|
+
const registryDir = path.join(process.env.HOME || process.env.USERPROFILE, '.agentvibes');
|
|
905
|
+
await fs.mkdir(registryDir, { recursive: true });
|
|
906
|
+
const registryPath = path.join(registryDir, 'custom-tracks.json');
|
|
907
|
+
await fs.writeFile(registryPath, JSON.stringify(tracks, null, 2));
|
|
908
|
+
} catch (err) {
|
|
909
|
+
// Silently fail - non-critical
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Track currently playing audio preview to prevent overlaps
|
|
914
|
+
let currentAudioPreview = null;
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Preview audio track using available audio player
|
|
918
|
+
* @param {string} trackName - Name of the track file to preview
|
|
919
|
+
* @param {string} tracksDir - Directory containing audio tracks
|
|
920
|
+
* @returns {Promise<boolean>} True if preview was attempted, false if no audio tools available
|
|
921
|
+
*/
|
|
922
|
+
async function previewAudioTrack(trackName, tracksDir) {
|
|
923
|
+
// Stop any currently playing preview
|
|
924
|
+
if (currentAudioPreview && !currentAudioPreview.killed) {
|
|
925
|
+
currentAudioPreview.kill('SIGTERM');
|
|
926
|
+
currentAudioPreview = null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const trackPath = path.join(tracksDir, trackName);
|
|
930
|
+
|
|
931
|
+
// Verify track exists
|
|
932
|
+
if (!fsSync.existsSync(trackPath)) {
|
|
933
|
+
console.log(chalk.yellow('ā ļø Track file not found'));
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Try available audio players in order of preference
|
|
938
|
+
const audioPlayers = ['ffplay', 'play', 'mpv'];
|
|
939
|
+
let playerAvailable = false;
|
|
940
|
+
|
|
941
|
+
for (const player of audioPlayers) {
|
|
942
|
+
try {
|
|
943
|
+
execSync(`which ${player}`, { stdio: 'ignore' });
|
|
944
|
+
playerAvailable = true;
|
|
945
|
+
|
|
946
|
+
console.log(chalk.cyan('ā¶ Playing preview (10 seconds)...'));
|
|
947
|
+
|
|
948
|
+
// Build appropriate command for each player
|
|
949
|
+
let playerArgs = [];
|
|
950
|
+
if (player === 'ffplay') {
|
|
951
|
+
playerArgs = ['-nodisp', '-autoexit', '-t', '10', '-volume', '30', trackPath];
|
|
952
|
+
} else if (player === 'play') {
|
|
953
|
+
playerArgs = [trackPath, 'trim', '0', '10'];
|
|
954
|
+
} else if (player === 'mpv') {
|
|
955
|
+
playerArgs = ['--no-video', '--duration=10', '--volume=30', trackPath];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Spawn player process (manual setTimeout below handles the safety kill)
|
|
959
|
+
const audioProcess = spawn(player, playerArgs, {
|
|
960
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// Store reference to current preview
|
|
964
|
+
currentAudioPreview = audioProcess;
|
|
965
|
+
|
|
966
|
+
// Handle process completion
|
|
967
|
+
return new Promise((resolve) => {
|
|
968
|
+
const timeoutHandle = setTimeout(() => {
|
|
969
|
+
if (audioProcess && !audioProcess.killed) {
|
|
970
|
+
audioProcess.kill('SIGTERM');
|
|
971
|
+
}
|
|
972
|
+
if (currentAudioPreview === audioProcess) {
|
|
973
|
+
currentAudioPreview = null;
|
|
974
|
+
}
|
|
975
|
+
resolve(true);
|
|
976
|
+
}, 11000); // 11 second timeout
|
|
977
|
+
|
|
978
|
+
audioProcess.on('close', () => {
|
|
979
|
+
clearTimeout(timeoutHandle);
|
|
980
|
+
if (currentAudioPreview === audioProcess) {
|
|
981
|
+
currentAudioPreview = null;
|
|
982
|
+
}
|
|
983
|
+
resolve(true);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
audioProcess.on('error', () => {
|
|
987
|
+
clearTimeout(timeoutHandle);
|
|
988
|
+
if (currentAudioPreview === audioProcess) {
|
|
989
|
+
currentAudioPreview = null;
|
|
990
|
+
}
|
|
991
|
+
resolve(true);
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
} catch (err) {
|
|
995
|
+
// This player not available, try next
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (!playerAvailable) {
|
|
1001
|
+
console.log(chalk.yellow('ā ļø Audio preview requires ffplay, sox (play), or mpv'));
|
|
1002
|
+
}
|
|
1003
|
+
return false;
|
|
520
1004
|
}
|
|
521
1005
|
|
|
522
1006
|
/**
|
|
@@ -530,6 +1014,7 @@ async function collectConfiguration(options = {}) {
|
|
|
530
1014
|
piperPath: null,
|
|
531
1015
|
sshHost: null,
|
|
532
1016
|
defaultVoice: null,
|
|
1017
|
+
pretext: '',
|
|
533
1018
|
personality: 'none',
|
|
534
1019
|
reverb: 'light',
|
|
535
1020
|
backgroundMusic: {
|
|
@@ -539,6 +1024,22 @@ async function collectConfiguration(options = {}) {
|
|
|
539
1024
|
verbosity: 'high'
|
|
540
1025
|
};
|
|
541
1026
|
|
|
1027
|
+
// Load existing pretext from file if it exists (Story 1.2: File Persistence)
|
|
1028
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
1029
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
1030
|
+
const pretextFile = path.join(claudeDir, 'config', 'tts-pretext.txt');
|
|
1031
|
+
try {
|
|
1032
|
+
if (fsSync.existsSync(pretextFile)) {
|
|
1033
|
+
const existingPretext = fsSync.readFileSync(pretextFile, 'utf-8').trim();
|
|
1034
|
+
if (existingPretext) {
|
|
1035
|
+
config.pretext = existingPretext;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
// Gracefully handle read errors (file permissions, encoding issues, etc.)
|
|
1040
|
+
// Pretext will remain empty string if file can't be read
|
|
1041
|
+
}
|
|
1042
|
+
|
|
542
1043
|
// Detect environment type
|
|
543
1044
|
const environment = detectEnvironment();
|
|
544
1045
|
const isAndroid = isTermux();
|
|
@@ -573,7 +1074,7 @@ async function collectConfiguration(options = {}) {
|
|
|
573
1074
|
}
|
|
574
1075
|
|
|
575
1076
|
let currentPage = 0;
|
|
576
|
-
const sectionPages =
|
|
1077
|
+
const sectionPages = 7; // System Dependencies, Provider, Voice Selection, Personality Selection, Reverb, Background Music, Verbosity
|
|
577
1078
|
const pageOffset = options.pageOffset || 0;
|
|
578
1079
|
const totalPages = options.totalPages || sectionPages;
|
|
579
1080
|
|
|
@@ -593,8 +1094,10 @@ async function collectConfiguration(options = {}) {
|
|
|
593
1094
|
const { header, footer } = createPageHeaderFooter(pageTitle, currentPage, totalPages, pageOffset);
|
|
594
1095
|
console.log(header);
|
|
595
1096
|
|
|
1097
|
+
let pageStatus = null; // Track page completion status for navigation message
|
|
1098
|
+
|
|
596
1099
|
if (currentPage === 0) {
|
|
597
|
-
await handleSystemDependenciesPage();
|
|
1100
|
+
pageStatus = await handleSystemDependenciesPage();
|
|
598
1101
|
} else if (currentPage === 1) {
|
|
599
1102
|
// Page 2: TTS Provider & Voice Storage
|
|
600
1103
|
|
|
@@ -880,6 +1383,57 @@ async function collectConfiguration(options = {}) {
|
|
|
880
1383
|
|
|
881
1384
|
// Validate provider installation before accepting selection
|
|
882
1385
|
console.log(chalk.gray(`\n Checking for ${getProviderDisplayName(provider)}...`));
|
|
1386
|
+
|
|
1387
|
+
// Special handling for Soprano - check if running, auto-start if installed
|
|
1388
|
+
if (provider === 'soprano') {
|
|
1389
|
+
const sopranoInstalled = isSopranoInstalled();
|
|
1390
|
+
const sopranoRunning = await isSopranoRunning();
|
|
1391
|
+
|
|
1392
|
+
if (sopranoInstalled && !sopranoRunning) {
|
|
1393
|
+
// Soprano installed but not running - offer to start it
|
|
1394
|
+
console.log(chalk.yellow('\nā ļø Soprano TTS is installed but server is not running'));
|
|
1395
|
+
|
|
1396
|
+
const { startAction } = await inquirer.prompt([{
|
|
1397
|
+
type: 'list',
|
|
1398
|
+
name: 'startAction',
|
|
1399
|
+
message: 'What would you like to do?',
|
|
1400
|
+
choices: [
|
|
1401
|
+
{ name: chalk.green('Start Soprano server now (recommended)'), value: 'start' },
|
|
1402
|
+
{ name: 'I\'ll start it myself', value: 'manual' },
|
|
1403
|
+
{ name: 'Choose a different provider', value: 'back' }
|
|
1404
|
+
]
|
|
1405
|
+
}]);
|
|
1406
|
+
|
|
1407
|
+
if (startAction === 'start') {
|
|
1408
|
+
const started = await startSopranoServer();
|
|
1409
|
+
if (started) {
|
|
1410
|
+
config.provider = provider;
|
|
1411
|
+
providerSelected = true;
|
|
1412
|
+
continue;
|
|
1413
|
+
} else {
|
|
1414
|
+
console.log(chalk.yellow('\nā ļø Failed to start Soprano automatically'));
|
|
1415
|
+
console.log(chalk.cyan(' Try starting manually:'));
|
|
1416
|
+
console.log(chalk.gray(' $ soprano-webui --port 7860\n'));
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
} else if (startAction === 'manual') {
|
|
1420
|
+
console.log(chalk.cyan('\nš To start Soprano manually, run:'));
|
|
1421
|
+
console.log(chalk.gray(' $ soprano-webui --port 7860\n'));
|
|
1422
|
+
continue;
|
|
1423
|
+
} else {
|
|
1424
|
+
// User chose to go back
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
} else if (sopranoInstalled && sopranoRunning) {
|
|
1428
|
+
// Soprano installed and running - all good!
|
|
1429
|
+
console.log(chalk.green('\nā Soprano TTS detected and running!\n'));
|
|
1430
|
+
config.provider = provider;
|
|
1431
|
+
providerSelected = true;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
// If not installed, fall through to normal validation below
|
|
1435
|
+
}
|
|
1436
|
+
|
|
883
1437
|
const validation = await validateProvider(provider);
|
|
884
1438
|
|
|
885
1439
|
if (!validation.installed) {
|
|
@@ -956,6 +1510,9 @@ async function collectConfiguration(options = {}) {
|
|
|
956
1510
|
console.log(chalk.green(`\nā ${displayName} Detected and selected!\n`));
|
|
957
1511
|
config.provider = provider;
|
|
958
1512
|
providerSelected = true; // Exit provider selection loop
|
|
1513
|
+
|
|
1514
|
+
// Auto-advance flag for navigation
|
|
1515
|
+
config._autoAdvance = true;
|
|
959
1516
|
}
|
|
960
1517
|
}
|
|
961
1518
|
|
|
@@ -1148,36 +1705,105 @@ async function collectConfiguration(options = {}) {
|
|
|
1148
1705
|
));
|
|
1149
1706
|
|
|
1150
1707
|
if (config.provider === 'piper') {
|
|
1151
|
-
// Piper
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1708
|
+
// Check if Piper is installed for voice previews
|
|
1709
|
+
const piperAvailable = isPiperInstalled();
|
|
1710
|
+
|
|
1711
|
+
// Load voice metadata for friendly names
|
|
1712
|
+
let voiceMetadata;
|
|
1713
|
+
try {
|
|
1714
|
+
const metadataPath = path.join(__dirname, '..', '.agentvibes', 'config', 'voice-metadata.json');
|
|
1715
|
+
voiceMetadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
voiceMetadata = null;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Build voice choices
|
|
1721
|
+
const previewHint = piperAvailable ? ' ' + chalk.gray('[SPACE to preview]') : '';
|
|
1722
|
+
let piperVoices;
|
|
1723
|
+
|
|
1724
|
+
if (voiceMetadata && voiceMetadata.installerVoices) {
|
|
1725
|
+
// Use voice metadata system - all 10 voices with friendly names
|
|
1726
|
+
piperVoices = voiceMetadata.installerVoices.map(friendlyName => {
|
|
1727
|
+
const voice = voiceMetadata.voices[friendlyName];
|
|
1728
|
+
const isMale = voice.gender === 'male';
|
|
1729
|
+
const color = isMale ? chalk.cyan : chalk.hex('#FF69B4'); // Lighter pink for females
|
|
1730
|
+
|
|
1731
|
+
return {
|
|
1732
|
+
name: color(voice.displayName) + chalk.gray(` (${voice.gender}, ${voice.accent}, ${voice.quality})`) + previewHint,
|
|
1733
|
+
value: friendlyName // Store friendly name
|
|
1734
|
+
};
|
|
1735
|
+
});
|
|
1736
|
+
} else {
|
|
1737
|
+
// Fallback to old hardcoded list
|
|
1738
|
+
piperVoices = [
|
|
1739
|
+
{ name: chalk.cyan('en_US-ryan-high') + chalk.gray(' (Male, American, High Quality)') + previewHint, value: 'en_US-ryan-high' },
|
|
1740
|
+
{ name: chalk.magenta('en_US-amy-medium') + chalk.gray(' (Female, American, Clear)') + previewHint, value: 'en_US-amy-medium' },
|
|
1741
|
+
{ name: chalk.cyan('en_US-joe-medium') + chalk.gray(' (Male, American, Warm)') + previewHint, value: 'en_US-joe-medium' },
|
|
1742
|
+
{ name: chalk.magenta('en_US-lessac-medium') + chalk.gray(' (Female, American, Professional)') + previewHint, value: 'en_US-lessac-medium' },
|
|
1743
|
+
{ name: chalk.cyan('en_GB-alan-medium') + chalk.gray(' (Male, British, Refined)') + previewHint, value: 'en_GB-alan-medium' },
|
|
1744
|
+
{ name: chalk.magenta('en_GB-southern_english_female-medium') + chalk.gray(' (Female, British)') + previewHint, value: 'en_GB-southern_english_female-medium' }
|
|
1745
|
+
];
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
piperVoices.push(
|
|
1159
1749
|
new inquirer.Separator(),
|
|
1160
1750
|
{ name: chalk.yellow('Skip - I\'ll set this later'), value: '__skip__' },
|
|
1161
1751
|
{ name: chalk.magentaBright('ā Back to Provider Selection'), value: '__back__' }
|
|
1162
|
-
|
|
1752
|
+
);
|
|
1163
1753
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1754
|
+
let selectedVoice;
|
|
1755
|
+
if (piperAvailable) {
|
|
1756
|
+
const result = await createPreviewListPrompt(inquirer, {
|
|
1757
|
+
name: 'selectedVoice',
|
|
1758
|
+
message: chalk.yellow('Select your default Piper voice:'),
|
|
1759
|
+
choices: piperVoices,
|
|
1760
|
+
default: 'en_US-ryan-high',
|
|
1761
|
+
pageSize: 12,
|
|
1762
|
+
onPreview: async (voiceName) => {
|
|
1763
|
+
await playVoiceSample(voiceName, 'piper');
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
selectedVoice = result.selectedVoice;
|
|
1767
|
+
} else {
|
|
1768
|
+
const result = await inquirer.prompt([{
|
|
1769
|
+
type: 'list',
|
|
1770
|
+
name: 'selectedVoice',
|
|
1771
|
+
message: chalk.yellow('Select your default Piper voice:'),
|
|
1772
|
+
choices: piperVoices,
|
|
1773
|
+
default: 'en_US-ryan-high',
|
|
1774
|
+
pageSize: 12
|
|
1775
|
+
}]);
|
|
1776
|
+
selectedVoice = result.selectedVoice;
|
|
1777
|
+
}
|
|
1172
1778
|
|
|
1173
1779
|
if (selectedVoice === '__back__') {
|
|
1174
1780
|
return null;
|
|
1175
1781
|
}
|
|
1176
1782
|
|
|
1177
1783
|
if (selectedVoice !== '__skip__') {
|
|
1178
|
-
|
|
1784
|
+
// Convert friendly name to Piper ID if using metadata
|
|
1785
|
+
if (voiceMetadata && voiceMetadata.voices[selectedVoice]) {
|
|
1786
|
+
config.defaultVoice = voiceMetadata.voices[selectedVoice].id;
|
|
1787
|
+
console.log(chalk.green(`\nā Voice selected: ${voiceMetadata.voices[selectedVoice].displayName} (${config.defaultVoice})\n`));
|
|
1788
|
+
} else {
|
|
1789
|
+
config.defaultVoice = selectedVoice;
|
|
1790
|
+
console.log(chalk.green(`\nā Voice selected: ${selectedVoice}\n`));
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Show hint about voice browser
|
|
1794
|
+
console.log(boxen(
|
|
1795
|
+
chalk.cyan('š” Want to explore 914+ voices?\n\n') +
|
|
1796
|
+
chalk.white('Run: ') + chalk.yellow('npx agentvibes-voice-browser') + chalk.gray('\n\nBrowse all LibriTTS voices with preview, search, and install.'),
|
|
1797
|
+
{
|
|
1798
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
1799
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
1800
|
+
borderStyle: 'round',
|
|
1801
|
+
borderColor: 'cyan',
|
|
1802
|
+
dimBorder: true
|
|
1803
|
+
}
|
|
1804
|
+
));
|
|
1805
|
+
|
|
1179
1806
|
// Auto-advance to next page after selection
|
|
1180
|
-
console.log(chalk.green(`\nā Voice selected: ${selectedVoice}\n`));
|
|
1181
1807
|
currentPage++; // Skip to next page immediately
|
|
1182
1808
|
continue; // Skip navigation and go to next iteration
|
|
1183
1809
|
} else {
|
|
@@ -1215,9 +1841,24 @@ async function collectConfiguration(options = {}) {
|
|
|
1215
1841
|
}
|
|
1216
1842
|
|
|
1217
1843
|
if (selectedVoice !== '__skip__') {
|
|
1844
|
+
// macOS voices use their name directly (no piper metadata conversion needed)
|
|
1218
1845
|
config.defaultVoice = selectedVoice;
|
|
1219
|
-
// Auto-advance to next page after selection
|
|
1220
1846
|
console.log(chalk.green(`\nā Voice selected: ${selectedVoice}\n`));
|
|
1847
|
+
|
|
1848
|
+
// Show hint about voice browser
|
|
1849
|
+
console.log(boxen(
|
|
1850
|
+
chalk.cyan('š” Want to explore 914+ voices?\n\n') +
|
|
1851
|
+
chalk.white('Run: ') + chalk.yellow('npx agentvibes-voice-browser') + chalk.gray('\n\nBrowse all LibriTTS voices with preview, search, and install.'),
|
|
1852
|
+
{
|
|
1853
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
1854
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
1855
|
+
borderStyle: 'round',
|
|
1856
|
+
borderColor: 'cyan',
|
|
1857
|
+
dimBorder: true
|
|
1858
|
+
}
|
|
1859
|
+
));
|
|
1860
|
+
|
|
1861
|
+
// Auto-advance to next page after selection
|
|
1221
1862
|
currentPage++; // Skip to next page immediately
|
|
1222
1863
|
continue; // Skip navigation and go to next iteration
|
|
1223
1864
|
} else {
|
|
@@ -1314,7 +1955,69 @@ async function collectConfiguration(options = {}) {
|
|
|
1314
1955
|
}
|
|
1315
1956
|
|
|
1316
1957
|
} else if (currentPage === 3) {
|
|
1317
|
-
// Page 4: Personality Selection
|
|
1958
|
+
// Page 4: Pretext and Personality Selection
|
|
1959
|
+
console.log(boxen(
|
|
1960
|
+
chalk.white('Customize your Agent\'s introduction!\n\n') +
|
|
1961
|
+
chalk.gray('Add optional intro text that prefixes all TTS messages.\n') +
|
|
1962
|
+
chalk.gray('Examples: "FireBot: ", "Agent: ", "š¤ Assistant: "\n\n') +
|
|
1963
|
+
chalk.gray('You can change this anytime with: ') + chalk.cyan('/agent-vibes:set-pretext <text>'),
|
|
1964
|
+
{
|
|
1965
|
+
padding: 1,
|
|
1966
|
+
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
1967
|
+
borderStyle: 'round',
|
|
1968
|
+
borderColor: 'gray',
|
|
1969
|
+
width: 80
|
|
1970
|
+
}
|
|
1971
|
+
));
|
|
1972
|
+
|
|
1973
|
+
// Pretext input with validation
|
|
1974
|
+
let pretextValid = false;
|
|
1975
|
+
let pretext = config.pretext || '';
|
|
1976
|
+
|
|
1977
|
+
while (!pretextValid) {
|
|
1978
|
+
const { pretextInput } = await inquirer.prompt([{
|
|
1979
|
+
type: 'input',
|
|
1980
|
+
name: 'pretextInput',
|
|
1981
|
+
message: chalk.yellow('Enter intro text (optional, max 50 chars):'),
|
|
1982
|
+
default: pretext,
|
|
1983
|
+
// Live character count display in prompt
|
|
1984
|
+
prefix: `${chalk.cyan('[Intro Text]')}`,
|
|
1985
|
+
validate: (input) => {
|
|
1986
|
+
// Trim first for consistent length checking
|
|
1987
|
+
const trimmed = input.trim();
|
|
1988
|
+
// Check for newlines
|
|
1989
|
+
if (input.includes('\n') || input.includes('\r')) {
|
|
1990
|
+
return chalk.red('Error: Newlines not allowed');
|
|
1991
|
+
}
|
|
1992
|
+
// Check length after trimming for consistency with display
|
|
1993
|
+
if (trimmed.length > 50) {
|
|
1994
|
+
return chalk.red(`Too long: ${trimmed.length}/50 characters`);
|
|
1995
|
+
}
|
|
1996
|
+
return true;
|
|
1997
|
+
},
|
|
1998
|
+
filter: (input) => {
|
|
1999
|
+
// Trim and treat whitespace-only as empty
|
|
2000
|
+
const trimmed = input.trim();
|
|
2001
|
+
return trimmed;
|
|
2002
|
+
}
|
|
2003
|
+
}]);
|
|
2004
|
+
|
|
2005
|
+
pretext = pretextInput;
|
|
2006
|
+
|
|
2007
|
+
// Display preview
|
|
2008
|
+
if (pretext) {
|
|
2009
|
+
console.log(chalk.yellow(`\nCharacter count: ${pretext.length}/50`));
|
|
2010
|
+
console.log(chalk.cyan(`Preview: "${pretext}" This is how it will sound\n`));
|
|
2011
|
+
console.log(chalk.green(`ā Intro text set: "${pretext}"\n`));
|
|
2012
|
+
} else {
|
|
2013
|
+
console.log(chalk.gray('ā No intro text (messages will speak normally)\n'));
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
config.pretext = pretext;
|
|
2017
|
+
pretextValid = true;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Page 5: Personality Selection
|
|
1318
2021
|
console.log(boxen(
|
|
1319
2022
|
chalk.white('Give your Agent a personality!\n\n') +
|
|
1320
2023
|
chalk.gray('Personalities add character and style to TTS responses.\n') +
|
|
@@ -1334,6 +2037,9 @@ async function collectConfiguration(options = {}) {
|
|
|
1334
2037
|
const personalitiesDir = path.join(__dirname, '..', '.claude', 'personalities');
|
|
1335
2038
|
let personalityChoices = [];
|
|
1336
2039
|
|
|
2040
|
+
// Story 2.3 & 2.4: Check emoji support once, pass to all personality icons (performance fix)
|
|
2041
|
+
const emojiSupported = supportsEmoji();
|
|
2042
|
+
|
|
1337
2043
|
try {
|
|
1338
2044
|
const personalityFiles = await fs.readdir(personalitiesDir);
|
|
1339
2045
|
const personalities = [];
|
|
@@ -1359,16 +2065,18 @@ async function collectConfiguration(options = {}) {
|
|
|
1359
2065
|
personalities.sort((a, b) => a.name.localeCompare(b.name));
|
|
1360
2066
|
|
|
1361
2067
|
// Add "none" as first option (default)
|
|
2068
|
+
const noneIcon = getPersonalityIcon('none', emojiSupported);
|
|
1362
2069
|
personalityChoices.push(
|
|
1363
|
-
{ name: chalk.green('none') + chalk.gray(' (Professional, no personality) - Recommended'), value: 'none' },
|
|
2070
|
+
{ name: noneIcon + ' ' + chalk.green('none') + chalk.gray(' (Professional, no personality) - Recommended'), value: 'none' },
|
|
1364
2071
|
new inquirer.Separator(chalk.gray('ā'.repeat(60)))
|
|
1365
2072
|
);
|
|
1366
2073
|
|
|
1367
2074
|
// Add all other personalities
|
|
1368
2075
|
for (const p of personalities) {
|
|
1369
2076
|
if (p.name !== 'normal') { // Skip 'normal' as it's similar to 'none'
|
|
2077
|
+
const icon = getPersonalityIcon(p.name, emojiSupported);
|
|
1370
2078
|
personalityChoices.push({
|
|
1371
|
-
name: chalk.cyan(p.name) + chalk.gray(` - ${p.description}`),
|
|
2079
|
+
name: icon + ' ' + chalk.cyan(p.name) + chalk.gray(` - ${p.description}`),
|
|
1372
2080
|
value: p.name
|
|
1373
2081
|
});
|
|
1374
2082
|
}
|
|
@@ -1387,14 +2095,28 @@ async function collectConfiguration(options = {}) {
|
|
|
1387
2095
|
];
|
|
1388
2096
|
}
|
|
1389
2097
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
2098
|
+
// Story 2.3 & 2.4: Show emoji support status in help text (reuse emojiSupported from above)
|
|
2099
|
+
const emojiNote = emojiSupported
|
|
2100
|
+
? chalk.gray('(Emoji icons shown for visual recognition)')
|
|
2101
|
+
: chalk.gray('(Text labels shown - emoji not supported in this terminal)');
|
|
2102
|
+
|
|
2103
|
+
// Use search prompt for keyboard navigation (type to filter)
|
|
2104
|
+
const selectedPersonality = await search({
|
|
2105
|
+
message: chalk.yellow('Select your default personality (type to search):') + ' ' + emojiNote,
|
|
2106
|
+
source: async (input) => {
|
|
2107
|
+
// Filter personalityChoices based on input
|
|
2108
|
+
if (!input) {
|
|
2109
|
+
return personalityChoices;
|
|
2110
|
+
}
|
|
2111
|
+
return personalityChoices.filter(choice => {
|
|
2112
|
+
// Check if choice is a Separator, if so skip it
|
|
2113
|
+
if (!choice.value) return false;
|
|
2114
|
+
return choice.value.toLowerCase().includes(input.toLowerCase()) ||
|
|
2115
|
+
(choice.name && choice.name.toLowerCase().includes(input.toLowerCase()));
|
|
2116
|
+
});
|
|
2117
|
+
},
|
|
1396
2118
|
pageSize: 15
|
|
1397
|
-
}
|
|
2119
|
+
});
|
|
1398
2120
|
|
|
1399
2121
|
if (selectedPersonality === '__back__') {
|
|
1400
2122
|
currentPage--; // Go back to voice selection
|
|
@@ -1487,15 +2209,45 @@ async function collectConfiguration(options = {}) {
|
|
|
1487
2209
|
{ name: 'Light (Small room) - Recommended', value: 'light' },
|
|
1488
2210
|
{ name: 'Medium (Conference room)', value: 'medium' },
|
|
1489
2211
|
{ name: 'Heavy (Large hall)', value: 'heavy' },
|
|
1490
|
-
{ name: 'Cathedral (Epic space)', value: 'cathedral' }
|
|
2212
|
+
{ name: 'Cathedral (Epic space)', value: 'cathedral' },
|
|
2213
|
+
new inquirer.Separator(),
|
|
2214
|
+
{ name: chalk.magentaBright('ā Previous'), value: '__back__' }
|
|
1491
2215
|
],
|
|
1492
2216
|
default: config.reverb || 'light'
|
|
1493
2217
|
}]);
|
|
1494
2218
|
|
|
2219
|
+
if (reverbLevel === '__back__') {
|
|
2220
|
+
currentPage--;
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
1495
2224
|
config.reverb = reverbLevel;
|
|
1496
2225
|
|
|
1497
|
-
|
|
1498
|
-
|
|
2226
|
+
console.log(chalk.green('\nā Reverb level set\n'));
|
|
2227
|
+
currentPage++;
|
|
2228
|
+
continue;
|
|
2229
|
+
|
|
2230
|
+
} else if (currentPage === 5) {
|
|
2231
|
+
// Page 6: Background Music Settings
|
|
2232
|
+
|
|
2233
|
+
// Skip for termux-ssh - background music doesn't work with SSH text-only TTS
|
|
2234
|
+
if (config.provider === 'termux-ssh' || config.provider === 'ssh-pulseaudio') {
|
|
2235
|
+
console.log(boxen(
|
|
2236
|
+
chalk.white('SSH-Remote: Audio Effects Apply on Android\n\n') +
|
|
2237
|
+
chalk.green('ā
Background music works:\n') +
|
|
2238
|
+
chalk.gray(' ⢠Music plays on Android device\n') +
|
|
2239
|
+
chalk.gray(' ⢠All settings configured below will apply\n\n') +
|
|
2240
|
+
chalk.cyan('Configure background music below!'),
|
|
2241
|
+
{
|
|
2242
|
+
padding: 1,
|
|
2243
|
+
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
2244
|
+
borderStyle: 'round',
|
|
2245
|
+
borderColor: 'green',
|
|
2246
|
+
width: 80
|
|
2247
|
+
}
|
|
2248
|
+
));
|
|
2249
|
+
console.log('');
|
|
2250
|
+
}
|
|
1499
2251
|
|
|
1500
2252
|
// Background music
|
|
1501
2253
|
console.log(chalk.gray('šµ Background music plays ambient tracks during TTS for a more engaging experience.'));
|
|
@@ -1510,48 +2262,194 @@ async function collectConfiguration(options = {}) {
|
|
|
1510
2262
|
config.backgroundMusic.enabled = enableMusic;
|
|
1511
2263
|
|
|
1512
2264
|
if (enableMusic) {
|
|
2265
|
+
// Check if an MP3-capable player is available; offer to install ffmpeg if not
|
|
2266
|
+
const { execSync: _execSync } = await import('child_process');
|
|
2267
|
+
const _mp3Players = ['ffplay', 'mpg123', 'mpv', 'cvlc'];
|
|
2268
|
+
const _hasPlayer = _mp3Players.some(p => {
|
|
2269
|
+
try { _execSync(`which ${p}`, { stdio: 'pipe' }); return true; } catch { return false; }
|
|
2270
|
+
});
|
|
2271
|
+
if (!_hasPlayer) {
|
|
2272
|
+
console.log('');
|
|
2273
|
+
console.log(chalk.yellow('ā ļø No MP3 player found ā background music requires one.'));
|
|
2274
|
+
console.log(chalk.gray(' ffmpeg is recommended (provides ffplay for MP3 playback).\n'));
|
|
2275
|
+
|
|
2276
|
+
const _osPlatform = process.platform;
|
|
2277
|
+
let _installCmd;
|
|
2278
|
+
if (_osPlatform === 'darwin') {
|
|
2279
|
+
_installCmd = 'brew install ffmpeg';
|
|
2280
|
+
} else {
|
|
2281
|
+
// Prefer pkexec (GUI password dialog) when available ā works in
|
|
2282
|
+
// environments where sudo lacks a tty (e.g., AI assistant terminals).
|
|
2283
|
+
// Fall back to sudo for headless/SSH setups.
|
|
2284
|
+
let _hasPkexec = false;
|
|
2285
|
+
try { _execSync('which pkexec', { stdio: 'pipe' }); _hasPkexec = true; } catch {}
|
|
2286
|
+
_installCmd = _hasPkexec
|
|
2287
|
+
? 'pkexec apt-get install -y ffmpeg'
|
|
2288
|
+
: 'sudo apt-get install -y ffmpeg';
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
if (!options.yes) {
|
|
2292
|
+
const { installFfmpeg } = await inquirer.prompt([{
|
|
2293
|
+
type: 'confirm',
|
|
2294
|
+
name: 'installFfmpeg',
|
|
2295
|
+
message: chalk.yellow(`Install ffmpeg now? (${_installCmd})`),
|
|
2296
|
+
default: true,
|
|
2297
|
+
}]);
|
|
2298
|
+
if (installFfmpeg) {
|
|
2299
|
+
try {
|
|
2300
|
+
console.log(chalk.cyan(`\nš¦ Running: ${_installCmd}\n`));
|
|
2301
|
+
const { execSync: _exec } = await import('child_process');
|
|
2302
|
+
_exec(_installCmd, { stdio: 'inherit', timeout: 120000 });
|
|
2303
|
+
console.log(chalk.green('\nā
ffmpeg installed successfully!\n'));
|
|
2304
|
+
} catch {
|
|
2305
|
+
console.log(chalk.yellow('\nā ļø ffmpeg installation failed. You can install it manually:'));
|
|
2306
|
+
console.log(chalk.cyan(` ${_installCmd}\n`));
|
|
2307
|
+
}
|
|
2308
|
+
} else {
|
|
2309
|
+
console.log(chalk.gray(` Install later with: ${_installCmd}\n`));
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// Check for sox ā required to mix background music into TTS audio
|
|
2315
|
+
let _hasSox = false;
|
|
2316
|
+
try { _execSync('which sox', { stdio: 'pipe' }); _hasSox = true; } catch {}
|
|
2317
|
+
if (!_hasSox) {
|
|
2318
|
+
console.log('');
|
|
2319
|
+
console.log(chalk.yellow('ā ļø sox not found ā required to mix background music into voice audio.'));
|
|
2320
|
+
console.log(chalk.gray(' Without sox, you\'ll hear voice only, no background music.\n'));
|
|
2321
|
+
|
|
2322
|
+
const _osPlatform2 = process.platform;
|
|
2323
|
+
let _soxCmd;
|
|
2324
|
+
if (_osPlatform2 === 'darwin') {
|
|
2325
|
+
_soxCmd = 'brew install sox';
|
|
2326
|
+
} else {
|
|
2327
|
+
let _hasPkexec2 = false;
|
|
2328
|
+
try { _execSync('which pkexec', { stdio: 'pipe' }); _hasPkexec2 = true; } catch {}
|
|
2329
|
+
_soxCmd = _hasPkexec2
|
|
2330
|
+
? 'pkexec apt-get install -y sox libsox-fmt-mp3'
|
|
2331
|
+
: 'sudo apt-get install -y sox libsox-fmt-mp3';
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
if (!options.yes) {
|
|
2335
|
+
const { installSox } = await inquirer.prompt([{
|
|
2336
|
+
type: 'confirm',
|
|
2337
|
+
name: 'installSox',
|
|
2338
|
+
message: chalk.yellow(`Install sox now? (${_soxCmd})`),
|
|
2339
|
+
default: true,
|
|
2340
|
+
}]);
|
|
2341
|
+
if (installSox) {
|
|
2342
|
+
try {
|
|
2343
|
+
console.log(chalk.cyan(`\nš¦ Running: ${_soxCmd}\n`));
|
|
2344
|
+
_execSync(_soxCmd, { stdio: 'inherit', timeout: 120000 });
|
|
2345
|
+
console.log(chalk.green('\nā
sox installed successfully!\n'));
|
|
2346
|
+
} catch {
|
|
2347
|
+
console.log(chalk.yellow('\nā ļø sox installation failed. You can install it manually:'));
|
|
2348
|
+
console.log(chalk.cyan(` ${_soxCmd}\n`));
|
|
2349
|
+
}
|
|
2350
|
+
} else {
|
|
2351
|
+
console.log(chalk.gray(` Install later with: ${_soxCmd}\n`));
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
1513
2356
|
// Add spacing before track selection
|
|
1514
2357
|
console.log('');
|
|
1515
2358
|
console.log(chalk.gray('š¼ Choose your default background music genre (you can change this anytime).'));
|
|
1516
2359
|
|
|
2360
|
+
// Load custom tracks from registry
|
|
2361
|
+
const customTracks = await loadCustomTracks();
|
|
2362
|
+
const customTrackChoices = customTracks.map(track => ({
|
|
2363
|
+
name: `š ${track.name} ` + chalk.gray('[SPACE to preview]'),
|
|
2364
|
+
value: track.filename
|
|
2365
|
+
}));
|
|
2366
|
+
|
|
1517
2367
|
const trackChoices = [
|
|
1518
|
-
{ name: 'š» Soft Flamenco (Spanish guitar)', value: 'agentvibes_soft_flamenco_loop.mp3' },
|
|
1519
|
-
{ name: 'šŗ Bachata (Latin - Romantic guitar & bongos)', value: 'agent_vibes_bachata_v1_loop.mp3' },
|
|
1520
|
-
{ name: 'š Salsa (Latin - Upbeat brass & percussion)', value: 'agent_vibes_salsa_v2_loop.mp3' },
|
|
1521
|
-
{ name: 'šø Cumbia (Latin - Accordion & drums)', value: 'agent_vibes_cumbia_v1_loop.mp3' },
|
|
1522
|
-
{ name: 'šø Bossa Nova (Brazilian jazz)', value: 'agent_vibes_bossa_nova_v2_loop.mp3' },
|
|
1523
|
-
{ name: 'šļø Japanese City Pop (80s synth)', value: 'agent_vibes_japanese_city_pop_v1_loop.mp3' },
|
|
1524
|
-
{ name: 'š Chillwave (Electronic ambient)', value: 'agent_vibes_chillwave_v2_loop.mp3' },
|
|
1525
|
-
{ name: '
|
|
1526
|
-
{ name: '
|
|
1527
|
-
{ name: '
|
|
1528
|
-
{ name: '
|
|
1529
|
-
{ name: '
|
|
1530
|
-
{ name: '
|
|
1531
|
-
{ name: '
|
|
1532
|
-
{ name: '
|
|
1533
|
-
{ name: 'š„ Tabla Dream Pop (Indian percussion)', value: 'agent_vibes_tabla_dream_pop_v1_loop.mp3' }
|
|
2368
|
+
{ name: 'š» Soft Flamenco (Spanish guitar) ' + chalk.gray('[SPACE to preview]'), value: 'agentvibes_soft_flamenco_loop.mp3' },
|
|
2369
|
+
{ name: 'šŗ Bachata (Latin - Romantic guitar & bongos) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_bachata_v1_loop.mp3' },
|
|
2370
|
+
{ name: 'š Salsa (Latin - Upbeat brass & percussion) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_salsa_v2_loop.mp3' },
|
|
2371
|
+
{ name: 'šø Cumbia (Latin - Accordion & drums) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_cumbia_v1_loop.mp3' },
|
|
2372
|
+
{ name: 'šø Bossa Nova (Brazilian jazz) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_bossa_nova_v2_loop.mp3' },
|
|
2373
|
+
{ name: 'šļø Japanese City Pop (80s synth) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_japanese_city_pop_v1_loop.mp3' },
|
|
2374
|
+
{ name: 'š Chillwave (Electronic ambient) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_chillwave_v2_loop.mp3' },
|
|
2375
|
+
{ name: 'š Dark Chill Step (Electronic bass) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_dark_chill_step_loop.mp3' },
|
|
2376
|
+
{ name: 'šļø Goa Trance (Psychedelic electronic) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_goa_trance_v2_loop.mp3' },
|
|
2377
|
+
{ name: 'š¼ Harpsichord (Baroque classical) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_harpsichord_v2_loop.mp3' },
|
|
2378
|
+
{ name: 'š» Celtic Harp (Irish traditional) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_celtic_harp_v1_loop.mp3' },
|
|
2379
|
+
{ name: 'šŗ Hawaiian Slack Key Guitar ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3' },
|
|
2380
|
+
{ name: 'šļø Arabic Oud (Middle Eastern) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_arabic_v2_loop.mp3' },
|
|
2381
|
+
{ name: 'šŖ Gnawa Ambient (North African) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_ganawa_ambient_v2_loop.mp3' },
|
|
2382
|
+
{ name: 'š„ Tabla Dream Pop (Indian percussion) ' + chalk.gray('[SPACE to preview]'), value: 'agent_vibes_tabla_dream_pop_v1_loop.mp3' }
|
|
1534
2383
|
];
|
|
1535
2384
|
|
|
1536
|
-
|
|
1537
|
-
|
|
2385
|
+
// Add custom tracks separator and options if any exist
|
|
2386
|
+
if (customTrackChoices.length > 0) {
|
|
2387
|
+
trackChoices.push(
|
|
2388
|
+
new inquirer.Separator(chalk.gray('ā'.repeat(50))),
|
|
2389
|
+
...customTrackChoices
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// Add custom track option
|
|
2394
|
+
trackChoices.push(
|
|
2395
|
+
new inquirer.Separator(chalk.gray('ā'.repeat(50))),
|
|
2396
|
+
{ name: 'ā Add Custom Track...', value: '__custom__' }
|
|
2397
|
+
);
|
|
2398
|
+
|
|
2399
|
+
// Interactive track selection - Enter=Select, Spacebar=Preview
|
|
2400
|
+
const tracksDir = path.join(__dirname, '..', '.claude', 'audio', 'tracks');
|
|
2401
|
+
|
|
2402
|
+
const result = await createPreviewListPrompt(inquirer, {
|
|
1538
2403
|
name: 'selectedTrack',
|
|
1539
|
-
message: chalk.yellow('Choose
|
|
2404
|
+
message: chalk.yellow('Choose background music:'),
|
|
1540
2405
|
choices: trackChoices,
|
|
1541
2406
|
default: config.backgroundMusic.track || 'agentvibes_soft_flamenco_loop.mp3',
|
|
1542
|
-
pageSize:
|
|
1543
|
-
|
|
2407
|
+
pageSize: 18,
|
|
2408
|
+
loop: false,
|
|
2409
|
+
onPreview: async (trackFile) => {
|
|
2410
|
+
console.log(chalk.cyan('\n š Playing preview...\n'));
|
|
2411
|
+
await previewAudioTrack(trackFile, tracksDir);
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
const selectedTrack = result.selectedTrack;
|
|
2415
|
+
|
|
2416
|
+
// Handle custom track selection
|
|
2417
|
+
if (selectedTrack === '__custom__') {
|
|
2418
|
+
console.log('');
|
|
2419
|
+
const result = await promptForCustomMusic(claudeDir);
|
|
2420
|
+
|
|
2421
|
+
if (result.success && result.filename) {
|
|
2422
|
+
config.backgroundMusic.track = result.filename;
|
|
2423
|
+
|
|
2424
|
+
// Update registry
|
|
2425
|
+
const allCustomTracks = await loadCustomTracks();
|
|
2426
|
+
if (!allCustomTracks.some(t => t.filename === result.filename)) {
|
|
2427
|
+
const trackName = path.basename(result.filename, path.extname(result.filename));
|
|
2428
|
+
allCustomTracks.push({ name: trackName, filename: result.filename });
|
|
2429
|
+
await saveCustomTracks(allCustomTracks);
|
|
2430
|
+
}
|
|
2431
|
+
} else {
|
|
2432
|
+
// Fallback to default
|
|
2433
|
+
config.backgroundMusic.track = 'agentvibes_soft_flamenco_loop.mp3';
|
|
2434
|
+
if (result.error) {
|
|
2435
|
+
console.log(chalk.yellow(`ā ļø ${result.error}`));
|
|
2436
|
+
}
|
|
2437
|
+
console.log(chalk.yellow('ā ļø Using default track'));
|
|
2438
|
+
}
|
|
2439
|
+
} else {
|
|
2440
|
+
config.backgroundMusic.track = selectedTrack;
|
|
2441
|
+
}
|
|
1544
2442
|
|
|
1545
|
-
config.backgroundMusic.track
|
|
2443
|
+
console.log(chalk.green(`\nā Selected: ${config.backgroundMusic.track}\n`));
|
|
1546
2444
|
}
|
|
1547
2445
|
|
|
1548
2446
|
// Auto-advance to next page after audio settings
|
|
1549
|
-
console.log(chalk.green('
|
|
2447
|
+
console.log(chalk.green('ā Background music configured\n'));
|
|
1550
2448
|
currentPage++;
|
|
1551
2449
|
continue;
|
|
1552
2450
|
|
|
1553
|
-
} else if (currentPage ===
|
|
1554
|
-
// Page
|
|
2451
|
+
} else if (currentPage === 6) {
|
|
2452
|
+
// Page 7: Verbosity Settings
|
|
1555
2453
|
console.log(boxen(
|
|
1556
2454
|
chalk.white('Choose how much Claude speaks during interactions.\n\n') +
|
|
1557
2455
|
chalk.yellow('š High:\n') +
|
|
@@ -1582,23 +2480,39 @@ async function collectConfiguration(options = {}) {
|
|
|
1582
2480
|
choices: [
|
|
1583
2481
|
{ name: 'š High - Maximum transparency', value: 'high' },
|
|
1584
2482
|
{ name: 'š Medium - Balanced', value: 'medium' },
|
|
1585
|
-
{ name: 'š Low - Minimal', value: 'low' }
|
|
2483
|
+
{ name: 'š Low - Minimal', value: 'low' },
|
|
2484
|
+
new inquirer.Separator(),
|
|
2485
|
+
{ name: chalk.magentaBright('ā Previous'), value: '__back__' }
|
|
1586
2486
|
],
|
|
1587
2487
|
default: config.verbosity || 'high'
|
|
1588
2488
|
}]);
|
|
1589
2489
|
|
|
2490
|
+
if (verbosity === '__back__') {
|
|
2491
|
+
currentPage--;
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
1590
2495
|
config.verbosity = verbosity;
|
|
1591
2496
|
|
|
1592
|
-
//
|
|
2497
|
+
// Show confirmation and auto-advance to next page
|
|
1593
2498
|
console.log(chalk.green('\nā Verbosity level set\n'));
|
|
1594
2499
|
currentPage++;
|
|
1595
2500
|
continue;
|
|
1596
2501
|
}
|
|
1597
2502
|
|
|
1598
|
-
//
|
|
2503
|
+
// Auto-advance if provider was just detected (skip navigation prompt)
|
|
2504
|
+
if (config._autoAdvance) {
|
|
2505
|
+
delete config._autoAdvance;
|
|
2506
|
+
await new Promise(resolve => setTimeout(resolve, 800)); // Brief pause
|
|
2507
|
+
currentPage++;
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// Navigation with page titles
|
|
1599
2512
|
const navChoices = [];
|
|
1600
2513
|
if (currentPage < totalPages - 1) {
|
|
1601
|
-
|
|
2514
|
+
const nextPageTitle = getPageTitle(currentPage + 1).replace(/[š§šļøš¤šš§š]\s*/, ''); // Remove emoji
|
|
2515
|
+
navChoices.push({ name: chalk.green('Next ā') + chalk.gray(` (${nextPageTitle})`), value: 'next' });
|
|
1602
2516
|
} else {
|
|
1603
2517
|
navChoices.push({ name: chalk.cyan('ā Continue to Installation'), value: 'continue' });
|
|
1604
2518
|
}
|
|
@@ -1607,13 +2521,23 @@ async function collectConfiguration(options = {}) {
|
|
|
1607
2521
|
if (currentPage === 0) {
|
|
1608
2522
|
navChoices.push({ name: chalk.magentaBright('ā Back to Welcome'), value: 'back' });
|
|
1609
2523
|
} else {
|
|
1610
|
-
|
|
2524
|
+
const prevPageTitle = getPageTitle(currentPage - 1).replace(/[š§šļøš¤šš§š]\s*/, ''); // Remove emoji
|
|
2525
|
+
navChoices.push({ name: chalk.magentaBright('ā Previous') + chalk.gray(` (${prevPageTitle})`), value: 'prev' });
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// Set navigation message based on page status
|
|
2529
|
+
let navMessage = '';
|
|
2530
|
+
if (currentPage === 0 && pageStatus) {
|
|
2531
|
+
navMessage = pageStatus.allMet
|
|
2532
|
+
? chalk.green('ā') + chalk.cyan(' All system dependencies met')
|
|
2533
|
+
: chalk.yellow('ā ') + chalk.cyan(` ${pageStatus.missingCount} optional tool${pageStatus.missingCount > 1 ? 's' : ''} missing`);
|
|
1611
2534
|
}
|
|
1612
2535
|
|
|
1613
2536
|
const { action } = await inquirer.prompt([{
|
|
1614
2537
|
type: 'list',
|
|
1615
2538
|
name: 'action',
|
|
1616
|
-
message:
|
|
2539
|
+
message: navMessage,
|
|
2540
|
+
prefix: '',
|
|
1617
2541
|
choices: navChoices,
|
|
1618
2542
|
default: 'next'
|
|
1619
2543
|
}]);
|
|
@@ -1690,22 +2614,22 @@ function showWelcome() {
|
|
|
1690
2614
|
* Shown during install and update commands
|
|
1691
2615
|
*/
|
|
1692
2616
|
function getReleaseInfoBoxen() {
|
|
1693
|
-
return chalk.cyan.bold('š¦ AgentVibes
|
|
2617
|
+
return chalk.cyan.bold('š¦ AgentVibes v4.0.0 - Interactive Console & Voice Explorer\n\n') +
|
|
1694
2618
|
chalk.green.bold('šļø WHAT\'S NEW:\n\n') +
|
|
1695
|
-
chalk.cyan('
|
|
1696
|
-
chalk.cyan('
|
|
1697
|
-
chalk.cyan('
|
|
2619
|
+
chalk.cyan('Major release with a full interactive TUI console, voice browser with 914+ voices,\n') +
|
|
2620
|
+
chalk.cyan('and comprehensive platform support. Includes 58 security fixes, reliable TTS hooks,\n') +
|
|
2621
|
+
chalk.cyan('and support for Windows, macOS, Android/Termux, and SSH-remote audio.\n\n') +
|
|
1698
2622
|
chalk.green.bold('⨠KEY HIGHLIGHTS:\n\n') +
|
|
1699
|
-
chalk.gray('
|
|
1700
|
-
chalk.gray('
|
|
1701
|
-
chalk.gray('
|
|
1702
|
-
chalk.gray('
|
|
1703
|
-
chalk.gray('
|
|
2623
|
+
chalk.gray(' š„ļø Interactive TUI Console - Settings, Voices, Music tabs with live preview\n') +
|
|
2624
|
+
chalk.gray(' š¤ Voice Browser - Browse and preview 914+ Piper TTS voices\n') +
|
|
2625
|
+
chalk.gray(' š§ Reliable TTS Hooks - JSON context injection, auto git-init\n') +
|
|
2626
|
+
chalk.gray(' š Multi-Platform - Windows, macOS, Android/Termux, SSH-remote\n') +
|
|
2627
|
+
chalk.gray(' š Security Hardened - 58 issues fixed, 180+ security tests\n\n') +
|
|
1704
2628
|
chalk.gray('š Full Release Notes: RELEASE_NOTES.md\n') +
|
|
1705
2629
|
chalk.gray('š Website: https://agentvibes.org\n') +
|
|
1706
2630
|
chalk.gray('š¦ Repository: https://github.com/paulpreibisch/AgentVibes\n\n') +
|
|
1707
2631
|
chalk.gray('Co-created by Paul Preibisch with Claude AI\n') +
|
|
1708
|
-
chalk.gray('Copyright Ā©
|
|
2632
|
+
chalk.gray('Copyright Ā© 2026 Paul Preibisch | Apache-2.0 License');
|
|
1709
2633
|
}
|
|
1710
2634
|
|
|
1711
2635
|
/**
|
|
@@ -2509,30 +3433,6 @@ async function copyPersonalityFiles(targetDir, spinner) {
|
|
|
2509
3433
|
content += chalk.gray('Personalities change how Claude speaks - adding humor, emotion, or style.\n');
|
|
2510
3434
|
content += chalk.gray('Change with: ') + chalk.yellow('/agent-vibes:personality <name>') + chalk.gray(' or say "change personality to sassy"\n\n');
|
|
2511
3435
|
|
|
2512
|
-
// Map personalities to emojis
|
|
2513
|
-
const personalityEmojis = {
|
|
2514
|
-
'angry': 'š ',
|
|
2515
|
-
'annoying': 'š¤',
|
|
2516
|
-
'crass': 'š¤¬',
|
|
2517
|
-
'dramatic': 'š',
|
|
2518
|
-
'dry-humor': 'š',
|
|
2519
|
-
'flirty': 'š',
|
|
2520
|
-
'funny': 'š',
|
|
2521
|
-
'grandpa': 'š“',
|
|
2522
|
-
'millennial': 'š',
|
|
2523
|
-
'moody': 'š',
|
|
2524
|
-
'normal': 'š',
|
|
2525
|
-
'pirate': 'š“āā ļø',
|
|
2526
|
-
'poetic': 'š',
|
|
2527
|
-
'professional': 'š',
|
|
2528
|
-
'rapper': 'š¤',
|
|
2529
|
-
'robot': 'š¤',
|
|
2530
|
-
'sarcastic': 'š',
|
|
2531
|
-
'sassy': 'š',
|
|
2532
|
-
'surfer-dude': 'š',
|
|
2533
|
-
'zen': 'š§'
|
|
2534
|
-
};
|
|
2535
|
-
|
|
2536
3436
|
// Display personalities in two columns
|
|
2537
3437
|
const personalities = installedPersonalities.map(file => {
|
|
2538
3438
|
const name = file.replace('.md', '');
|
|
@@ -2859,8 +3759,33 @@ async function configureSessionStartHook(targetDir, spinner) {
|
|
|
2859
3759
|
} else {
|
|
2860
3760
|
spinner.info(chalk.yellow('SessionStart hook already configured\n'));
|
|
2861
3761
|
}
|
|
2862
|
-
} catch (error) {
|
|
2863
|
-
spinner.fail(chalk.red('Failed to configure hook: ' + error.message + '\n'));
|
|
3762
|
+
} catch (error) {
|
|
3763
|
+
spinner.fail(chalk.red('Failed to configure hook: ' + error.message + '\n'));
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
/**
|
|
3768
|
+
* Ensure target directory is a git repo (required for Claude Code hook context injection)
|
|
3769
|
+
* @param {string} targetDir - Target installation directory
|
|
3770
|
+
* @param {Object} spinner - Ora spinner instance
|
|
3771
|
+
*/
|
|
3772
|
+
async function ensureGitRepo(targetDir, spinner) {
|
|
3773
|
+
const gitDir = path.join(targetDir, '.git');
|
|
3774
|
+
try {
|
|
3775
|
+
await fs.access(gitDir);
|
|
3776
|
+
// Already a git repo
|
|
3777
|
+
} catch {
|
|
3778
|
+
console.log(chalk.cyan('\nš§ Initializing git repository (required for Claude Code hooks)...'));
|
|
3779
|
+
try {
|
|
3780
|
+
const { execSync: execSyncLocal } = await import('child_process');
|
|
3781
|
+
execSyncLocal('git init', { cwd: targetDir, stdio: 'pipe' });
|
|
3782
|
+
// Stage only files that exist
|
|
3783
|
+
execSyncLocal('git add .', { cwd: targetDir, stdio: 'pipe' });
|
|
3784
|
+
execSyncLocal('git commit -m "chore: initialize AgentVibes"', { cwd: targetDir, stdio: 'pipe' });
|
|
3785
|
+
console.log(chalk.green('ā Git repository initialized (required for TTS hooks)'));
|
|
3786
|
+
} catch (error) {
|
|
3787
|
+
console.log(chalk.yellow(`ā Could not initialize git repo - TTS hooks may not work: ${error.message}`));
|
|
3788
|
+
}
|
|
2864
3789
|
}
|
|
2865
3790
|
}
|
|
2866
3791
|
|
|
@@ -2932,7 +3857,7 @@ async function checkAndInstallPiper(targetDir, options) {
|
|
|
2932
3857
|
try {
|
|
2933
3858
|
if (fsSync.existsSync(piperDownloadPath)) {
|
|
2934
3859
|
execScript(`${piperDownloadPath} --yes`, {
|
|
2935
|
-
stdio: 'inherit',
|
|
3860
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
2936
3861
|
env: process.env
|
|
2937
3862
|
});
|
|
2938
3863
|
console.log(chalk.green('\nā
Voice models downloaded successfully!\n'));
|
|
@@ -2976,7 +3901,7 @@ async function checkAndInstallPiper(targetDir, options) {
|
|
|
2976
3901
|
|
|
2977
3902
|
try {
|
|
2978
3903
|
execScript(`${piperInstallerPath} --non-interactive`, {
|
|
2979
|
-
stdio: 'inherit',
|
|
3904
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
2980
3905
|
env: process.env
|
|
2981
3906
|
});
|
|
2982
3907
|
console.log(chalk.green('\nā
Piper TTS installed successfully!\n'));
|
|
@@ -3522,12 +4447,9 @@ async function executeMigrationScript(migrationScript, targetDir, spinner) {
|
|
|
3522
4447
|
try {
|
|
3523
4448
|
await fs.access(migrationScript);
|
|
3524
4449
|
|
|
3525
|
-
// Execute migration script using
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
const execFilePromise = promisify(execFile);
|
|
3529
|
-
|
|
3530
|
-
await execFilePromise('bash', [migrationScript], { cwd: targetDir });
|
|
4450
|
+
// Execute migration script using execFileSync to prevent command injection
|
|
4451
|
+
// Uses top-level import of execFileSync (ESM-compatible, no require())
|
|
4452
|
+
execFileSync('bash', [migrationScript], { cwd: targetDir, stdio: 'pipe' });
|
|
3531
4453
|
|
|
3532
4454
|
spinner.succeed(chalk.green('ā Configuration migrated to .agentvibes/'));
|
|
3533
4455
|
console.log(chalk.gray(' Old locations: .claude/config/, .claude/plugins/'));
|
|
@@ -3770,7 +4692,8 @@ async function updatePersonalityFiles(targetDir, srcPersonalitiesDir) {
|
|
|
3770
4692
|
* @returns {Object} Mock spinner object
|
|
3771
4693
|
*/
|
|
3772
4694
|
function createSilentSpinner() {
|
|
3773
|
-
|
|
4695
|
+
const s = { start: () => s, succeed: () => s, info: () => s, fail: () => s, warn: () => s, stop: () => s };
|
|
4696
|
+
return s;
|
|
3774
4697
|
}
|
|
3775
4698
|
|
|
3776
4699
|
/**
|
|
@@ -3845,6 +4768,7 @@ async function performUpdateOperations(targetDir, spinner) {
|
|
|
3845
4768
|
// Update settings.json
|
|
3846
4769
|
spinner.text = 'Updating AgentVibes hook configuration...';
|
|
3847
4770
|
await configureSessionStartHook(targetDir, silentSpinner);
|
|
4771
|
+
await ensureGitRepo(targetDir, silentSpinner);
|
|
3848
4772
|
|
|
3849
4773
|
// Detect and migrate old configuration
|
|
3850
4774
|
spinner.text = 'Checking for old configuration...';
|
|
@@ -3955,157 +4879,7 @@ async function install(options = {}) {
|
|
|
3955
4879
|
const piperVoicesPath = userConfig.piperPath;
|
|
3956
4880
|
const targetDir = options.directory || currentDir;
|
|
3957
4881
|
|
|
3958
|
-
//
|
|
3959
|
-
const preInstallPages = [];
|
|
3960
|
-
|
|
3961
|
-
// Page 1: Configuration Summary
|
|
3962
|
-
const providerLabels = {
|
|
3963
|
-
piper: 'Piper TTS',
|
|
3964
|
-
macos: 'macOS Say',
|
|
3965
|
-
soprano: 'Soprano TTS',
|
|
3966
|
-
'termux-ssh': 'Termux SSH (Android)',
|
|
3967
|
-
'ssh-pulseaudio': 'PulseAudio Tunnel',
|
|
3968
|
-
pulseaudio: 'PulseAudio Tunnel',
|
|
3969
|
-
'windows-piper': 'Windows Piper TTS',
|
|
3970
|
-
'windows-sapi': 'Windows SAPI'
|
|
3971
|
-
};
|
|
3972
|
-
const reverbLabels = {
|
|
3973
|
-
off: 'Off',
|
|
3974
|
-
light: 'Light',
|
|
3975
|
-
medium: 'Medium',
|
|
3976
|
-
heavy: 'Heavy',
|
|
3977
|
-
cathedral: 'Cathedral'
|
|
3978
|
-
};
|
|
3979
|
-
const verbosityLabels = {
|
|
3980
|
-
high: 'High',
|
|
3981
|
-
medium: 'Medium',
|
|
3982
|
-
low: 'Low'
|
|
3983
|
-
};
|
|
3984
|
-
|
|
3985
|
-
let configContent = chalk.bold('Your Configuration\n\n');
|
|
3986
|
-
configContent += chalk.cyan('š¤ TTS Provider:\n');
|
|
3987
|
-
configContent += chalk.white(` ${providerLabels[selectedProvider]}\n`);
|
|
3988
|
-
if (selectedProvider === 'piper' && piperVoicesPath) {
|
|
3989
|
-
configContent += chalk.gray(` Voice storage: ${piperVoicesPath}\n`);
|
|
3990
|
-
}
|
|
3991
|
-
if (selectedProvider === 'termux-ssh') {
|
|
3992
|
-
if (userConfig.sshHost) {
|
|
3993
|
-
configContent += chalk.gray(` SSH host: ${userConfig.sshHost}\n`);
|
|
3994
|
-
} else {
|
|
3995
|
-
configContent += chalk.yellow(` SSH host: Not configured (set later)\n`);
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
configContent += '\n';
|
|
3999
|
-
configContent += chalk.cyan('šļø Audio Settings:\n');
|
|
4000
|
-
configContent += chalk.white(` Reverb: ${reverbLabels[userConfig.reverb]}\n`);
|
|
4001
|
-
configContent += chalk.white(` Background Music: ${userConfig.backgroundMusic.enabled ? 'Enabled' : 'Disabled'}\n`);
|
|
4002
|
-
if (userConfig.backgroundMusic.enabled) {
|
|
4003
|
-
// Find the track name from the track choices
|
|
4004
|
-
const trackChoices = {
|
|
4005
|
-
'agentvibes_soft_flamenco_loop.mp3': 'Soft Flamenco',
|
|
4006
|
-
'agent_vibes_bachata_v1_loop.mp3': 'Bachata',
|
|
4007
|
-
'agent_vibes_salsa_v2_loop.mp3': 'Salsa',
|
|
4008
|
-
'agent_vibes_cumbia_v1_loop.mp3': 'Cumbia',
|
|
4009
|
-
'agent_vibes_bossa_nova_v2_loop.mp3': 'Bossa Nova',
|
|
4010
|
-
'agent_vibes_japanese_city_pop_v1_loop.mp3': 'Japanese City Pop',
|
|
4011
|
-
'agent_vibes_chillwave_v2_loop.mp3': 'Chillwave',
|
|
4012
|
-
'dreamy_house_loop.mp3': 'Dreamy House',
|
|
4013
|
-
'agent_vibes_dark_chill_step_loop.mp3': 'Dark Chill Step',
|
|
4014
|
-
'agent_vibes_goa_trance_v2_loop.mp3': 'Goa Trance',
|
|
4015
|
-
'agent_vibes_harpsichord_v2_loop.mp3': 'Harpsichord',
|
|
4016
|
-
'agent_vibes_celtic_harp_v1_loop.mp3': 'Celtic Harp',
|
|
4017
|
-
'agent_vibes_hawaiian_slack_key_guitar_v2_loop.mp3': 'Hawaiian Slack Key Guitar',
|
|
4018
|
-
'agent_vibes_arabic_v2_loop.mp3': 'Arabic Oud',
|
|
4019
|
-
'agent_vibes_ganawa_ambient_v2_loop.mp3': 'Gnawa Ambient',
|
|
4020
|
-
'agent_vibes_tabla_dream_pop_v1_loop.mp3': 'Tabla Dream Pop'
|
|
4021
|
-
};
|
|
4022
|
-
const trackName = trackChoices[userConfig.backgroundMusic.track] || userConfig.backgroundMusic.track;
|
|
4023
|
-
configContent += chalk.gray(` Default track: ${trackName}\n`);
|
|
4024
|
-
}
|
|
4025
|
-
configContent += '\n';
|
|
4026
|
-
configContent += chalk.cyan('š Personality:\n');
|
|
4027
|
-
const personalityDisplay = userConfig.personality === 'none' ? 'None (Professional)' : userConfig.personality.charAt(0).toUpperCase() + userConfig.personality.slice(1);
|
|
4028
|
-
configContent += chalk.white(` ${personalityDisplay}\n`);
|
|
4029
|
-
configContent += '\n';
|
|
4030
|
-
configContent += chalk.cyan('š Verbosity:\n');
|
|
4031
|
-
configContent += chalk.white(` ${verbosityLabels[userConfig.verbosity]}\n`);
|
|
4032
|
-
|
|
4033
|
-
const configBoxen = boxen(configContent.trim(), {
|
|
4034
|
-
padding: 1,
|
|
4035
|
-
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
4036
|
-
borderStyle: 'round',
|
|
4037
|
-
borderColor: 'green',
|
|
4038
|
-
width: 80
|
|
4039
|
-
});
|
|
4040
|
-
|
|
4041
|
-
// Don't add Configuration Summary to preInstallPages yet - we'll handle it specially
|
|
4042
|
-
|
|
4043
|
-
// Show pre-install pages up to (but NOT including) Configuration Summary
|
|
4044
|
-
if (!options.yes && preInstallPages.length > 0) {
|
|
4045
|
-
console.log(chalk.cyan('\nš Installation Preview\n'));
|
|
4046
|
-
const preInstallOffset = 4; // After 4 config pages (System Dependencies + Provider + Audio + Verbosity)
|
|
4047
|
-
// For pre-install, estimate post-install pages (will be exact in post-install)
|
|
4048
|
-
const estimatedPostInstall = 7; // Typical: 5 summaries + 1 BMAD/recommendation + 1 complete
|
|
4049
|
-
const estimatedTotal = configPages + preInstallPages.length + 1 + estimatedPostInstall; // +1 for Config Summary
|
|
4050
|
-
|
|
4051
|
-
const result = await showPaginatedContent(preInstallPages, {
|
|
4052
|
-
...options,
|
|
4053
|
-
continueLabel: 'ā Next',
|
|
4054
|
-
pageOffset: preInstallOffset,
|
|
4055
|
-
totalPages: estimatedTotal,
|
|
4056
|
-
showPreviousOnFirst: true
|
|
4057
|
-
});
|
|
4058
|
-
|
|
4059
|
-
// If user went back from first pre-install page, restart configuration
|
|
4060
|
-
if (result === 'prev') {
|
|
4061
|
-
console.log(chalk.yellow('\nā©ļø Returning to configuration...\n'));
|
|
4062
|
-
return install(options); // Restart the install function
|
|
4063
|
-
}
|
|
4064
|
-
}
|
|
4065
|
-
|
|
4066
|
-
// Handle Configuration Summary page specially with welcome message prompt
|
|
4067
|
-
if (!options.yes) {
|
|
4068
|
-
// Show Configuration Summary page
|
|
4069
|
-
console.clear();
|
|
4070
|
-
const currentPageNum = 4 + preInstallPages.length; // After config pages + pre-install pages
|
|
4071
|
-
const estimatedPostInstall = 7;
|
|
4072
|
-
const estimatedTotal = configPages + preInstallPages.length + 1 + estimatedPostInstall;
|
|
4073
|
-
const { header } = createPageHeaderFooter('Configuration Summary', currentPageNum, estimatedTotal, 0);
|
|
4074
|
-
|
|
4075
|
-
console.log(header);
|
|
4076
|
-
console.log(configBoxen);
|
|
4077
|
-
console.log('');
|
|
4078
|
-
// Don't show welcome message text for termux-ssh (it won't work)
|
|
4079
|
-
if (userConfig.provider !== 'termux-ssh') {
|
|
4080
|
-
console.log(chalk.gray('Play audio welcome message from Paul, creator of AgentVibes.\n'));
|
|
4081
|
-
}
|
|
4082
|
-
}
|
|
4083
|
-
|
|
4084
|
-
// Ask welcome message question BEFORE showing navigation
|
|
4085
|
-
// Skip for termux-ssh - welcome audio plays locally, not on Android device
|
|
4086
|
-
if (!options.yes && userConfig.provider !== 'termux-ssh') {
|
|
4087
|
-
// Ask if user wants to hear welcome message
|
|
4088
|
-
const { playWelcome } = await inquirer.prompt([
|
|
4089
|
-
{
|
|
4090
|
-
type: 'confirm',
|
|
4091
|
-
name: 'playWelcome',
|
|
4092
|
-
message: chalk.yellow('šµ Listen to Welcome Message?'),
|
|
4093
|
-
default: false,
|
|
4094
|
-
},
|
|
4095
|
-
]);
|
|
4096
|
-
|
|
4097
|
-
if (playWelcome) {
|
|
4098
|
-
console.log(''); // Spacing before spinner
|
|
4099
|
-
const spinner = ora('Playing welcome message...').start();
|
|
4100
|
-
await playWelcomeDemo(targetDir, spinner, options);
|
|
4101
|
-
spinner.succeed(chalk.green('Welcome message complete!'));
|
|
4102
|
-
console.log(''); // Spacing after completion
|
|
4103
|
-
}
|
|
4104
|
-
} else if (!options.yes && userConfig.provider === 'termux-ssh' || userConfig.provider === 'ssh-pulseaudio') {
|
|
4105
|
-
console.log(chalk.yellow('ā Welcome message skipped (not available for Termux SSH)\n'));
|
|
4106
|
-
}
|
|
4107
|
-
|
|
4108
|
-
// Now show navigation menu (Continue to installation)
|
|
4882
|
+
// Confirm and start installation
|
|
4109
4883
|
const { startInstall } = await inquirer.prompt([
|
|
4110
4884
|
{
|
|
4111
4885
|
type: 'confirm',
|
|
@@ -4120,59 +4894,36 @@ async function install(options = {}) {
|
|
|
4120
4894
|
process.exit(0);
|
|
4121
4895
|
}
|
|
4122
4896
|
|
|
4123
|
-
//
|
|
4897
|
+
// Silent spinner for copy functions ā suppresses per-file output
|
|
4898
|
+
const silentSpinner = createSilentSpinner();
|
|
4899
|
+
|
|
4124
4900
|
console.log('');
|
|
4125
|
-
const spinner = ora('
|
|
4901
|
+
const spinner = ora('Installing AgentVibes...').start();
|
|
4126
4902
|
|
|
4127
4903
|
try {
|
|
4128
4904
|
// Create .claude directory structure
|
|
4129
4905
|
const claudeDir = path.join(targetDir, '.claude');
|
|
4130
4906
|
const commandsDir = path.join(claudeDir, 'commands');
|
|
4131
4907
|
const hooksDir = path.join(claudeDir, isNativeWindows() ? 'hooks-windows' : 'hooks');
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
await fs.mkdir(tracksDir, { recursive: true });
|
|
4150
|
-
console.log(chalk.green(' ā Directories created!\n'));
|
|
4151
|
-
} else {
|
|
4152
|
-
spinner.succeed(chalk.green('.claude directory found!'));
|
|
4153
|
-
console.log(chalk.gray(` Location: ${claudeDir}\n`));
|
|
4154
|
-
|
|
4155
|
-
// Ensure audio/tracks directory exists even if .claude already exists
|
|
4156
|
-
const audioDir = path.join(claudeDir, 'audio');
|
|
4157
|
-
const tracksDir = path.join(audioDir, 'tracks');
|
|
4158
|
-
await fs.mkdir(tracksDir, { recursive: true });
|
|
4159
|
-
}
|
|
4160
|
-
|
|
4161
|
-
// Copy all files using helper functions
|
|
4162
|
-
const commandResult = await copyCommandFiles(targetDir, spinner);
|
|
4163
|
-
const hookResult = await copyHookFiles(targetDir, spinner);
|
|
4164
|
-
const personalityResult = await copyPersonalityFiles(targetDir, spinner);
|
|
4165
|
-
const pluginFileCount = await copyPluginFiles(targetDir, spinner);
|
|
4166
|
-
const bmadConfigFileCount = await copyBmadConfigFiles(targetDir, spinner);
|
|
4167
|
-
const backgroundMusicResult = await copyBackgroundMusicFiles(targetDir, spinner);
|
|
4168
|
-
const configFileCount = await copyConfigFiles(targetDir, spinner);
|
|
4169
|
-
|
|
4170
|
-
// Configure hooks and manifests
|
|
4171
|
-
await configureSessionStartHook(targetDir, spinner);
|
|
4172
|
-
await installPluginManifest(targetDir, spinner);
|
|
4908
|
+
const audioDir = path.join(claudeDir, 'audio');
|
|
4909
|
+
const tracksDir = path.join(audioDir, 'tracks');
|
|
4910
|
+
await fs.mkdir(commandsDir, { recursive: true });
|
|
4911
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
4912
|
+
await fs.mkdir(tracksDir, { recursive: true });
|
|
4913
|
+
|
|
4914
|
+
// Copy all files silently
|
|
4915
|
+
await copyCommandFiles(targetDir, silentSpinner);
|
|
4916
|
+
await copyHookFiles(targetDir, silentSpinner);
|
|
4917
|
+
await copyPersonalityFiles(targetDir, silentSpinner);
|
|
4918
|
+
await copyPluginFiles(targetDir, silentSpinner);
|
|
4919
|
+
await copyBmadConfigFiles(targetDir, silentSpinner);
|
|
4920
|
+
await copyBackgroundMusicFiles(targetDir, silentSpinner);
|
|
4921
|
+
await copyConfigFiles(targetDir, silentSpinner);
|
|
4922
|
+
await configureSessionStartHook(targetDir, silentSpinner);
|
|
4923
|
+
await installPluginManifest(targetDir, silentSpinner);
|
|
4924
|
+
await ensureGitRepo(targetDir, silentSpinner);
|
|
4173
4925
|
|
|
4174
4926
|
// Save provider configuration
|
|
4175
|
-
spinner.start('Saving provider configuration...');
|
|
4176
4927
|
const providerConfigPath = path.join(claudeDir, 'tts-provider.txt');
|
|
4177
4928
|
await fs.writeFile(providerConfigPath, selectedProvider);
|
|
4178
4929
|
|
|
@@ -4188,29 +4939,20 @@ async function install(options = {}) {
|
|
|
4188
4939
|
|
|
4189
4940
|
// Set up receiver script if in receiver mode (Termux)
|
|
4190
4941
|
if (userConfig.isReceiver) {
|
|
4191
|
-
spinner.start('Setting up receiver script...');
|
|
4192
|
-
|
|
4193
4942
|
const receiverDir = path.join(process.env.HOME || process.env.USERPROFILE, '.agentvibes');
|
|
4194
4943
|
await fs.mkdir(receiverDir, { recursive: true, mode: 0o700 });
|
|
4195
|
-
|
|
4196
4944
|
const receiverScriptPath = path.join(receiverDir, 'receiver.sh');
|
|
4197
4945
|
const templatePath = path.join(__dirname, '..', 'templates', 'agentvibes-receiver.sh');
|
|
4198
|
-
|
|
4199
4946
|
try {
|
|
4200
4947
|
const templateContent = await fs.readFile(templatePath, 'utf8');
|
|
4201
4948
|
await fs.writeFile(receiverScriptPath, templateContent, { mode: 0o755 });
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
} catch (error) {
|
|
4205
|
-
spinner.warn(chalk.yellow('Could not install receiver script'));
|
|
4206
|
-
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
4949
|
+
} catch {
|
|
4950
|
+
// Receiver script install failed ā non-fatal
|
|
4207
4951
|
}
|
|
4208
4952
|
}
|
|
4209
4953
|
|
|
4210
|
-
// Save setup guide for SSH-remote installations
|
|
4954
|
+
// Save setup guide for SSH-remote installations (file only, no terminal output)
|
|
4211
4955
|
if (selectedProvider === 'termux-ssh' || selectedProvider === 'ssh-pulseaudio') {
|
|
4212
|
-
spinner.start('Saving setup guide...');
|
|
4213
|
-
|
|
4214
4956
|
const agentvibesDir = path.join(process.env.HOME || process.env.USERPROFILE, '.agentvibes');
|
|
4215
4957
|
await fs.mkdir(agentvibesDir, { recursive: true, mode: 0o700 });
|
|
4216
4958
|
|
|
@@ -4271,397 +5013,100 @@ Troubleshooting:
|
|
|
4271
5013
|
- Verify AgentVibes on receiver: ssh ${userConfig.sshHost || 'phone'} which agentvibes
|
|
4272
5014
|
- Test receiver script: ssh ${userConfig.sshHost || 'phone'} ~/.agentvibes/receiver.sh "Test"
|
|
4273
5015
|
`;
|
|
4274
|
-
|
|
4275
5016
|
try {
|
|
4276
5017
|
await fs.writeFile(setupGuidePath, setupGuideContent);
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
} catch (error) {
|
|
4280
|
-
spinner.warn(chalk.yellow('Could not save setup guide'));
|
|
4281
|
-
console.log(chalk.gray(` Error: ${error.message}\n`));
|
|
5018
|
+
} catch {
|
|
5019
|
+
// Setup guide write failed ā non-fatal
|
|
4282
5020
|
}
|
|
4283
5021
|
}
|
|
4284
5022
|
|
|
4285
|
-
// Set default voice
|
|
5023
|
+
// Set default voice
|
|
4286
5024
|
const voiceConfigPath = path.join(claudeDir, 'tts-voice.txt');
|
|
4287
5025
|
let defaultVoice = userConfig.defaultVoice;
|
|
4288
|
-
|
|
4289
|
-
// Fallback to defaults if voice wasn't selected
|
|
4290
5026
|
if (!defaultVoice) {
|
|
4291
5027
|
switch (selectedProvider) {
|
|
4292
|
-
case 'piper':
|
|
4293
|
-
defaultVoice = '
|
|
4294
|
-
|
|
4295
|
-
case '
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
defaultVoice = 'en_US-ryan-high';
|
|
4300
|
-
break;
|
|
4301
|
-
case 'windows-sapi':
|
|
4302
|
-
defaultVoice = 'Microsoft David Desktop';
|
|
4303
|
-
break;
|
|
4304
|
-
case 'soprano':
|
|
4305
|
-
defaultVoice = 'soprano-default';
|
|
4306
|
-
break;
|
|
4307
|
-
case 'termux-ssh':
|
|
4308
|
-
// Android TTS voices are managed in Android settings, not here
|
|
4309
|
-
defaultVoice = 'android-system-default';
|
|
4310
|
-
break;
|
|
4311
|
-
default:
|
|
4312
|
-
defaultVoice = 'Samantha';
|
|
4313
|
-
break;
|
|
5028
|
+
case 'piper': defaultVoice = 'en_US-ryan-high'; break;
|
|
5029
|
+
case 'macos': defaultVoice = 'Samantha'; break;
|
|
5030
|
+
case 'windows-piper': defaultVoice = 'en_US-ryan-high'; break;
|
|
5031
|
+
case 'windows-sapi': defaultVoice = 'Microsoft David Desktop'; break;
|
|
5032
|
+
case 'soprano': defaultVoice = 'soprano-default'; break;
|
|
5033
|
+
case 'termux-ssh': defaultVoice = 'android-system-default'; break;
|
|
5034
|
+
default: defaultVoice = 'Samantha'; break;
|
|
4314
5035
|
}
|
|
4315
5036
|
}
|
|
4316
5037
|
await fs.writeFile(voiceConfigPath, defaultVoice);
|
|
4317
5038
|
|
|
4318
|
-
spinner.succeed();
|
|
4319
|
-
|
|
4320
5039
|
// Detect and migrate old configuration
|
|
4321
|
-
await detectAndMigrateOldConfig(targetDir,
|
|
4322
|
-
|
|
4323
|
-
// Snapshot existing Piper voices BEFORE installation (for proper summary display)
|
|
4324
|
-
let preExistingVoices = [];
|
|
4325
|
-
if (selectedProvider === 'piper') {
|
|
4326
|
-
const piperVoicesDir = path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'piper-voices');
|
|
4327
|
-
try {
|
|
4328
|
-
if (fsSync.existsSync(piperVoicesDir)) {
|
|
4329
|
-
const files = fsSync.readdirSync(piperVoicesDir);
|
|
4330
|
-
preExistingVoices = files
|
|
4331
|
-
.filter(f => f.endsWith('.onnx'))
|
|
4332
|
-
.map(f => f.replace('.onnx', ''));
|
|
4333
|
-
}
|
|
4334
|
-
} catch {
|
|
4335
|
-
// Ignore errors
|
|
4336
|
-
}
|
|
4337
|
-
}
|
|
5040
|
+
await detectAndMigrateOldConfig(targetDir, silentSpinner);
|
|
4338
5041
|
|
|
4339
5042
|
// Auto-install Piper if selected
|
|
4340
5043
|
if (selectedProvider === 'piper') {
|
|
5044
|
+
spinner.text = 'Installing Piper TTS...';
|
|
4341
5045
|
await checkAndInstallPiper(targetDir, options);
|
|
4342
5046
|
} else if (selectedProvider === 'windows-piper') {
|
|
5047
|
+
spinner.text = 'Installing Piper TTS for Windows...';
|
|
4343
5048
|
await checkAndInstallPiperWindows(targetDir, options);
|
|
4344
|
-
} else if (selectedProvider === 'soprano') {
|
|
4345
|
-
console.log(chalk.magenta('ā” Soprano TTS selected'));
|
|
4346
|
-
console.log(chalk.gray(' Install: pip install soprano-tts'));
|
|
4347
|
-
console.log(chalk.gray(' GPU: pip install soprano-tts[lmdeploy]'));
|
|
4348
|
-
console.log(chalk.gray(' Start: soprano-webui\n'));
|
|
4349
|
-
} else if (selectedProvider === 'windows-sapi') {
|
|
4350
|
-
console.log(chalk.green('ā Windows SAPI provider selected - no additional setup needed\n'));
|
|
4351
5049
|
}
|
|
4352
5050
|
|
|
4353
|
-
// Apply background music configuration
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
// Write enabled flag
|
|
4360
|
-
const enabledFile = path.join(configDir, 'background-music-enabled.txt');
|
|
4361
|
-
await fs.writeFile(enabledFile, 'true');
|
|
4362
|
-
|
|
4363
|
-
// Update audio-effects.cfg with selected track
|
|
5051
|
+
// Apply background music configuration
|
|
5052
|
+
const configDir = path.join(claudeDir, 'config');
|
|
5053
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
5054
|
+
if (userConfig.backgroundMusic?.enabled) {
|
|
5055
|
+
await fs.writeFile(path.join(configDir, 'background-music-enabled.txt'), 'true');
|
|
5056
|
+
try {
|
|
4364
5057
|
const audioEffectsPath = path.join(configDir, 'audio-effects.cfg');
|
|
4365
5058
|
let audioEffectsContent = await fs.readFile(audioEffectsPath, 'utf-8');
|
|
4366
|
-
|
|
4367
|
-
// Update the default entry with selected track
|
|
4368
5059
|
audioEffectsContent = audioEffectsContent.replace(
|
|
4369
5060
|
/^default\|([^|]*)\|([^|]*)\|(.*)$/m,
|
|
4370
5061
|
`default|$1|${userConfig.backgroundMusic.track}|$3`
|
|
4371
5062
|
);
|
|
4372
|
-
|
|
4373
5063
|
await fs.writeFile(audioEffectsPath, audioEffectsContent);
|
|
5064
|
+
} catch {
|
|
5065
|
+
// Audio effects config not yet available ā non-fatal
|
|
4374
5066
|
}
|
|
4375
5067
|
}
|
|
4376
5068
|
|
|
4377
|
-
// Apply
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
// Apply verbosity configuration from userConfig
|
|
4381
|
-
const verbosityFile = path.join(claudeDir, 'tts-verbosity.txt');
|
|
4382
|
-
await fs.writeFile(verbosityFile, userConfig.verbosity);
|
|
4383
|
-
|
|
4384
|
-
// Apply personality configuration from userConfig
|
|
5069
|
+
// Apply verbosity, personality, pretext
|
|
5070
|
+
await fs.writeFile(path.join(claudeDir, 'tts-verbosity.txt'), userConfig.verbosity);
|
|
4385
5071
|
if (userConfig.personality && userConfig.personality !== 'none') {
|
|
4386
|
-
|
|
4387
|
-
await fs.writeFile(personalityFile, userConfig.personality);
|
|
4388
|
-
}
|
|
4389
|
-
|
|
4390
|
-
// Initialize piperVoicesBoxen outside the conditional for proper scoping
|
|
4391
|
-
let piperVoicesBoxen = null;
|
|
4392
|
-
|
|
4393
|
-
if (selectedProvider === 'macos') {
|
|
4394
|
-
// macOS Say provider summary
|
|
4395
|
-
console.log(chalk.white(` ⢠Using macOS built-in Say command`));
|
|
4396
|
-
console.log(chalk.white(` ⢠System voices available (Samantha, Alex, etc.)`));
|
|
4397
|
-
console.log(chalk.green(` ⢠No API key needed ā`));
|
|
4398
|
-
console.log(chalk.green(` ⢠Zero setup required ā`));
|
|
4399
|
-
} else {
|
|
4400
|
-
// Check for installed Piper voices
|
|
4401
|
-
const piperVoicesDir = path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'piper-voices');
|
|
4402
|
-
let installedVoices = [];
|
|
4403
|
-
let missingVoices = [];
|
|
4404
|
-
|
|
4405
|
-
const commonVoices = [
|
|
4406
|
-
'en_US-lessac-medium',
|
|
4407
|
-
'en_US-amy-medium',
|
|
4408
|
-
'en_US-joe-medium',
|
|
4409
|
-
'en_US-ryan-high',
|
|
4410
|
-
'en_US-libritts-high',
|
|
4411
|
-
'16Speakers'
|
|
4412
|
-
];
|
|
4413
|
-
|
|
4414
|
-
try {
|
|
4415
|
-
if (fsSync.existsSync(piperVoicesDir)) {
|
|
4416
|
-
const files = fsSync.readdirSync(piperVoicesDir);
|
|
4417
|
-
installedVoices = files
|
|
4418
|
-
.filter(f => f.endsWith('.onnx'))
|
|
4419
|
-
.map(f => {
|
|
4420
|
-
const voiceName = f.replace('.onnx', '');
|
|
4421
|
-
const voicePath = path.join(piperVoicesDir, f);
|
|
4422
|
-
try {
|
|
4423
|
-
const stats = fsSync.statSync(voicePath);
|
|
4424
|
-
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
4425
|
-
return { name: voiceName, path: voicePath, size: `${sizeMB}M` };
|
|
4426
|
-
} catch (statErr) {
|
|
4427
|
-
// Skip files that can't be read (broken symlinks, etc)
|
|
4428
|
-
return null;
|
|
4429
|
-
}
|
|
4430
|
-
})
|
|
4431
|
-
.filter(v => v !== null);
|
|
4432
|
-
|
|
4433
|
-
// Check which common voices are missing
|
|
4434
|
-
for (const voice of commonVoices) {
|
|
4435
|
-
if (!installedVoices.some(v => v.name === voice)) {
|
|
4436
|
-
missingVoices.push(voice);
|
|
4437
|
-
}
|
|
4438
|
-
}
|
|
4439
|
-
} else {
|
|
4440
|
-
missingVoices = commonVoices;
|
|
4441
|
-
}
|
|
4442
|
-
} catch (err) {
|
|
4443
|
-
// On error, show default message
|
|
4444
|
-
installedVoices = [];
|
|
4445
|
-
missingVoices = commonVoices;
|
|
4446
|
-
}
|
|
4447
|
-
|
|
4448
|
-
// Create Piper voices boxen (only if newly installed)
|
|
4449
|
-
if (installedVoices.length > 0) {
|
|
4450
|
-
// Separate newly installed from pre-existing voices
|
|
4451
|
-
const newlyInstalled = installedVoices.filter(v => !preExistingVoices.includes(v.name));
|
|
4452
|
-
const alreadyInstalled = installedVoices.filter(v => preExistingVoices.includes(v.name));
|
|
4453
|
-
|
|
4454
|
-
// Only create boxen if there are newly installed voices
|
|
4455
|
-
if (newlyInstalled.length > 0) {
|
|
4456
|
-
let content = chalk.bold.green(`${newlyInstalled.length} Newly Installed\n\n`);
|
|
4457
|
-
newlyInstalled.forEach(voice => {
|
|
4458
|
-
content += chalk.green(`ā ${voice.name}`) + chalk.gray(` (${voice.size})\n`);
|
|
4459
|
-
content += chalk.dim(` ${voice.path}\n`);
|
|
4460
|
-
});
|
|
4461
|
-
|
|
4462
|
-
if (alreadyInstalled.length > 0) {
|
|
4463
|
-
content += '\n' + chalk.gray('ā'.repeat(60)) + '\n\n';
|
|
4464
|
-
content += chalk.bold.cyan(`${alreadyInstalled.length} Already Installed\n\n`);
|
|
4465
|
-
alreadyInstalled.forEach(voice => {
|
|
4466
|
-
content += chalk.cyan(`ā ${voice.name}`) + chalk.gray(` (${voice.size})\n`);
|
|
4467
|
-
content += chalk.dim(` ${voice.path}\n`);
|
|
4468
|
-
});
|
|
4469
|
-
}
|
|
4470
|
-
|
|
4471
|
-
// Add additional info at the bottom of boxen
|
|
4472
|
-
content += '\n' + chalk.gray('ā'.repeat(60)) + '\n\n';
|
|
4473
|
-
content += chalk.white('⢠18 languages supported\n');
|
|
4474
|
-
content += chalk.green('⢠No API key needed ā');
|
|
4475
|
-
|
|
4476
|
-
piperVoicesBoxen = boxen(content.trim(), {
|
|
4477
|
-
padding: 1,
|
|
4478
|
-
margin: 1,
|
|
4479
|
-
borderStyle: 'round',
|
|
4480
|
-
borderColor: 'cyan',
|
|
4481
|
-
title: chalk.bold(`š¤ Piper Voices (${installedVoices.length} total)`),
|
|
4482
|
-
titleAlignment: 'center'
|
|
4483
|
-
});
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
|
|
4487
|
-
if (missingVoices.length > 0) {
|
|
4488
|
-
console.log(chalk.yellow(` ⢠${missingVoices.length} voices to download:`));
|
|
4489
|
-
missingVoices.forEach(voice => {
|
|
4490
|
-
console.log(chalk.gray(` ā ${voice}`));
|
|
4491
|
-
});
|
|
4492
|
-
}
|
|
4493
|
-
|
|
4494
|
-
if (installedVoices.length === 0 && missingVoices.length === 0) {
|
|
4495
|
-
console.log(chalk.white(` ⢠50+ Piper neural voices available (free!)`));
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4498
|
-
console.log('');
|
|
4499
|
-
|
|
4500
|
-
// Collect all boxens for pagination
|
|
4501
|
-
const pages = [];
|
|
4502
|
-
if (commandResult.boxen) {
|
|
4503
|
-
pages.push({ title: 'Summary: Slash Commands', content: commandResult.boxen });
|
|
4504
|
-
}
|
|
4505
|
-
if (backgroundMusicResult.boxen) {
|
|
4506
|
-
pages.push({ title: 'Summary: Background Music', content: backgroundMusicResult.boxen });
|
|
4507
|
-
}
|
|
4508
|
-
if (hookResult.boxen) {
|
|
4509
|
-
pages.push({ title: 'Summary: TTS Scripts', content: hookResult.boxen });
|
|
4510
|
-
}
|
|
4511
|
-
if (personalityResult.boxen) {
|
|
4512
|
-
pages.push({ title: 'Summary: Personalities', content: personalityResult.boxen });
|
|
5072
|
+
await fs.writeFile(path.join(claudeDir, 'tts-personality.txt'), userConfig.personality);
|
|
4513
5073
|
}
|
|
4514
|
-
if (
|
|
4515
|
-
|
|
4516
|
-
}
|
|
4517
|
-
|
|
4518
|
-
// Recent Changes already shown on page 2 after welcome banner - no need to show again
|
|
4519
|
-
|
|
4520
|
-
// Handle MCP configuration - offer to create .mcp.json if not exists
|
|
4521
|
-
await handleMcpConfiguration(targetDir, options);
|
|
4522
|
-
|
|
4523
|
-
// Create default BMAD voice assignments (works even if BMAD not installed yet)
|
|
4524
|
-
await createDefaultBmadVoiceAssignmentsProactive(targetDir);
|
|
4525
|
-
|
|
4526
|
-
// Handle BMAD integration
|
|
4527
|
-
const bmadDetection = await handleBmadIntegration(targetDir, options);
|
|
4528
|
-
const bmadDetected = bmadDetection.installed;
|
|
4529
|
-
|
|
4530
|
-
if (bmadDetected) {
|
|
4531
|
-
const versionLabel = bmadDetection.version === 6
|
|
4532
|
-
? `v${bmadDetection.detailedVersion}`
|
|
4533
|
-
: 'v4';
|
|
4534
|
-
|
|
4535
|
-
const bmadContent =
|
|
4536
|
-
chalk.green.bold(`š BMAD-METHOD⢠${versionLabel} Detected!\n\n`) +
|
|
4537
|
-
chalk.white.bold('We detected you ALREADY have the BMAD-METHODā¢\n') +
|
|
4538
|
-
chalk.white.bold('The Universal AI Agent Framework installed!\n\n') +
|
|
4539
|
-
chalk.cyan('⨠Try the Party Mode command:\n') +
|
|
4540
|
-
chalk.yellow.bold(' /bmad:core:workflows:party-mode\n\n') +
|
|
4541
|
-
chalk.gray('AgentVibes will assign a unique voice to each agent\n') +
|
|
4542
|
-
chalk.gray('while they help you with your project!\n\n') +
|
|
4543
|
-
chalk.cyan('Other Commands:\n') +
|
|
4544
|
-
chalk.gray(' ⢠/agent-vibes:bmad status - View voice assignments\n') +
|
|
4545
|
-
chalk.gray(' ⢠/agent-vibes:bmad set <agent> <voice> - Customize voices');
|
|
4546
|
-
|
|
4547
|
-
pages.push({ title: 'BMAD Integration', content: bmadContent });
|
|
5074
|
+
if (userConfig.pretext && userConfig.pretext.trim()) {
|
|
5075
|
+
await fs.writeFile(path.join(configDir, 'tts-pretext.txt'), userConfig.pretext, { mode: 0o600 });
|
|
4548
5076
|
} else {
|
|
4549
|
-
|
|
4550
|
-
chalk.cyan.bold('š” We also Recommend:\n\n') +
|
|
4551
|
-
chalk.white.bold('BMAD-METHODā¢: Universal AI Agent Framework\n\n') +
|
|
4552
|
-
chalk.gray('AgentVibes auto-detects BMAD and assigns voices to agents!\n\n') +
|
|
4553
|
-
chalk.cyan('https://github.com/bmad-code-org/BMAD-METHOD'),
|
|
4554
|
-
{
|
|
4555
|
-
padding: 1,
|
|
4556
|
-
margin: 1,
|
|
4557
|
-
borderStyle: 'round',
|
|
4558
|
-
borderColor: 'cyan',
|
|
4559
|
-
}
|
|
4560
|
-
);
|
|
4561
|
-
|
|
4562
|
-
pages.push({ title: 'Recommended Tools', content: bmadRecommendBoxen });
|
|
5077
|
+
try { await fs.unlink(path.join(configDir, 'tts-pretext.txt')); } catch { /* ok */ }
|
|
4563
5078
|
}
|
|
4564
5079
|
|
|
4565
|
-
// Apply reverb setting
|
|
5080
|
+
// Apply reverb setting
|
|
5081
|
+
const selectedReverb = userConfig.reverb;
|
|
4566
5082
|
if (selectedReverb && selectedReverb !== 'off') {
|
|
4567
5083
|
const effectsManagerPath = path.join(targetDir, '.claude', 'hooks', 'effects-manager.sh');
|
|
4568
|
-
// Validate reverb value to prevent command injection
|
|
4569
5084
|
const validReverb = ['light', 'medium', 'heavy', 'cathedral'];
|
|
4570
5085
|
if (validReverb.includes(selectedReverb)) {
|
|
4571
5086
|
try {
|
|
4572
|
-
execFileSync('bash', [effectsManagerPath, 'set-reverb', selectedReverb, 'default'], {
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
} catch (error) {
|
|
4576
|
-
// Silent fail - will be shown in success message if needed
|
|
5087
|
+
execFileSync('bash', [effectsManagerPath, 'set-reverb', selectedReverb, 'default'], { stdio: 'pipe' });
|
|
5088
|
+
} catch {
|
|
5089
|
+
// Reverb setting failed ā non-fatal
|
|
4577
5090
|
}
|
|
4578
5091
|
}
|
|
4579
5092
|
}
|
|
4580
5093
|
|
|
4581
|
-
//
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
if (userConfig.isReceiver) {
|
|
4586
|
-
successContent +=
|
|
4587
|
-
chalk.green('ā Receiver mode configured!\n') +
|
|
4588
|
-
chalk.green('ā Receiver script: ~/.agentvibes/receiver.sh\n') +
|
|
4589
|
-
chalk.green('ā Provider: piper (local playback)\n\n') +
|
|
4590
|
-
chalk.cyan('š Next: On your server, set this device as target:\n') +
|
|
4591
|
-
chalk.white(' echo "phone" > ~/.claude/ssh-remote-host.txt\n\n') +
|
|
4592
|
-
chalk.gray('(Use your SSH hostname or Tailscale name)\n\n');
|
|
4593
|
-
}
|
|
4594
|
-
// SSH-Remote success message (Voiceless server)
|
|
4595
|
-
else if (selectedProvider === 'termux-ssh' || selectedProvider === 'ssh-pulseaudio') {
|
|
4596
|
-
successContent +=
|
|
4597
|
-
chalk.green('ā Provider configured: ssh-remote\n\n') +
|
|
4598
|
-
chalk.cyan('š Setup Instructions Saved:\n') +
|
|
4599
|
-
chalk.white(' ~/.agentvibes/setup-guide.txt\n\n') +
|
|
4600
|
-
chalk.cyan('Quick Start:\n') +
|
|
4601
|
-
chalk.white('1. Install AgentVibes on target device\n') +
|
|
4602
|
-
chalk.white('2. Configure SSH/Tailscale access\n') +
|
|
4603
|
-
chalk.white('3. Test: agentvibes tts "Hello!"\n\n');
|
|
4604
|
-
}
|
|
4605
|
-
// Standard installation success message
|
|
4606
|
-
else {
|
|
4607
|
-
successContent +=
|
|
4608
|
-
chalk.green('ā
AgentVibes TTS is now active via SessionStart hook!\n') +
|
|
4609
|
-
chalk.gray(' (No additional setup needed - TTS protocol auto-loads on every session)\n\n');
|
|
4610
|
-
}
|
|
4611
|
-
|
|
4612
|
-
// Common commands section for all installations
|
|
4613
|
-
successContent +=
|
|
4614
|
-
chalk.white('š¤ Available Commands:\n\n') +
|
|
4615
|
-
chalk.cyan(' /agent-vibes') + chalk.gray(' .................... Show all commands\n') +
|
|
4616
|
-
chalk.cyan(' /agent-vibes:list') + chalk.gray(' ............... List available voices\n') +
|
|
4617
|
-
chalk.cyan(' /agent-vibes:preview') + chalk.gray(' ............ Preview voice samples\n') +
|
|
4618
|
-
chalk.cyan(' /agent-vibes:switch <name>') + chalk.gray(' ...... Change active voice\n') +
|
|
4619
|
-
chalk.cyan(' /agent-vibes:replay [N]') + chalk.gray(' ......... Replay last audio\n') +
|
|
4620
|
-
chalk.cyan(' /agent-vibes:whoami') + chalk.gray(' .............. Show current voice\n\n');
|
|
4621
|
-
|
|
4622
|
-
if (!userConfig.isReceiver) {
|
|
4623
|
-
successContent += chalk.yellow('šµ Try: ') + chalk.cyan('/agent-vibes:preview') + chalk.yellow(' to hear the voices!\n\n');
|
|
4624
|
-
}
|
|
4625
|
-
|
|
4626
|
-
successContent +=
|
|
4627
|
-
chalk.gray('š¦ Repo: ') + chalk.cyan('https://github.com/paulpreibisch/AgentVibes\n') +
|
|
4628
|
-
chalk.gray('š Docs: ') + chalk.cyan('https://github.com/paulpreibisch/AgentVibes/blob/master/README.md');
|
|
4629
|
-
|
|
4630
|
-
pages.push({ title: 'Installation Complete', content: successContent });
|
|
4631
|
-
|
|
4632
|
-
// Show all pages with pagination navigation
|
|
4633
|
-
const postInstallOffset = configPages + preInstallPages.length; // After config + pre-install pages
|
|
4634
|
-
const actualTotalPages = configPages + preInstallPages.length + pages.length;
|
|
4635
|
-
|
|
4636
|
-
await showPaginatedContent(pages, {
|
|
4637
|
-
...options,
|
|
4638
|
-
continueLabel: 'ā Installation Complete',
|
|
4639
|
-
pageOffset: postInstallOffset,
|
|
4640
|
-
totalPages: actualTotalPages
|
|
4641
|
-
});
|
|
4642
|
-
|
|
4643
|
-
// Final message after pagination
|
|
4644
|
-
console.log(chalk.green.bold('\nā
AgentVibes is Ready!\n'));
|
|
4645
|
-
|
|
4646
|
-
// Check for .mcp.json file
|
|
4647
|
-
const mcpConfigPath = path.join(targetDir, '.mcp.json');
|
|
4648
|
-
const hasMcpConfig = fsSync.existsSync(mcpConfigPath);
|
|
4649
|
-
|
|
4650
|
-
if (hasMcpConfig) {
|
|
4651
|
-
console.log(chalk.white(' Launch Claude Code with MCP:'));
|
|
4652
|
-
console.log(chalk.cyan(' claude --mcp-config .mcp.json\n'));
|
|
4653
|
-
} else {
|
|
4654
|
-
console.log(chalk.white(' Start a new session to activate TTS.\n'));
|
|
4655
|
-
}
|
|
5094
|
+
// MCP configuration and BMAD voice assignments (silent)
|
|
5095
|
+
await handleMcpConfiguration(targetDir, { ...options, yes: true });
|
|
5096
|
+
await createDefaultBmadVoiceAssignmentsProactive(targetDir);
|
|
5097
|
+
await handleBmadIntegration(targetDir, { ...options, yes: true });
|
|
4656
5098
|
|
|
4657
|
-
|
|
4658
|
-
console.log(chalk.white(' ⢠/agent-vibes:switch <name>') + chalk.gray(' - Change your voice'));
|
|
4659
|
-
console.log(chalk.white(' ⢠/agent-vibes:personality <style>') + chalk.gray(' - Set personality\n'));
|
|
5099
|
+
spinner.succeed(chalk.green('AgentVibes installed successfully!'));
|
|
4660
5100
|
|
|
4661
|
-
//
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
}
|
|
5101
|
+
// Clean final summary
|
|
5102
|
+
console.log('');
|
|
5103
|
+
console.log(chalk.green.bold(' ā
Installation Complete'));
|
|
5104
|
+
console.log(chalk.gray(` Provider: ${selectedProvider}`));
|
|
5105
|
+
console.log(chalk.gray(` Location: ${targetDir}/.claude/`));
|
|
5106
|
+
console.log(chalk.gray(` Version: ${VERSION}`));
|
|
5107
|
+
console.log('');
|
|
5108
|
+
console.log(chalk.white(' Run ') + chalk.cyan('npx agentvibes') + chalk.white(' to open the console.'));
|
|
5109
|
+
console.log('');
|
|
4665
5110
|
|
|
4666
5111
|
} catch (error) {
|
|
4667
5112
|
spinner.fail('Installation failed!');
|
|
@@ -4680,7 +5125,6 @@ program
|
|
|
4680
5125
|
.description('Install AgentVibes voice commands')
|
|
4681
5126
|
.option('-d, --directory <path>', 'Installation directory (default: current directory)')
|
|
4682
5127
|
.option('-y, --yes', 'Skip confirmation prompt (auto-confirm)')
|
|
4683
|
-
.option('--with-audio', 'Play welcome demo audio after installation')
|
|
4684
5128
|
.action(async (options) => {
|
|
4685
5129
|
await install(options);
|
|
4686
5130
|
});
|
|
@@ -4700,7 +5144,8 @@ program
|
|
|
4700
5144
|
console.log(chalk.gray(` Update location: ${targetDir}/.claude/`));
|
|
4701
5145
|
console.log(chalk.gray(` Package version: ${VERSION}`));
|
|
4702
5146
|
|
|
4703
|
-
|
|
5147
|
+
const releaseInfo = getReleaseInfoBoxen();
|
|
5148
|
+
if (releaseInfo) console.log(releaseInfo);
|
|
4704
5149
|
|
|
4705
5150
|
// Check if already installed
|
|
4706
5151
|
const commandsDir = path.join(targetDir, '.claude', 'commands', 'agent-vibes');
|
|
@@ -5055,6 +5500,232 @@ program
|
|
|
5055
5500
|
await resetBmadVoices(options);
|
|
5056
5501
|
});
|
|
5057
5502
|
|
|
5503
|
+
// Story 1.4: Config Command - Post-install configuration
|
|
5504
|
+
program
|
|
5505
|
+
.command('config <setting>')
|
|
5506
|
+
.description('Configure AgentVibes settings after installation')
|
|
5507
|
+
.usage('intro-text [--no-default]')
|
|
5508
|
+
.action(async (setting, options) => {
|
|
5509
|
+
if (setting === 'intro-text' || setting === 'pretext') {
|
|
5510
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
5511
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
5512
|
+
const pretextFile = path.join(claudeDir, 'config', 'tts-pretext.txt');
|
|
5513
|
+
|
|
5514
|
+
// Read current pretext if it exists
|
|
5515
|
+
let currentPretext = '';
|
|
5516
|
+
try {
|
|
5517
|
+
if (fsSync.existsSync(pretextFile)) {
|
|
5518
|
+
currentPretext = fsSync.readFileSync(pretextFile, 'utf-8').trim();
|
|
5519
|
+
}
|
|
5520
|
+
} catch (err) {
|
|
5521
|
+
// Ignore read errors
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
// Prompt for new intro text
|
|
5525
|
+
const { newPretext } = await inquirer.prompt([
|
|
5526
|
+
{
|
|
5527
|
+
type: 'input',
|
|
5528
|
+
name: 'newPretext',
|
|
5529
|
+
message: chalk.yellow('Enter new intro text (max 50 chars):'),
|
|
5530
|
+
default: currentPretext || '(none)',
|
|
5531
|
+
validate: (input) => {
|
|
5532
|
+
if (input === '(none)') return true;
|
|
5533
|
+
if (input.length > 50) return 'Max 50 characters';
|
|
5534
|
+
if (input.includes('\n') || input.includes('\r')) return 'No newlines allowed';
|
|
5535
|
+
return true;
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
]);
|
|
5539
|
+
|
|
5540
|
+
// Handle the response
|
|
5541
|
+
if (newPretext === '(none)' || newPretext === '') {
|
|
5542
|
+
// Remove pretext
|
|
5543
|
+
try {
|
|
5544
|
+
await fs.unlink(pretextFile);
|
|
5545
|
+
console.log(chalk.green('ā Intro text cleared\n'));
|
|
5546
|
+
} catch (err) {
|
|
5547
|
+
// File doesn't exist - that's fine
|
|
5548
|
+
console.log(chalk.green('ā No intro text configured\n'));
|
|
5549
|
+
}
|
|
5550
|
+
} else {
|
|
5551
|
+
// Save new pretext
|
|
5552
|
+
try {
|
|
5553
|
+
const configDir = path.join(claudeDir, 'config');
|
|
5554
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
5555
|
+
await fs.writeFile(pretextFile, newPretext, { mode: 0o600 });
|
|
5556
|
+
console.log(chalk.green(`ā Intro text updated: "${newPretext}"\n`));
|
|
5557
|
+
console.log(chalk.cyan('Preview: ') + chalk.gray(`"${newPretext}" This is a sample response\n`));
|
|
5558
|
+
} catch (err) {
|
|
5559
|
+
console.log(chalk.red(`ā Error saving intro text: ${err.message}\n`));
|
|
5560
|
+
process.exit(1);
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
} else if (setting === 'music') {
|
|
5564
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
5565
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
5566
|
+
const musicTracksDir = path.join(claudeDir, 'audio', 'custom-music', 'tracks');
|
|
5567
|
+
const musicConfigFile = path.join(claudeDir, 'config', 'background-music.txt');
|
|
5568
|
+
const musicEnabledFile = path.join(claudeDir, 'config', 'background-music-enabled.txt');
|
|
5569
|
+
|
|
5570
|
+
console.log(boxen(
|
|
5571
|
+
chalk.cyan.bold('šµ Background Music Configuration\n\n') +
|
|
5572
|
+
chalk.white('Manage your custom background music settings.'),
|
|
5573
|
+
{ padding: 1, borderColor: 'cyan', borderStyle: 'round' }
|
|
5574
|
+
));
|
|
5575
|
+
|
|
5576
|
+
// Read current music setting
|
|
5577
|
+
let currentMusic = null;
|
|
5578
|
+
let musicEnabled = false;
|
|
5579
|
+
|
|
5580
|
+
try {
|
|
5581
|
+
if (fsSync.existsSync(musicEnabledFile)) {
|
|
5582
|
+
const enabled = fsSync.readFileSync(musicEnabledFile, 'utf-8').trim();
|
|
5583
|
+
musicEnabled = enabled === 'true' || enabled === '1';
|
|
5584
|
+
}
|
|
5585
|
+
if (fsSync.existsSync(musicConfigFile)) {
|
|
5586
|
+
currentMusic = fsSync.readFileSync(musicConfigFile, 'utf-8').trim();
|
|
5587
|
+
}
|
|
5588
|
+
} catch (err) {
|
|
5589
|
+
// Ignore read errors
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
// Display current setting
|
|
5593
|
+
console.log(chalk.gray('\nCurrent Settings:'));
|
|
5594
|
+
console.log(chalk.gray(' Background Music: ') + (musicEnabled ? chalk.green('Enabled') : chalk.yellow('Disabled')));
|
|
5595
|
+
|
|
5596
|
+
if (currentMusic && musicEnabled) {
|
|
5597
|
+
const isCustom = currentMusic.startsWith('custom-') || !currentMusic.includes('agentvibes_');
|
|
5598
|
+
if (isCustom) {
|
|
5599
|
+
console.log(chalk.gray(' Track: ') + chalk.cyan(currentMusic) + chalk.yellow(' (Custom)'));
|
|
5600
|
+
} else {
|
|
5601
|
+
console.log(chalk.gray(' Track: ') + chalk.white(currentMusic) + chalk.gray(' (Default)'));
|
|
5602
|
+
}
|
|
5603
|
+
} else if (!musicEnabled) {
|
|
5604
|
+
console.log(chalk.gray(' Track: ') + chalk.gray('(None - music disabled)'));
|
|
5605
|
+
}
|
|
5606
|
+
console.log('');
|
|
5607
|
+
|
|
5608
|
+
// Show menu
|
|
5609
|
+
const { action } = await inquirer.prompt([{
|
|
5610
|
+
type: 'list',
|
|
5611
|
+
name: 'action',
|
|
5612
|
+
message: chalk.yellow('What would you like to do?'),
|
|
5613
|
+
choices: [
|
|
5614
|
+
{ name: 'šµ Change custom music file', value: 'change' },
|
|
5615
|
+
{ name: 'šļø Remove custom music (use defaults)', value: 'remove' },
|
|
5616
|
+
{ name: 'š Reset to factory defaults', value: 'reset' },
|
|
5617
|
+
{ name: musicEnabled ? 'š Disable background music' : 'š Enable background music', value: 'toggle' },
|
|
5618
|
+
new inquirer.Separator(),
|
|
5619
|
+
{ name: 'ā Back', value: 'back' }
|
|
5620
|
+
]
|
|
5621
|
+
}]);
|
|
5622
|
+
|
|
5623
|
+
if (action === 'change') {
|
|
5624
|
+
// Change music - reuse promptForCustomMusic from Story 4.5
|
|
5625
|
+
console.log('');
|
|
5626
|
+
const result = await promptForCustomMusic(claudeDir);
|
|
5627
|
+
|
|
5628
|
+
if (result.success && result.filename) {
|
|
5629
|
+
// Update config to point to custom music
|
|
5630
|
+
const configDir = path.join(claudeDir, 'config');
|
|
5631
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
5632
|
+
await fs.writeFile(musicConfigFile, result.filename, { mode: 0o600 });
|
|
5633
|
+
|
|
5634
|
+
// Ensure music is enabled
|
|
5635
|
+
await fs.writeFile(musicEnabledFile, 'true', { mode: 0o644 });
|
|
5636
|
+
|
|
5637
|
+
console.log(chalk.green(`\nā Background music updated to: ${result.filename}`));
|
|
5638
|
+
console.log(chalk.gray(' Changes take effect immediately\n'));
|
|
5639
|
+
} else if (result.error) {
|
|
5640
|
+
console.log(chalk.yellow(`\nā ļø ${result.error}`));
|
|
5641
|
+
console.log(chalk.gray(' Keeping previous setting\n'));
|
|
5642
|
+
} else {
|
|
5643
|
+
console.log(chalk.gray('\n No changes made\n'));
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
} else if (action === 'remove') {
|
|
5647
|
+
// Remove custom music - keep defaults
|
|
5648
|
+
const customMusicFiles = fsSync.existsSync(musicTracksDir) ?
|
|
5649
|
+
fsSync.readdirSync(musicTracksDir) : [];
|
|
5650
|
+
|
|
5651
|
+
if (customMusicFiles.length === 0) {
|
|
5652
|
+
console.log(chalk.yellow('\nā ļø No custom music files found\n'));
|
|
5653
|
+
} else {
|
|
5654
|
+
const { confirm } = await inquirer.prompt([{
|
|
5655
|
+
type: 'confirm',
|
|
5656
|
+
name: 'confirm',
|
|
5657
|
+
message: 'Remove all custom music files?',
|
|
5658
|
+
default: false
|
|
5659
|
+
}]);
|
|
5660
|
+
|
|
5661
|
+
if (confirm) {
|
|
5662
|
+
try {
|
|
5663
|
+
// Remove custom music files
|
|
5664
|
+
for (const file of customMusicFiles) {
|
|
5665
|
+
await fs.unlink(path.join(musicTracksDir, file));
|
|
5666
|
+
}
|
|
5667
|
+
|
|
5668
|
+
// Reset to default music
|
|
5669
|
+
await fs.writeFile(musicConfigFile, 'agentvibes_soft_flamenco_loop.mp3', { mode: 0o600 });
|
|
5670
|
+
|
|
5671
|
+
console.log(chalk.green('\nā Custom music removed, reverted to defaults\n'));
|
|
5672
|
+
} catch (err) {
|
|
5673
|
+
console.log(chalk.red(`\nā Error removing custom music: ${err.message}\n`));
|
|
5674
|
+
}
|
|
5675
|
+
} else {
|
|
5676
|
+
console.log(chalk.gray('\n Cancelled\n'));
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
|
|
5680
|
+
} else if (action === 'reset') {
|
|
5681
|
+
// Reset to factory defaults
|
|
5682
|
+
const { confirm } = await inquirer.prompt([{
|
|
5683
|
+
type: 'confirm',
|
|
5684
|
+
name: 'confirm',
|
|
5685
|
+
message: 'Reset all music settings to factory defaults?',
|
|
5686
|
+
default: false
|
|
5687
|
+
}]);
|
|
5688
|
+
|
|
5689
|
+
if (confirm) {
|
|
5690
|
+
try {
|
|
5691
|
+
// Reset to default track
|
|
5692
|
+
await fs.writeFile(musicConfigFile, 'agentvibes_soft_flamenco_loop.mp3', { mode: 0o600 });
|
|
5693
|
+
|
|
5694
|
+
// Enable music
|
|
5695
|
+
await fs.writeFile(musicEnabledFile, 'true', { mode: 0o644 });
|
|
5696
|
+
|
|
5697
|
+
console.log(chalk.green('\nā Reset to factory defaults'));
|
|
5698
|
+
console.log(chalk.gray(' Track: Soft Flamenco (Spanish guitar)'));
|
|
5699
|
+
console.log(chalk.gray(' Status: Enabled\n'));
|
|
5700
|
+
} catch (err) {
|
|
5701
|
+
console.log(chalk.red(`\nā Error resetting settings: ${err.message}\n`));
|
|
5702
|
+
}
|
|
5703
|
+
} else {
|
|
5704
|
+
console.log(chalk.gray('\n Cancelled\n'));
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
} else if (action === 'toggle') {
|
|
5708
|
+
// Toggle music on/off
|
|
5709
|
+
try {
|
|
5710
|
+
const newState = !musicEnabled;
|
|
5711
|
+
await fs.writeFile(musicEnabledFile, newState ? 'true' : 'false', { mode: 0o644 });
|
|
5712
|
+
|
|
5713
|
+
console.log(chalk.green(`\nā Background music ${newState ? 'enabled' : 'disabled'}\n`));
|
|
5714
|
+
} catch (err) {
|
|
5715
|
+
console.log(chalk.red(`\nā Error toggling music: ${err.message}\n`));
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
} else if (action === 'back') {
|
|
5719
|
+
console.log(chalk.gray('\n No changes made\n'));
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
} else {
|
|
5723
|
+
console.log(chalk.red(`ā Unknown setting: ${setting}`));
|
|
5724
|
+
console.log(chalk.gray('Available settings: intro-text, music\n'));
|
|
5725
|
+
process.exit(1);
|
|
5726
|
+
}
|
|
5727
|
+
});
|
|
5728
|
+
|
|
5058
5729
|
// BMAD PR Testing Command
|
|
5059
5730
|
program
|
|
5060
5731
|
.command('test-bmad-pr [pr-number]')
|
|
@@ -5096,5 +5767,11 @@ if (__entryFile === __argvFile) {
|
|
|
5096
5767
|
}
|
|
5097
5768
|
/* c8 ignore stop */
|
|
5098
5769
|
|
|
5099
|
-
// Export functions for testing
|
|
5100
|
-
export {
|
|
5770
|
+
// Export functions for testing and TUI installer
|
|
5771
|
+
export {
|
|
5772
|
+
isTermux, isNativeWindows, detectAndNotifyTermux,
|
|
5773
|
+
copyCommandFiles, copyHookFiles, copyPersonalityFiles,
|
|
5774
|
+
copyPluginFiles, copyBmadConfigFiles, copyBackgroundMusicFiles,
|
|
5775
|
+
copyConfigFiles, configureSessionStartHook, ensureGitRepo,
|
|
5776
|
+
installPluginManifest, checkAndInstallPiper,
|
|
5777
|
+
};
|