agentvibes 5.1.4 → 5.2.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 (32) hide show
  1. package/.agentvibes/config.json +23 -13
  2. package/.claude/commands/agent-vibes/verbosity.md +98 -89
  3. package/.claude/config/audio-effects.cfg +4 -1
  4. package/.claude/hooks/bmad-speak.sh +2 -2
  5. package/.claude/hooks/piper-download-voices.sh +233 -225
  6. package/.claude/hooks/piper-installer.sh +1 -1
  7. package/.claude/hooks/piper-voice-manager.sh +125 -0
  8. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +97 -90
  9. package/.claude/hooks/play-tts-enhanced.sh +1 -1
  10. package/.claude/hooks/play-tts-piper.sh +16 -5
  11. package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
  12. package/.claude/hooks/play-tts.sh +21 -9
  13. package/.claude/hooks/session-start-tts.sh +4 -1
  14. package/.claude/hooks/stop-tts.sh +1 -1
  15. package/.claude/hooks/verbosity-manager.sh +185 -178
  16. package/.claude/hooks-windows/download-extra-voices.ps1 +243 -185
  17. package/.claude/hooks-windows/play-tts-piper.ps1 +7 -2
  18. package/.claude/hooks-windows/play-tts.ps1 +7 -1
  19. package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
  20. package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
  21. package/README.md +10 -2
  22. package/RELEASE_NOTES.md +36 -0
  23. package/bin/agentvibes-voice-browser.js +1939 -1840
  24. package/mcp-server/server.py +52 -9
  25. package/package.json +1 -1
  26. package/src/console/tabs/receiver-tab.js +1527 -1483
  27. package/src/console/tabs/settings-tab.js +2 -2
  28. package/src/console/tabs/setup-tab.js +53 -8
  29. package/src/console/tabs/voices-tab.js +130 -13
  30. package/src/i18n/en.js +202 -202
  31. package/src/services/verbosity-service.js +159 -157
  32. package/templates/agentvibes-receiver.sh +3 -2
@@ -64,7 +64,7 @@ const FOOTER_TEXT =
64
64
  '[↑↓] Navigate [Enter] Edit [Esc] Tab Bar';
65
65
 
66
66
  const MUSIC_DEFAULTS = Object.freeze({ enabled: false, track: 'agentvibes_soft_flamenco_loop.mp3', volume: 20 });
67
- const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', minimal: 'Minimal', custom: 'Custom' });
67
+ const VERBOSITY_LABELS = Object.freeze({ high: 'High', medium: 'Medium', low: 'Low', caveman: 'Caveman', minimal: 'Minimal', custom: 'Custom' });
68
68
 
69
69
  // ---------------------------------------------------------------------------
70
70
  // Exported format helpers (pure functions — used by tests and UI)
