agentvibes 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/config.json +17 -12
- package/.agentvibes/install-manifest.json +98 -86
- package/.claude/audio/ui/CREDITS.txt +16 -0
- package/.claude/audio/ui/bling-success.wav +0 -0
- package/.claude/commands/agent-vibes/receiver.md +64 -0
- package/.claude/config/audio-effects.cfg +3 -3
- package/.claude/config/personality.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -1
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-processor.sh +57 -18
- package/.claude/hooks/kokoro-installer.sh +117 -0
- package/.claude/hooks/kokoro-server.py +219 -0
- package/.claude/hooks/kokoro-tts.py +141 -0
- package/.claude/hooks/piper-download-voices.sh +39 -16
- package/.claude/hooks/piper-installer.sh +11 -6
- package/.claude/hooks/piper-voice-manager.sh +6 -6
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
- package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
- package/.claude/hooks/play-tts-kokoro.sh +127 -0
- package/.claude/hooks/play-tts-piper.sh +20 -13
- package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
- package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
- package/.claude/hooks/play-tts.sh +78 -2
- package/.claude/hooks/provider-commands.sh +71 -22
- package/.claude/hooks/provider-manager.sh +15 -7
- package/.claude/hooks/voice-manager.sh +6 -0
- package/.claude/hooks-windows/kokoro-server.py +219 -0
- package/.claude/hooks-windows/kokoro-tts.py +107 -0
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
- package/.claude/hooks-windows/play-tts.ps1 +29 -1
- package/README.md +12 -86
- package/RELEASE_NOTES.md +60 -0
- package/mcp-server/server.py +17 -7
- package/package.json +4 -3
- package/src/commands/install-mcp.js +730 -476
- package/src/console/app.js +28 -12
- package/src/console/audio-env.js +85 -1
- package/src/console/navigation.js +4 -0
- package/src/console/tabs/agents-tab.js +18 -12
- package/src/console/tabs/music-tab.js +7 -25
- package/src/console/tabs/receiver-tab.js +13 -13
- package/src/console/tabs/settings-tab.js +2 -2
- package/src/console/tabs/setup-tab.js +1708 -124
- package/src/console/tabs/voices-tab.js +76 -92
- package/src/console/widgets/format-utils.js +14 -2
- package/src/console/widgets/help-bar.js +55 -0
- package/src/console/widgets/personality-picker.js +2 -2
- package/src/console/widgets/reverb-picker.js +429 -41
- package/src/console/widgets/track-picker.js +60 -51
- package/src/i18n/de.js +204 -203
- package/src/i18n/en.js +1 -0
- package/src/i18n/es.js +204 -203
- package/src/i18n/fr.js +204 -203
- package/src/i18n/hi.js +204 -203
- package/src/i18n/ja.js +204 -203
- package/src/i18n/ko.js +204 -203
- package/src/i18n/pt.js +204 -203
- package/src/i18n/strings.js +54 -54
- package/src/i18n/zh-CN.js +204 -203
- package/src/installer.js +127 -34
- package/src/services/provider-service.js +178 -143
- package/src/services/provider-voice-catalog.js +126 -0
- package/src/services/tts-engine-service.js +53 -4
- package/src/utils/audio-duration-validator.js +341 -298
- package/src/utils/list-formatter.js +200 -194
- package/src/utils/platform-resolver.js +369 -0
- package/src/utils/preview-list-prompt.js +8 -0
- package/src/utils/provider-validator.js +79 -9
- package/templates/agentvibes-receiver.sh +6 -2
package/src/console/app.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import blessed from 'blessed';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
15
16
|
import { NavigationService, TAB_ORDER } from '../services/navigation-service.js';
|
|
@@ -238,9 +239,9 @@ export class AgentVibesConsole {
|
|
|
238
239
|
// Right-aligned: git remote + branch when available, else AgentVibes repo link
|
|
239
240
|
let topRightContent = `{${BRAND_PINK}-fg}github.com/preibisch/agentvibes{/${BRAND_PINK}-fg}`;
|
|
240
241
|
try {
|
|
241
|
-
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'],
|
|
242
|
+
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], // NOSONAR
|
|
242
243
|
{ encoding: 'utf8', timeout: 2000, cwd });
|
|
243
|
-
const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'],
|
|
244
|
+
const remoteResult = spawnSync('git', ['remote', 'get-url', 'origin'], // NOSONAR
|
|
244
245
|
{ encoding: 'utf8', timeout: 2000, cwd });
|
|
245
246
|
if (branchResult.status === 0 && remoteResult.status === 0) {
|
|
246
247
|
const branch = branchResult.stdout.trim();
|
|
@@ -627,24 +628,39 @@ export class AgentVibesConsole {
|
|
|
627
628
|
_createFooter() {
|
|
628
629
|
// Detect installed providers inline (same logic as ProviderService)
|
|
629
630
|
const _has = (bin) => {
|
|
630
|
-
try { execFileSync('which', [bin], { stdio: 'ignore', timeout: 2000 }); return true; }
|
|
631
|
+
try { execFileSync('which', [bin], { stdio: 'ignore', timeout: 2000 }); return true; } // NOSONAR
|
|
631
632
|
catch { return false; }
|
|
632
633
|
};
|
|
634
|
+
const _pyHas = (mod) => {
|
|
635
|
+
try { execFileSync('python3', ['-c', `import ${mod}`], { stdio: 'ignore', timeout: 3000 }); return true; } // NOSONAR
|
|
636
|
+
catch { return false; }
|
|
637
|
+
};
|
|
638
|
+
const _elKeySet = () => {
|
|
639
|
+
if (process.env.ELEVENLABS_API_KEY) return true;
|
|
640
|
+
try {
|
|
641
|
+
const kf = path.join(os.homedir(), '.agentvibes', 'elevenlabs-key.txt');
|
|
642
|
+
return existsSync(kf) && readFileSync(kf, 'utf8').trim().length > 0;
|
|
643
|
+
} catch { return false; }
|
|
644
|
+
};
|
|
633
645
|
const detected = {
|
|
634
|
-
piper:
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
646
|
+
piper: _has('piper'),
|
|
647
|
+
kokoro: _pyHas('kokoro'),
|
|
648
|
+
elevenlabs: _elKeySet(),
|
|
649
|
+
soprano: _has('soprano'),
|
|
650
|
+
sapi: process.platform === 'win32',
|
|
651
|
+
macos: process.platform === 'darwin' && _has('say'),
|
|
638
652
|
};
|
|
639
653
|
|
|
640
654
|
// Build provider status badges: ● Name (green if detected, grey if not)
|
|
641
655
|
const on = (label) => `{green-fg}●{/green-fg} ${label}`;
|
|
642
656
|
const off = (label) => `{#546e7a-fg}● ${label}{/#546e7a-fg}`;
|
|
643
657
|
const badges = [
|
|
644
|
-
detected.piper
|
|
645
|
-
detected.
|
|
646
|
-
detected.
|
|
647
|
-
detected.
|
|
658
|
+
detected.piper ? on('Piper') : off('Piper'),
|
|
659
|
+
detected.kokoro ? on('Kokoro') : off('Kokoro'),
|
|
660
|
+
detected.elevenlabs ? on('ElevenLabs') : off('ElevenLabs'),
|
|
661
|
+
detected.soprano ? on('Soprano') : off('Soprano'),
|
|
662
|
+
detected.sapi ? on('Windows SAPI') : off('Windows SAPI'),
|
|
663
|
+
detected.macos ? on('Mac Say') : off('Mac Say'),
|
|
648
664
|
].join(' ');
|
|
649
665
|
|
|
650
666
|
const footer = blessed.box({
|
package/src/console/audio-env.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import os from 'node:os';
|
|
11
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Build a spawn environment with correct PULSE_SERVER handling.
|
|
@@ -151,6 +151,90 @@ export function detectMp3Player(env) {
|
|
|
151
151
|
return _detect(MP3_PLAYERS, env);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Returns true when running headless with PulseAudio over TCP.
|
|
156
|
+
* SDL2-based players (ffplay) fail in this configuration with exit 123.
|
|
157
|
+
*/
|
|
158
|
+
function _isHeadlessPulse(env) {
|
|
159
|
+
return !env.DISPLAY && typeof env.PULSE_SERVER === 'string' && env.PULSE_SERVER.startsWith('tcp:');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Spawn an MP3 player for the given track path.
|
|
164
|
+
*
|
|
165
|
+
* On headless servers with PulseAudio TCP, SDL2-based players (ffplay) fail
|
|
166
|
+
* because DISPLAY is not set. In that case uses ffmpeg→pacat pipe.
|
|
167
|
+
*
|
|
168
|
+
* Returns a process-like object with:
|
|
169
|
+
* .pid — primary process PID
|
|
170
|
+
* .kill() — stops playback, killing all spawned processes
|
|
171
|
+
* .on(event, cb) — supports 'exit' and 'error'
|
|
172
|
+
*
|
|
173
|
+
* Returns null if no suitable player or required binaries are unavailable.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} trackPath - Absolute path to the MP3 file
|
|
176
|
+
* @param {Object} [env] - Environment (defaults to buildAudioEnv())
|
|
177
|
+
* @returns {{ pid: number, kill: Function, on: Function }|null}
|
|
178
|
+
*/
|
|
179
|
+
export function spawnMp3Player(trackPath, env) {
|
|
180
|
+
env = env ?? buildAudioEnv();
|
|
181
|
+
|
|
182
|
+
if (_isHeadlessPulse(env)) {
|
|
183
|
+
// Check required binaries are present.
|
|
184
|
+
// S4036: standard system media tools resolved via the user's local PATH on
|
|
185
|
+
// their own machine — no untrusted PATH, absolute paths aren't portable
|
|
186
|
+
// across distros/Homebrew/Nix. Risk accepted.
|
|
187
|
+
if (spawnSync('which', ['ffmpeg'], { stdio: 'ignore', env }).status !== 0) return null; // NOSONAR
|
|
188
|
+
if (spawnSync('which', ['pacat'], { stdio: 'ignore', env }).status !== 0) return null; // NOSONAR
|
|
189
|
+
|
|
190
|
+
const ff = spawn('ffmpeg', // NOSONAR — see S4036 note above
|
|
191
|
+
['-i', trackPath, '-f', 's16le', '-ac', '2', '-ar', '44100', 'pipe:1', '-loglevel', 'quiet'],
|
|
192
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], env },
|
|
193
|
+
);
|
|
194
|
+
const pa = spawn('pacat', // NOSONAR — see S4036 note above
|
|
195
|
+
['--playback', '--format=s16le', '--channels=2', '--rate=44100'],
|
|
196
|
+
{ stdio: ['pipe', 'ignore', 'ignore'], env },
|
|
197
|
+
);
|
|
198
|
+
ff.stdout.on('error', () => {});
|
|
199
|
+
pa.stdin.on('error', () => {});
|
|
200
|
+
ff.stdout.pipe(pa.stdin);
|
|
201
|
+
|
|
202
|
+
const _listeners = { exit: [], error: [] };
|
|
203
|
+
pa.on('exit', (code, sig) => _listeners.exit.forEach(cb => cb(code, sig)));
|
|
204
|
+
ff.on('error', err => _listeners.error.forEach(cb => cb(err)));
|
|
205
|
+
pa.on('error', err => _listeners.error.forEach(cb => cb(err)));
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
pid: ff.pid,
|
|
209
|
+
kill() { try { ff.kill(); } catch {} try { pa.kill(); } catch {} },
|
|
210
|
+
on(event, cb) { if (_listeners[event]) _listeners[event].push(cb); },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const player = detectMp3Player(env);
|
|
215
|
+
if (!player) return null;
|
|
216
|
+
|
|
217
|
+
const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
218
|
+
const proc = spawn(player.bin, player.args(trackPath), {
|
|
219
|
+
stdio: 'ignore', detached: !isWin, windowsHide: true, env,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
pid: proc.pid,
|
|
224
|
+
kill() {
|
|
225
|
+
try {
|
|
226
|
+
if (isWin) {
|
|
227
|
+
spawn('taskkill', ['/F', '/T', '/PID', String(proc.pid)], // NOSONAR
|
|
228
|
+
{ stdio: 'ignore', windowsHide: true });
|
|
229
|
+
} else {
|
|
230
|
+
process.kill(-proc.pid, 'SIGTERM');
|
|
231
|
+
}
|
|
232
|
+
} catch (e) { if (e.code !== 'ESRCH') { /* ignore */ } }
|
|
233
|
+
},
|
|
234
|
+
on(event, cb) { proc.on(event, cb); },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
154
238
|
/**
|
|
155
239
|
* Detect the best available WAV player.
|
|
156
240
|
* On Windows, uses PowerShell SoundPlayer as built-in fallback.
|
|
@@ -40,8 +40,12 @@ const KEY_TO_TAB = {
|
|
|
40
40
|
export function setupNavigation(screen, navigationService, focusMainTabBar) {
|
|
41
41
|
// Tab switching shortcuts — one handler per key (both cases)
|
|
42
42
|
// When a modal is open, force-close all modals then switch tabs.
|
|
43
|
+
// Exception: if already on the target tab, pressing its shortcut is a no-op —
|
|
44
|
+
// the user is trying to do something within the tab (e.g. 'I' in setup means
|
|
45
|
+
// "install", not "re-navigate to setup and close all my pickers").
|
|
43
46
|
for (const [key, tabId] of Object.entries(KEY_TO_TAB)) {
|
|
44
47
|
screen.key([key], () => {
|
|
48
|
+
if (navigationService.getActiveTab() === tabId) return;
|
|
45
49
|
if (navigationService.isModalOpen()) {
|
|
46
50
|
navigationService.forceCloseAll();
|
|
47
51
|
setTimeout(() => navigationService.switchTab(tabId), 0);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getFavorites, getThumbsDown, toggleThumbsUp, toggleThumbsDown,
|
|
21
21
|
} from './voices-tab.js';
|
|
22
22
|
import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
|
|
23
|
+
import { voicesForProvider } from '../../services/provider-voice-catalog.js';
|
|
23
24
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
24
25
|
import { BRAND_PINK } from '../brand-colors.js';
|
|
25
26
|
import { t } from '../../i18n/strings.js';
|
|
@@ -642,7 +643,7 @@ ${_tl('bmadDesc')}
|
|
|
642
643
|
const FIELDS = [
|
|
643
644
|
{ key: 'voice', label: 'Voice', getValue: () => draft.voice || '(global default)' },
|
|
644
645
|
{ key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(default)' },
|
|
645
|
-
{ key: 'reverbPreset', label: '
|
|
646
|
+
{ key: 'reverbPreset', label: 'Audio Effects', getValue: () => formatReverbState(draft.reverbPreset) },
|
|
646
647
|
{ key: 'personality', label: 'Personality', getValue: () => {
|
|
647
648
|
const p = draft.personality;
|
|
648
649
|
const emoji = PERSONALITY_EMOJIS[p] || '';
|
|
@@ -1061,7 +1062,7 @@ ${_tl('bmadDesc')}
|
|
|
1061
1062
|
if (!fs.existsSync(playTtsScript)) return;
|
|
1062
1063
|
_previewVoiceId = voiceId;
|
|
1063
1064
|
if (!_vpClosed) { vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}...{/bright-cyan-fg}`); _refreshVP(); }
|
|
1064
|
-
_previewProc = spawn('powershell', [
|
|
1065
|
+
_previewProc = spawn('powershell', [ // NOSONAR
|
|
1065
1066
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTtsScript, phrase, voiceId,
|
|
1066
1067
|
], { stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv });
|
|
1067
1068
|
_previewProc.on('exit', () => {
|
|
@@ -1079,7 +1080,7 @@ ${_tl('bmadDesc')}
|
|
|
1079
1080
|
const args = [playTtsScript, phrase, voiceId];
|
|
1080
1081
|
if (remoteLlm) args.push('--llm', remoteLlm);
|
|
1081
1082
|
|
|
1082
|
-
_previewProc = spawn('bash', args, {
|
|
1083
|
+
_previewProc = spawn('bash', args, { // NOSONAR
|
|
1083
1084
|
stdio: 'ignore', detached: true,
|
|
1084
1085
|
env: { ..._spawnEnv, CLAUDE_PROJECT_DIR: _projectRoot },
|
|
1085
1086
|
cwd: _projectRoot,
|
|
@@ -1277,7 +1278,7 @@ ${_tl('bmadDesc')}
|
|
|
1277
1278
|
|
|
1278
1279
|
const voiceId = profile.voice || '';
|
|
1279
1280
|
const pretext = profile.pretext || AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title);
|
|
1280
|
-
const phrase = `${pretext} ${SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)]}`;
|
|
1281
|
+
const phrase = `${pretext} ${SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)]}`; // NOSONAR
|
|
1281
1282
|
|
|
1282
1283
|
const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1283
1284
|
|
|
@@ -1295,7 +1296,7 @@ ${_tl('bmadDesc')}
|
|
|
1295
1296
|
if (voiceId) args.push(voiceId);
|
|
1296
1297
|
if (remoteLlm) args.push('--llm', remoteLlm);
|
|
1297
1298
|
|
|
1298
|
-
const proc = spawn('bash', args, {
|
|
1299
|
+
const proc = spawn('bash', args, { // NOSONAR
|
|
1299
1300
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1300
1301
|
detached: true,
|
|
1301
1302
|
env: { ..._spawnEnv },
|
|
@@ -1434,7 +1435,7 @@ ${_tl('bmadDesc')}
|
|
|
1434
1435
|
const psArgs = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase];
|
|
1435
1436
|
if (voiceId) psArgs.push(voiceId);
|
|
1436
1437
|
|
|
1437
|
-
const proc = spawn('powershell', psArgs, {
|
|
1438
|
+
const proc = spawn('powershell', psArgs, { // NOSONAR
|
|
1438
1439
|
stdio: 'ignore', detached: false, windowsHide: true, env: spawnEnv,
|
|
1439
1440
|
});
|
|
1440
1441
|
_playingProcess = proc;
|
|
@@ -1457,7 +1458,7 @@ ${_tl('bmadDesc')}
|
|
|
1457
1458
|
function _shuffleArray(arr) {
|
|
1458
1459
|
const a = [...arr];
|
|
1459
1460
|
for (let i = a.length - 1; i > 0; i--) {
|
|
1460
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
1461
|
+
const j = Math.floor(Math.random() * (i + 1)); // NOSONAR
|
|
1461
1462
|
[a[i], a[j]] = [a[j], a[i]];
|
|
1462
1463
|
}
|
|
1463
1464
|
return a;
|
|
@@ -1494,13 +1495,18 @@ ${_tl('bmadDesc')}
|
|
|
1494
1495
|
}
|
|
1495
1496
|
|
|
1496
1497
|
function _autoAssignVoices() {
|
|
1497
|
-
|
|
1498
|
+
// Pull the pool from whichever provider is currently active, not always Piper.
|
|
1499
|
+
// Agent profiles store only a voice id (the active provider is global), so
|
|
1500
|
+
// switching the provider and re-running auto-assign rebinds every agent to a
|
|
1501
|
+
// voice from that provider (Kokoro, ElevenLabs, soprano, …).
|
|
1502
|
+
const provider = providerService?.getActiveProvider?.() ?? 'piper';
|
|
1503
|
+
const installed = voicesForProvider(provider, { scanInstalledVoices, getVoiceMeta });
|
|
1498
1504
|
if (installed.length === 0) return false;
|
|
1499
1505
|
|
|
1500
|
-
//
|
|
1501
|
-
const femaleVoices = _shuffleArray(installed.filter(v =>
|
|
1502
|
-
const maleVoices = _shuffleArray(installed.filter(v =>
|
|
1503
|
-
const otherVoices = _shuffleArray(installed.filter(v => !['Male', 'Female'].includes(
|
|
1506
|
+
// installed is [{ id, gender }] — separate the ids by gender.
|
|
1507
|
+
const femaleVoices = _shuffleArray(installed.filter(v => v.gender === 'Female').map(v => v.id));
|
|
1508
|
+
const maleVoices = _shuffleArray(installed.filter(v => v.gender === 'Male').map(v => v.id));
|
|
1509
|
+
const otherVoices = _shuffleArray(installed.filter(v => !['Male', 'Female'].includes(v.gender)).map(v => v.id));
|
|
1504
1510
|
|
|
1505
1511
|
// Separate agents by gender
|
|
1506
1512
|
const femaleAgents = _agents.filter(a => _inferAgentGender(a) === 'Female');
|
|
@@ -13,8 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import {
|
|
17
|
-
import { buildAudioEnv, detectMp3Player } from '../audio-env.js';
|
|
16
|
+
import { buildAudioEnv, spawnMp3Player } from '../audio-env.js';
|
|
18
17
|
import { t } from '../../i18n/strings.js';
|
|
19
18
|
|
|
20
19
|
// Package-relative tracks dir — used as fallback when cwd has no .claude/audio/tracks/
|
|
@@ -542,28 +541,14 @@ export function createMusicTab(screen, services) {
|
|
|
542
541
|
let _playingProcess = null;
|
|
543
542
|
let _playingTrackId = null;
|
|
544
543
|
|
|
545
|
-
// Kill the entire process group so child audio processes (ffplay, play, mpg123) all die
|
|
546
544
|
function _killPlayingProcess() {
|
|
547
545
|
if (_playingProcess) {
|
|
548
|
-
|
|
549
|
-
try {
|
|
550
|
-
if (_isWin) {
|
|
551
|
-
// Windows: kill the process tree via taskkill (process group kill doesn't work)
|
|
552
|
-
spawn('taskkill', ['/F', '/T', '/PID', String(_playingProcess.pid)], {
|
|
553
|
-
stdio: 'ignore', windowsHide: true,
|
|
554
|
-
});
|
|
555
|
-
} else {
|
|
556
|
-
process.kill(-_playingProcess.pid, 'SIGTERM');
|
|
557
|
-
}
|
|
558
|
-
} catch (e) {
|
|
559
|
-
if (e.code !== 'ESRCH') { /* ignore */ }
|
|
560
|
-
}
|
|
546
|
+
_playingProcess.kill();
|
|
561
547
|
_playingProcess = null;
|
|
562
548
|
}
|
|
563
549
|
}
|
|
564
550
|
|
|
565
551
|
const _spawnEnv = buildAudioEnv();
|
|
566
|
-
const _detectedPlayer = detectMp3Player(_spawnEnv);
|
|
567
552
|
|
|
568
553
|
process.on('exit', () => { _killPlayingProcess(); });
|
|
569
554
|
|
|
@@ -594,7 +579,8 @@ export function createMusicTab(screen, services) {
|
|
|
594
579
|
_killPlayingProcess();
|
|
595
580
|
_playingTrackId = null;
|
|
596
581
|
|
|
597
|
-
|
|
582
|
+
const proc = spawnMp3Player(trackPath, _spawnEnv);
|
|
583
|
+
if (!proc) {
|
|
598
584
|
const installHint = process.platform === 'win32'
|
|
599
585
|
? 'No MP3 player found. Install ffmpeg: winget install ffmpeg'
|
|
600
586
|
: 'No MP3 player found. Install ffmpeg: sudo apt install ffmpeg';
|
|
@@ -604,18 +590,14 @@ export function createMusicTab(screen, services) {
|
|
|
604
590
|
return;
|
|
605
591
|
}
|
|
606
592
|
|
|
607
|
-
|
|
608
|
-
// Spawn the detected player directly (no sh -c chain — avoids VLC/cvlc stderr issues)
|
|
609
|
-
_playingProcess = spawn(_detectedPlayer.bin, _detectedPlayer.args(trackPath), {
|
|
610
|
-
stdio: 'ignore', detached: !_isWin, windowsHide: true, env: _spawnEnv,
|
|
611
|
-
});
|
|
593
|
+
_playingProcess = proc;
|
|
612
594
|
_playingTrackId = trackId;
|
|
613
595
|
|
|
614
596
|
const label = _allTracks.find(t => t.id === trackId)?.label ?? formatTrackLabel(trackId);
|
|
615
597
|
previewLine.setContent(`{${COLORS.playingFg}-fg}♪ Previewing: ${label} (Space again to stop){/${COLORS.playingFg}-fg}`);
|
|
616
598
|
screen.render();
|
|
617
599
|
|
|
618
|
-
|
|
600
|
+
proc.on('exit', () => {
|
|
619
601
|
if (_playingTrackId === trackId) {
|
|
620
602
|
_playingTrackId = null;
|
|
621
603
|
_playingProcess = null;
|
|
@@ -624,7 +606,7 @@ export function createMusicTab(screen, services) {
|
|
|
624
606
|
}
|
|
625
607
|
});
|
|
626
608
|
|
|
627
|
-
|
|
609
|
+
proc.on('error', () => {
|
|
628
610
|
if (_playingTrackId === trackId) {
|
|
629
611
|
_killPlayingProcess();
|
|
630
612
|
_playingTrackId = null;
|
|
@@ -73,10 +73,10 @@ function _getNetworkInfo() {
|
|
|
73
73
|
try {
|
|
74
74
|
if (isWin) {
|
|
75
75
|
// Use PowerShell to get local IP on Windows
|
|
76
|
-
localIp = execSync('powershell -NoProfile -Command "(Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notmatch \'Loopback\' } | Select-Object -First 1).IPAddress"',
|
|
76
|
+
localIp = execSync('powershell -NoProfile -Command "(Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notmatch \'Loopback\' } | Select-Object -First 1).IPAddress"', // NOSONAR
|
|
77
77
|
{ timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
78
78
|
} else {
|
|
79
|
-
localIp = execSync("hostname -I 2>/dev/null | awk '{print $1}'", { timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
79
|
+
localIp = execSync("hostname -I 2>/dev/null | awk '{print $1}'", { timeout: 3000, stdio: 'pipe' }).toString().trim(); // NOSONAR
|
|
80
80
|
}
|
|
81
81
|
} catch { /* ignore */ }
|
|
82
82
|
try {
|
|
@@ -86,7 +86,7 @@ function _getNetworkInfo() {
|
|
|
86
86
|
const m = sshdConf.match(/^Port\s+(\d+)/m);
|
|
87
87
|
if (m) sshPort = m[1];
|
|
88
88
|
} else {
|
|
89
|
-
const portLine = execSync("grep -E '^Port ' /etc/ssh/sshd_config 2>/dev/null || echo 'Port 22'", { timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
89
|
+
const portLine = execSync("grep -E '^Port ' /etc/ssh/sshd_config 2>/dev/null || echo 'Port 22'", { timeout: 3000, stdio: 'pipe' }).toString().trim(); // NOSONAR
|
|
90
90
|
const m = portLine.match(/^Port\s+(\d+)/);
|
|
91
91
|
if (m) sshPort = m[1];
|
|
92
92
|
}
|
|
@@ -130,7 +130,7 @@ function _detectSetupState() {
|
|
|
130
130
|
|
|
131
131
|
// Check sshd running
|
|
132
132
|
try {
|
|
133
|
-
const svc = execSync('powershell -NoProfile -Command "(Get-Service sshd -EA SilentlyContinue).Status"',
|
|
133
|
+
const svc = execSync('powershell -NoProfile -Command "(Get-Service sshd -EA SilentlyContinue).Status"', // NOSONAR
|
|
134
134
|
{ timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
135
135
|
state.sshdRunning = svc === 'Running';
|
|
136
136
|
} catch { /* sshd not installed */ }
|
|
@@ -143,13 +143,13 @@ function _detectSetupState() {
|
|
|
143
143
|
|
|
144
144
|
// Check ffmpeg
|
|
145
145
|
try {
|
|
146
|
-
execSync('where ffmpeg', { timeout: 3000, stdio: 'pipe' });
|
|
146
|
+
execSync('where ffmpeg', { timeout: 3000, stdio: 'pipe' }); // NOSONAR
|
|
147
147
|
state.ffmpegInstalled = true;
|
|
148
148
|
} catch { /* not found */ }
|
|
149
149
|
|
|
150
150
|
// Check piper
|
|
151
151
|
try {
|
|
152
|
-
execSync('where piper', { timeout: 3000, stdio: 'pipe' });
|
|
152
|
+
execSync('where piper', { timeout: 3000, stdio: 'pipe' }); // NOSONAR
|
|
153
153
|
state.piperInstalled = true;
|
|
154
154
|
} catch {
|
|
155
155
|
const piperPath = path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Piper', 'piper.exe');
|
|
@@ -165,10 +165,10 @@ function _detectSetupState() {
|
|
|
165
165
|
// Linux/macOS detection (original)
|
|
166
166
|
let receiverHome = '';
|
|
167
167
|
try {
|
|
168
|
-
execSync('id agentvibes-receiver', { timeout: 3000, stdio: 'pipe' });
|
|
168
|
+
execSync('id agentvibes-receiver', { timeout: 3000, stdio: 'pipe' }); // NOSONAR
|
|
169
169
|
state.receiverUserExists = true;
|
|
170
170
|
try {
|
|
171
|
-
receiverHome = execSync("getent passwd agentvibes-receiver 2>/dev/null | cut -d: -f6 || echo '/home/agentvibes-receiver'",
|
|
171
|
+
receiverHome = execSync("getent passwd agentvibes-receiver 2>/dev/null | cut -d: -f6 || echo '/home/agentvibes-receiver'", // NOSONAR
|
|
172
172
|
{ timeout: 3000, stdio: 'pipe' }).toString().trim();
|
|
173
173
|
} catch { receiverHome = '/home/agentvibes-receiver'; }
|
|
174
174
|
} catch { /* user does not exist */ }
|
|
@@ -201,7 +201,7 @@ function _detectSetupState() {
|
|
|
201
201
|
} catch { /* no read access */ }
|
|
202
202
|
|
|
203
203
|
try {
|
|
204
|
-
const modules = execSync('pactl list modules short 2>/dev/null', { timeout: 3000, stdio: 'pipe' }).toString();
|
|
204
|
+
const modules = execSync('pactl list modules short 2>/dev/null', { timeout: 3000, stdio: 'pipe' }).toString(); // NOSONAR
|
|
205
205
|
state.tcpModuleLoaded = modules.includes('module-native-protocol-tcp');
|
|
206
206
|
} catch { /* pactl not available */ }
|
|
207
207
|
}
|
|
@@ -1031,7 +1031,7 @@ export function createReceiverTab(screen, services) {
|
|
|
1031
1031
|
screen.render();
|
|
1032
1032
|
|
|
1033
1033
|
// Fire SSH in background — don't block the TUI
|
|
1034
|
-
const child = spawn('ssh', ['-o', 'ConnectTimeout=5', sshHost, encoded], {
|
|
1034
|
+
const child = spawn('ssh', ['-o', 'ConnectTimeout=5', sshHost, encoded], { // NOSONAR
|
|
1035
1035
|
stdio: 'ignore',
|
|
1036
1036
|
detached: true,
|
|
1037
1037
|
});
|
|
@@ -1361,7 +1361,7 @@ export function createReceiverTab(screen, services) {
|
|
|
1361
1361
|
}
|
|
1362
1362
|
_refreshCachedInfo();
|
|
1363
1363
|
const text = _buildDetailedInstructions(RECEIVER_ALIAS, RECEIVER_SCRIPT, _networkInfo)
|
|
1364
|
-
.replace(/\{[^}]*\}/g, '')
|
|
1364
|
+
.replace(/\{[^}]*\}/g, '') // NOSONAR
|
|
1365
1365
|
// eslint-disable-next-line no-control-regex
|
|
1366
1366
|
.replace(/\x1b\[[0-9;]*m/g, '');
|
|
1367
1367
|
// Try platform-appropriate clipboard command
|
|
@@ -1374,7 +1374,7 @@ export function createReceiverTab(screen, services) {
|
|
|
1374
1374
|
mkdirSync(AGENTVIBES_DIR, { recursive: true });
|
|
1375
1375
|
writeFileSync(tmpFile, '\ufeff' + text, 'utf-8');
|
|
1376
1376
|
const psCmd = `Get-Content -Path "${tmpFile.replace(/\\/g, '/')}" -Encoding UTF8 -Raw | Set-Clipboard; Remove-Item "${tmpFile.replace(/\\/g, '/')}"`;
|
|
1377
|
-
const r = spawnSync('powershell', ['-NoProfile', '-Command', psCmd], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1377
|
+
const r = spawnSync('powershell', ['-NoProfile', '-Command', psCmd], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }); // NOSONAR
|
|
1378
1378
|
if (r.status === 0) {
|
|
1379
1379
|
_showFeedback('{green-fg}Copied to clipboard!{/green-fg}');
|
|
1380
1380
|
copied = true;
|
|
@@ -1413,7 +1413,7 @@ export function createReceiverTab(screen, services) {
|
|
|
1413
1413
|
// List available audio sinks and let user pick one
|
|
1414
1414
|
let sinks;
|
|
1415
1415
|
try {
|
|
1416
|
-
const out = execSync('pactl --server=tcp:127.0.0.1:34567 list sinks short 2>/dev/null || pactl list sinks short 2>/dev/null', { timeout: 5000 }).toString().trim();
|
|
1416
|
+
const out = execSync('pactl --server=tcp:127.0.0.1:34567 list sinks short 2>/dev/null || pactl list sinks short 2>/dev/null', { timeout: 5000 }).toString().trim(); // NOSONAR
|
|
1417
1417
|
sinks = out.split('\n').filter(l => l.length > 0).map(line => {
|
|
1418
1418
|
const parts = line.split('\t');
|
|
1419
1419
|
return { id: parts[0], name: parts[1] || '', driver: parts[2] || '', state: parts[4] || '' };
|
|
@@ -645,7 +645,7 @@ export function createSettingsTab(screen, services) {
|
|
|
645
645
|
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
|
|
646
646
|
_killVP();
|
|
647
647
|
|
|
648
|
-
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
648
|
+
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)]; // NOSONAR
|
|
649
649
|
const playTtsScript = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
|
|
650
650
|
if (!fs.existsSync(playTtsScript)) return;
|
|
651
651
|
|
|
@@ -653,7 +653,7 @@ export function createSettingsTab(screen, services) {
|
|
|
653
653
|
const args = [playTtsScript, phrase, voiceId];
|
|
654
654
|
if (remoteLlm) args.push('--llm', remoteLlm);
|
|
655
655
|
|
|
656
|
-
_previewProc = spawn('bash', args, {
|
|
656
|
+
_previewProc = spawn('bash', args, { // NOSONAR
|
|
657
657
|
stdio: 'ignore',
|
|
658
658
|
detached: true,
|
|
659
659
|
env: _spawnEnv,
|