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.
Files changed (70) hide show
  1. package/.agentvibes/config.json +17 -12
  2. package/.agentvibes/install-manifest.json +98 -86
  3. package/.claude/audio/ui/CREDITS.txt +16 -0
  4. package/.claude/audio/ui/bling-success.wav +0 -0
  5. package/.claude/commands/agent-vibes/receiver.md +64 -0
  6. package/.claude/config/audio-effects.cfg +3 -3
  7. package/.claude/config/personality.txt +1 -0
  8. package/.claude/config/reverb-level.txt +1 -1
  9. package/.claude/github-star-reminder.txt +1 -1
  10. package/.claude/hooks/audio-processor.sh +57 -18
  11. package/.claude/hooks/kokoro-installer.sh +117 -0
  12. package/.claude/hooks/kokoro-server.py +219 -0
  13. package/.claude/hooks/kokoro-tts.py +141 -0
  14. package/.claude/hooks/piper-download-voices.sh +39 -16
  15. package/.claude/hooks/piper-installer.sh +11 -6
  16. package/.claude/hooks/piper-voice-manager.sh +6 -6
  17. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
  18. package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
  19. package/.claude/hooks/play-tts-kokoro.sh +127 -0
  20. package/.claude/hooks/play-tts-piper.sh +20 -13
  21. package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
  22. package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
  23. package/.claude/hooks/play-tts.sh +78 -2
  24. package/.claude/hooks/provider-commands.sh +71 -22
  25. package/.claude/hooks/provider-manager.sh +15 -7
  26. package/.claude/hooks/voice-manager.sh +6 -0
  27. package/.claude/hooks-windows/kokoro-server.py +219 -0
  28. package/.claude/hooks-windows/kokoro-tts.py +107 -0
  29. package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
  30. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +22 -16
  31. package/.claude/hooks-windows/play-tts.ps1 +29 -1
  32. package/README.md +12 -86
  33. package/RELEASE_NOTES.md +60 -0
  34. package/mcp-server/server.py +17 -7
  35. package/package.json +4 -3
  36. package/src/commands/install-mcp.js +730 -476
  37. package/src/console/app.js +28 -12
  38. package/src/console/audio-env.js +85 -1
  39. package/src/console/navigation.js +4 -0
  40. package/src/console/tabs/agents-tab.js +18 -12
  41. package/src/console/tabs/music-tab.js +7 -25
  42. package/src/console/tabs/receiver-tab.js +13 -13
  43. package/src/console/tabs/settings-tab.js +2 -2
  44. package/src/console/tabs/setup-tab.js +1708 -124
  45. package/src/console/tabs/voices-tab.js +76 -92
  46. package/src/console/widgets/format-utils.js +14 -2
  47. package/src/console/widgets/help-bar.js +55 -0
  48. package/src/console/widgets/personality-picker.js +2 -2
  49. package/src/console/widgets/reverb-picker.js +429 -41
  50. package/src/console/widgets/track-picker.js +60 -51
  51. package/src/i18n/de.js +204 -203
  52. package/src/i18n/en.js +1 -0
  53. package/src/i18n/es.js +204 -203
  54. package/src/i18n/fr.js +204 -203
  55. package/src/i18n/hi.js +204 -203
  56. package/src/i18n/ja.js +204 -203
  57. package/src/i18n/ko.js +204 -203
  58. package/src/i18n/pt.js +204 -203
  59. package/src/i18n/strings.js +54 -54
  60. package/src/i18n/zh-CN.js +204 -203
  61. package/src/installer.js +127 -34
  62. package/src/services/provider-service.js +178 -143
  63. package/src/services/provider-voice-catalog.js +126 -0
  64. package/src/services/tts-engine-service.js +53 -4
  65. package/src/utils/audio-duration-validator.js +341 -298
  66. package/src/utils/list-formatter.js +200 -194
  67. package/src/utils/platform-resolver.js +369 -0
  68. package/src/utils/preview-list-prompt.js +8 -0
  69. package/src/utils/provider-validator.js +79 -9
  70. package/templates/agentvibes-receiver.sh +6 -2
@@ -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: _has('piper'),
635
- soprano: _has('soprano'),
636
- sapi: process.platform === 'win32',
637
- macos: process.platform === 'darwin' && _has('say'),
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 ? on('Piper') : off('Piper'),
645
- detected.soprano ? on('Soprano') : off('Soprano'),
646
- detected.sapi ? on('Windows SAPI') : off('Windows SAPI'),
647
- detected.macos ? on('Mac Say') : off('Mac Say'),
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({
@@ -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: 'Reverb', getValue: () => formatReverbState(draft.reverbPreset) },
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
- const installed = scanInstalledVoices();
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
- // Separate voices by gender
1501
- const femaleVoices = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Female'));
1502
- const maleVoices = _shuffleArray(installed.filter(v => getVoiceMeta(v).gender === 'Male'));
1503
- const otherVoices = _shuffleArray(installed.filter(v => !['Male', 'Female'].includes(getVoiceMeta(v).gender)));
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 { spawn } from 'node:child_process';
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
- const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
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
- if (!_detectedPlayer) {
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
- const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
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
- _playingProcess.on('exit', () => {
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
- _playingProcess.on('error', () => {
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,