@@ -740,7 +740,7 @@ export function createSettingsTab(screen, services) {
740
740
  function _editVerbosity() {
741
741
  navigationService?.openModal();
742
742
 
743
- const levels = ['high', 'medium', 'low'];
743
+ const levels = ['high', 'medium', 'low', 'caveman'];
744
744
  const modal = blessed.list({
745
745
  parent: screen,
746
746
  top: 'center',
@@ -38,7 +38,7 @@ import { openReverbPicker, REVERB_PRESETS } from '../widgets/reverb-picker.js';
38
38
  import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
39
39
  import { formatTrackName } from '../widgets/format-utils.js';
40
40
  import { destroyList } from '../widgets/destroy-list.js';
41
- import { scanInstalledVoices, getVoiceMeta, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, toggleFavorite } from './voices-tab.js';
41
+ import { scanInstalledVoices, getVoiceMeta, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, getThumbsDown, toggleFavorite, toggleThumbsUp, toggleThumbsDown } from './voices-tab.js';
42
42
  import { attachBtnBlink } from './agents-tab.js';
43
43
  import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
44
44
  import { spawn } from 'node:child_process';
@@ -889,7 +889,7 @@ export function createSetupTab(screen, services) {
889
889
  const hooksSubdir = process.platform === 'win32' ? 'hooks-windows' : 'hooks';
890
890
  const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
891
891
  // Don't include pretext — play-tts already prepends it from the config
892
- const sampleText = 'Here is a preview of your audio settings.';
892
+ const sampleText = 'This is how your audio settings sound right now.';
893
893
 
894
894
  let cmd, args;
895
895
  if (isWin) {
@@ -1226,18 +1226,20 @@ export function createSetupTab(screen, services) {
1226
1226
 
1227
1227
  blessed.text({
1228
1228
  parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
1229
- content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [Home/End] [a-z] Jump [Enter] Select [Space] Preview [*] Fav [Esc] Cancel{/white-fg}',
1229
+ content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [a-z] Jump [Enter] Select [Space] Preview [+] 👍 [-] 👎 [Esc] Cancel{/white-fg}',
1230
1230
  style: { bg: COLORS.contentBg },
1231
1231
  });
1232
1232
 
1233
1233
  function _buildVoiceItems(voices) {
1234
1234
  const favs = getFavorites(configService);
1235
+ const td = getThumbsDown(configService);
1235
1236
  return voices.map(v => {
1236
1237
  const isActive = v === draft.voice;
1237
1238
  const isPrev = v === _previewVoiceId;
1238
- const isFav = favs.includes(v);
1239
+ const isUp = favs.includes(v);
1240
+ const isDown = td.includes(v);
1239
1241
  const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
1240
- const star = isFav ? '{yellow-fg}{/yellow-fg}' : ' ';
1242
+ const star = isUp ? '{green-fg}👍{/green-fg}' : (isDown ? '{red-fg}👎{/red-fg}' : ' ');
1241
1243
  const meta = getVoiceMeta(v);
1242
1244
  const name = meta.displayName.length > COL_N
1243
1245
  ? meta.displayName.slice(0, COL_N - 1) + '…'
@@ -1266,13 +1268,52 @@ export function createSetupTab(screen, services) {
1266
1268
  if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
1267
1269
  _killVP();
1268
1270
 
1271
+ const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
1272
+
1273
+ // Route through remote provider if active
1274
+ const _remoteProviders = ['ssh-remote', 'agentvibes-receiver'];
1275
+ let _activeProvider = '';
1276
+ try {
1277
+ const _projectRoot = path.resolve(__dirname, '..', '..');
1278
+ const _provPaths = [
1279
+ path.join(_projectRoot, '.claude', 'tts-provider.txt'),
1280
+ path.join(os.homedir(), '.claude', 'tts-provider.txt'),
1281
+ ];
1282
+ for (const p of _provPaths) {
1283
+ if (fs.existsSync(p)) { _activeProvider = fs.readFileSync(p, 'utf8').trim(); break; }
1284
+ }
1285
+ } catch {}
1286
+
1287
+ if (_remoteProviders.includes(_activeProvider)) {
1288
+ const _projectRoot = path.resolve(__dirname, '..', '..');
1289
+ let rProc;
1290
+ if (_isWin) {
1291
+ const _playTts = path.join(_projectRoot, '.claude', 'hooks-windows', 'play-tts.ps1');
1292
+ rProc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', _playTts, phrase, voiceId], {
1293
+ stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv,
1294
+ });
1295
+ } else {
1296
+ const _playTts = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
1297
+ rProc = spawn('bash', [_playTts, phrase, voiceId], {
1298
+ stdio: 'ignore', detached: true, env: _spawnEnv,
1299
+ });
1300
+ }
1301
+ _previewProc = rProc;
1302
+ _previewVoiceId = voiceId;
1303
+ if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing (remote): ${voiceId}{/cyan-fg}`); screen.render(); }
1304
+ rProc.on('exit', () => {
1305
+ if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
1306
+ });
1307
+ rProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
1308
+ return;
1309
+ }
1310
+
1269
1311
  const _ms = parseMultiSpeaker(voiceId);
1270
1312
  const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
1271
1313
  const safeBase = path.resolve(PIPER_VOICES_DIR);
1272
1314
  if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
1273
1315
 
1274
1316
  const tempWav = _secureTempWav('vp');
1275
- const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
1276
1317
 
1277
1318
  let _piperBin = 'piper';
1278
1319
  if (_isWin) {
@@ -1331,9 +1372,13 @@ export function createSetupTab(screen, services) {
1331
1372
  const sel = _allVoices[vpList.selected];
1332
1373
  if (sel) _previewVoice(sel);
1333
1374
  });
1334
- vpList.key(['*'], () => {
1375
+ vpList.key(['*', '+'], () => {
1376
+ const sel = _allVoices[vpList.selected];
1377
+ if (sel) { toggleThumbsUp(configService, sel); _refreshVP(); }
1378
+ });
1379
+ vpList.key(['-'], () => {
1335
1380
  const sel = _allVoices[vpList.selected];
1336
- if (sel) { toggleFavorite(configService, sel); _refreshVP(); }
1381
+ if (sel) { toggleThumbsDown(configService, sel); _refreshVP(); }
1337
1382
  });
1338
1383
  vpList.key(['escape', 'q'], _closeVP);
1339
1384
 
@@ -471,29 +471,85 @@ export function scanInstalledVoices() {
471
471
  }
472
472
 
473
473
  /**
474
- * Get favorites array from config.
474
+ * Get favorites (thumbs-up) array from config.
475
475
  * @param {object} configService
476
476
  * @returns {string[]}
477
477
  */
478
478
  export function getFavorites(configService) {
479
- const favs = configService.getConfig().favorites;
479
+ const cfg = configService.getConfig();
480
+ // Prefer thumbsUp if present, fall back to legacy favorites
481
+ const favs = cfg.thumbsUp ?? cfg.favorites;
480
482
  return Array.isArray(favs) ? favs : [];
481
483
  }
482
484
 
483
485
  /**
484
- * Toggle a voice in the favorites list.
486
+ * Get thumbs-down array from config.
487
+ * @param {object} configService
488
+ * @returns {string[]}
489
+ */
490
+ export function getThumbsDown(configService) {
491
+ const td = configService.getConfig().thumbsDown;
492
+ return Array.isArray(td) ? td : [];
493
+ }
494
+
495
+ /**
496
+ * Toggle thumbs-up on a voice. Clears thumbs-down if set.
485
497
  * @param {object} configService
486
498
  * @param {string} voiceId
499
+ * @returns {'added'|'removed'}
487
500
  */
488
- export function toggleFavorite(configService, voiceId) {
501
+ export function toggleThumbsUp(configService, voiceId) {
489
502
  const favs = getFavorites(configService);
490
503
  const idx = favs.indexOf(voiceId);
504
+ let result;
491
505
  if (idx >= 0) {
492
506
  favs.splice(idx, 1);
507
+ result = 'removed';
493
508
  } else {
494
509
  favs.push(voiceId);
510
+ // Remove from thumbs-down
511
+ const td = getThumbsDown(configService);
512
+ const tdIdx = td.indexOf(voiceId);
513
+ if (tdIdx >= 0) { td.splice(tdIdx, 1); configService.set('thumbsDown', td); }
514
+ result = 'added';
495
515
  }
496
- configService.set('favorites', favs);
516
+ configService.set('thumbsUp', favs);
517
+ configService.set('favorites', favs); // backward compat
518
+ return result;
519
+ }
520
+
521
+ /**
522
+ * Toggle thumbs-down on a voice. Clears thumbs-up if set.
523
+ * @param {object} configService
524
+ * @param {string} voiceId
525
+ * @returns {'added'|'removed'}
526
+ */
527
+ export function toggleThumbsDown(configService, voiceId) {
528
+ const td = getThumbsDown(configService);
529
+ const idx = td.indexOf(voiceId);
530
+ let result;
531
+ if (idx >= 0) {
532
+ td.splice(idx, 1);
533
+ result = 'removed';
534
+ } else {
535
+ td.push(voiceId);
536
+ // Remove from thumbs-up / favorites
537
+ const favs = getFavorites(configService);
538
+ const fIdx = favs.indexOf(voiceId);
539
+ if (fIdx >= 0) { favs.splice(fIdx, 1); configService.set('thumbsUp', favs); configService.set('favorites', favs); }
540
+ result = 'added';
541
+ }
542
+ configService.set('thumbsDown', td);
543
+ return result;
544
+ }
545
+
546
+ /**
547
+ * Toggle a voice in the favorites list (legacy compat — calls toggleThumbsUp).
548
+ * @param {object} configService
549
+ * @param {string} voiceId
550
+ */
551
+ export function toggleFavorite(configService, voiceId) {
552
+ toggleThumbsUp(configService, voiceId);
497
553
  }
498
554
 
499
555
  // ---------------------------------------------------------------------------
@@ -708,7 +764,7 @@ export function createVoicesTab(screen, services) {
708
764
 
709
765
  // -------------------------------------------------------------------------
710
766
  // Hint text shown in previewLine when the list has focus and nothing is playing
711
- const HINT_TEXT = '{white-fg}[Space] preview [Enter] select as default voice{/white-fg}';
767
+ const HINT_TEXT = '{white-fg}[Space] preview [Enter] select [+] thumbs up [-] thumbs down{/white-fg}';
712
768
  let _listFocused = false;
713
769
 
714
770
  // Inline selection hint appended to the currently highlighted voice row.
@@ -811,6 +867,55 @@ export function createVoicesTab(screen, services) {
811
867
  _killPlayingProcess();
812
868
  _playingVoiceId = null;
813
869
 
870
+ // Check if we should route through remote provider (ssh-remote / agentvibes-receiver)
871
+ const projectRoot = path.resolve(__dirname, '..', '..');
872
+ const remoteProviders = ['ssh-remote', 'agentvibes-receiver'];
873
+ let activeProvider = '';
874
+ try {
875
+ const providerPaths = [
876
+ path.join(projectRoot, '.claude', 'tts-provider.txt'),
877
+ path.join(os.homedir(), '.claude', 'tts-provider.txt'),
878
+ ];
879
+ for (const p of providerPaths) {
880
+ if (fs.existsSync(p)) { activeProvider = fs.readFileSync(p, 'utf8').trim(); break; }
881
+ }
882
+ } catch {}
883
+
884
+ if (remoteProviders.includes(activeProvider)) {
885
+ const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
886
+ const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
887
+ let proc;
888
+ if (isWindows) {
889
+ const playTts = path.join(projectRoot, '.claude', 'hooks-windows', 'play-tts.ps1');
890
+ proc = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase, voiceId], {
891
+ stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv,
892
+ });
893
+ } else {
894
+ const playTts = path.join(projectRoot, '.claude', 'hooks', 'play-tts.sh');
895
+ proc = spawn('bash', [playTts, phrase, voiceId], {
896
+ stdio: 'ignore', detached: true, env: _spawnEnv,
897
+ });
898
+ }
899
+ _playingProcess = proc;
900
+ _playingVoiceId = voiceId;
901
+ previewLine.setContent(`{${COLORS.activeFg}-fg}♪ Playing (remote): ${voiceId}{/${COLORS.activeFg}-fg}`);
902
+ screen.render();
903
+ proc.on('exit', () => {
904
+ if (_playingVoiceId === voiceId) {
905
+ _playingVoiceId = null; _playingProcess = null;
906
+ previewLine.setContent(_listFocused ? HINT_TEXT : '');
907
+ refreshDisplay();
908
+ }
909
+ });
910
+ proc.on('error', () => {
911
+ _playingVoiceId = null; _playingProcess = null;
912
+ previewLine.setContent(`{red-fg}Remote preview failed{/red-fg}`);
913
+ screen.render();
914
+ setTimeout(() => { previewLine.setContent(_listFocused ? HINT_TEXT : ''); screen.render(); }, 4000);
915
+ });
916
+ return;
917
+ }
918
+
814
919
  // Resolve model path (may be multi-speaker)
815
920
  const ms = parseMultiSpeaker(voiceId);
816
921
  const voicePath = path.resolve(PIPER_VOICES_DIR, ms.model + '.onnx');
@@ -1435,13 +1540,14 @@ export function createVoicesTab(screen, services) {
1435
1540
  return _installedSet.has(voiceId);
1436
1541
  }
1437
1542
 
1438
- function _buildListItems(voices, active, favorites) {
1543
+ function _buildListItems(voices, active, favorites, thumbsDown) {
1439
1544
  return voices.map(v => {
1440
1545
  const installed = _isInstalled(v);
1441
- const isFav = favorites.includes(v);
1546
+ const isUp = favorites.includes(v);
1547
+ const isDown = thumbsDown.includes(v);
1442
1548
  const isActive = v === active;
1443
1549
  const isPrev = v === _playingVoiceId;
1444
- const star = isFav ? '' : ' ';
1550
+ const star = isUp ? '{green-fg}👍{/green-fg}' : (isDown ? '{red-fg}👎{/red-fg}' : ' ');
1445
1551
  const dot = isPrev ? '♪' : (isActive ? '{green-fg}✓{/green-fg}' : ' ');
1446
1552
 
1447
1553
  let displayName, gender, provider;
@@ -1531,8 +1637,9 @@ export function createVoicesTab(screen, services) {
1531
1637
 
1532
1638
  const active = providerService.getActiveVoiceId();
1533
1639
  const favorites = getFavorites(configService);
1640
+ const thumbsDown = getThumbsDown(configService);
1534
1641
  const filtered = _getFilteredVoices();
1535
- const items = _buildListItems(filtered, active, favorites);
1642
+ const items = _buildListItems(filtered, active, favorites, thumbsDown);
1536
1643
 
1537
1644
  voiceList.setItems(items.length > 0 ? items : [' (no voices found — install piper first)']);
1538
1645
  const maxIdx = Math.max(0, (items.length > 0 ? items.length : 1) - 1);
@@ -1620,12 +1727,22 @@ export function createVoicesTab(screen, services) {
1620
1727
  if (typeof focusMainTabBar === 'function') { focusMainTabBar(); screen.render(); }
1621
1728
  });
1622
1729
 
1623
- // 'f' or '*' in voiceList toggles favorite
1624
- voiceList.key(['*'], () => {
1730
+ // '*' or '+' in voiceList toggles thumbs-up
1731
+ voiceList.key(['*', '+'], () => {
1625
1732
  const voices = _getFilteredVoices();
1626
1733
  const selected = voices[voiceList.selected];
1627
1734
  if (selected) {
1628
- toggleFavorite(configService, selected);
1735
+ toggleThumbsUp(configService, selected);
1736
+ refreshDisplay();
1737
+ }
1738
+ });
1739
+
1740
+ // '-' in voiceList toggles thumbs-down
1741
+ voiceList.key(['-'], () => {
1742
+ const voices = _getFilteredVoices();
1743
+ const selected = voices[voiceList.selected];
1744
+ if (selected) {
1745
+ toggleThumbsDown(configService, selected);
1629
1746
  refreshDisplay();
1630
1747
  }
1631
1748
  });