agentvibes 5.7.6 → 5.9.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 (137) hide show
  1. package/.agentvibes/config.json +12 -5
  2. package/.agentvibes/install-manifest.json +188 -300
  3. package/.claude/audio/tracks/celestial_velvet.mp3 +0 -0
  4. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  5. package/.claude/commands/agent-vibes-rdp.md +24 -24
  6. package/.claude/config/audio-effects.cfg +3 -2
  7. package/.claude/config/audio-effects.cfg.sample +52 -52
  8. package/.claude/config/background-music-enabled.txt +1 -0
  9. package/.claude/config/background-music-position.txt +1 -1
  10. package/.claude/config/language.txt +1 -0
  11. package/.claude/docs/TERMUX_SETUP.md +408 -408
  12. package/.claude/hooks/audio-cache-utils.sh +0 -0
  13. package/.claude/hooks/audio-processor.sh +0 -0
  14. package/.claude/hooks/background-music-manager.sh +0 -0
  15. package/.claude/hooks/bmad-party-speak.sh +27 -6
  16. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  17. package/.claude/hooks/bmad-speak.sh +0 -0
  18. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  19. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  20. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  21. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  22. package/.claude/hooks/clean-audio-cache.sh +0 -0
  23. package/.claude/hooks/cleanup-cache.sh +0 -0
  24. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  25. package/.claude/hooks/download-extra-voices.sh +0 -0
  26. package/.claude/hooks/effects-manager.sh +0 -0
  27. package/.claude/hooks/github-star-reminder.sh +0 -0
  28. package/.claude/hooks/language-manager.sh +0 -0
  29. package/.claude/hooks/learn-manager.sh +0 -0
  30. package/.claude/hooks/macos-voice-manager.sh +0 -0
  31. package/.claude/hooks/migrate-background-music.sh +0 -0
  32. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  33. package/.claude/hooks/optimize-background-music.sh +0 -0
  34. package/.claude/hooks/path-resolver.sh +0 -0
  35. package/.claude/hooks/personality-manager.sh +0 -0
  36. package/.claude/hooks/piper-download-voices.sh +0 -0
  37. package/.claude/hooks/piper-installer.sh +0 -0
  38. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  39. package/.claude/hooks/piper-voice-manager.sh +0 -0
  40. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
  41. package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
  42. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  43. package/.claude/hooks/play-tts-macos.sh +0 -0
  44. package/.claude/hooks/play-tts-piper.sh +0 -0
  45. package/.claude/hooks/play-tts-soprano.sh +0 -0
  46. package/.claude/hooks/play-tts-ssh-remote.sh +11 -8
  47. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  48. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  49. package/.claude/hooks/play-tts.sh +0 -0
  50. package/.claude/hooks/prepare-release.sh +0 -0
  51. package/.claude/hooks/provider-commands.sh +0 -0
  52. package/.claude/hooks/provider-manager.sh +0 -0
  53. package/.claude/hooks/replay-target-audio.sh +0 -0
  54. package/.claude/hooks/requirements.txt +6 -6
  55. package/.claude/hooks/sentiment-manager.sh +0 -0
  56. package/.claude/hooks/session-start-tts.sh +0 -0
  57. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  58. package/.claude/hooks/speed-manager.sh +0 -0
  59. package/.claude/hooks/stop-tts.sh +0 -0
  60. package/.claude/hooks/termux-installer.sh +0 -0
  61. package/.claude/hooks/translate-manager.sh +0 -0
  62. package/.claude/hooks/translator.py +237 -237
  63. package/.claude/hooks/tts-queue-worker.sh +0 -0
  64. package/.claude/hooks/tts-queue.sh +0 -0
  65. package/.claude/hooks/verbosity-manager.sh +0 -0
  66. package/.claude/hooks/voice-manager.sh +0 -0
  67. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  68. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  69. package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
  70. package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
  71. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  72. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  73. package/.claude/piper-voices-dir.txt +1 -1
  74. package/.claude/verbosity.txt +1 -1
  75. package/.clawdbot/README.md +105 -105
  76. package/.mcp.json +5 -14
  77. package/README.md +43 -2
  78. package/RELEASE_NOTES.md +110 -0
  79. package/WINDOWS-SETUP.md +208 -208
  80. package/bin/agent-vibes +39 -39
  81. package/bin/agentvibes-voice-browser.js +0 -0
  82. package/bin/agentvibes.js +0 -0
  83. package/bin/mcp-server.js +121 -121
  84. package/bin/mcp-server.sh +0 -0
  85. package/bin/test-bmad-pr +78 -78
  86. package/mcp-server/QUICK_START.md +203 -203
  87. package/mcp-server/README.md +345 -345
  88. package/mcp-server/WINDOWS_SETUP.md +0 -0
  89. package/mcp-server/examples/claude_desktop_config.json +11 -11
  90. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  91. package/mcp-server/examples/custom_instructions.md +169 -169
  92. package/mcp-server/install-deps.js +0 -0
  93. package/mcp-server/server.py +1797 -1797
  94. package/mcp-server/test_server.py +0 -0
  95. package/package.json +1 -1
  96. package/src/cli/list-personalities.js +110 -110
  97. package/src/cli/list-voices.js +114 -114
  98. package/src/commands/bmad-voices.js +394 -394
  99. package/src/commands/install-mcp.js +476 -476
  100. package/src/console/audio-env.js +4 -1
  101. package/src/console/brand-colors.js +13 -13
  102. package/src/console/constants/personalities.js +44 -44
  103. package/src/console/tabs/agents-tab.js +85 -62
  104. package/src/console/tabs/help-tab.js +314 -314
  105. package/src/console/tabs/music-tab.js +3 -0
  106. package/src/console/tabs/readme-tab.js +272 -272
  107. package/src/console/tabs/setup-tab.js +285 -41
  108. package/src/console/tabs/voices-tab.js +14 -2
  109. package/src/console/widgets/destroy-list.js +25 -25
  110. package/src/console/widgets/notice.js +55 -55
  111. package/src/i18n/de.js +202 -202
  112. package/src/i18n/es.js +202 -202
  113. package/src/i18n/fr.js +202 -202
  114. package/src/i18n/hi.js +202 -202
  115. package/src/i18n/ja.js +202 -202
  116. package/src/i18n/ko.js +202 -202
  117. package/src/i18n/pt.js +202 -202
  118. package/src/i18n/strings.js +54 -54
  119. package/src/i18n/zh-CN.js +202 -202
  120. package/src/installer/language-screen.js +31 -31
  121. package/src/installer/music-file-input.js +304 -304
  122. package/src/installer.js +0 -0
  123. package/src/services/config-service.js +264 -264
  124. package/src/services/language-service.js +47 -47
  125. package/src/services/provider-service.js +143 -143
  126. package/src/utils/audio-duration-validator.js +298 -298
  127. package/src/utils/audio-format-validator.js +277 -277
  128. package/src/utils/dependency-checker.js +469 -469
  129. package/src/utils/file-ownership-verifier.js +358 -358
  130. package/src/utils/list-formatter.js +194 -194
  131. package/src/utils/music-file-validator.js +285 -285
  132. package/src/utils/preview-list-prompt.js +136 -136
  133. package/src/utils/secure-music-storage.js +412 -412
  134. package/templates/agentvibes-receiver.sh +231 -231
  135. package/templates/audio/welcome-music.mp3 +0 -0
  136. package/.claude/hooks/bmad-party-manager.sh +0 -225
  137. package/.claude/hooks/stop.sh +0 -38
@@ -121,7 +121,10 @@ function _detect(players, env) {
121
121
  * @returns {string|null}
122
122
  */
123
123
  export function detectRemoteLlm() {
124
- const cfgPath = path.join(os.homedir(), '.agentvibes', 'transport-config.json');
124
+ // process.env.HOME takes priority over os.homedir() so tests can inject a fake home on all platforms.
125
+ // On Windows production use, HOME is typically unset, so os.homedir() (reads USERPROFILE) is the fallback.
126
+ const homeDir = process.env.HOME ?? os.homedir();
127
+ const cfgPath = path.join(homeDir, '.agentvibes', 'transport-config.json');
125
128
  if (!fs.existsSync(cfgPath)) return null;
126
129
  try {
127
130
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
@@ -1,13 +1,13 @@
1
- /**
2
- * AgentVibes TUI — Brand Color Constants
3
- *
4
- * Single source of truth for the two primary brand colors.
5
- * Change BRAND_PINK or BRAND_BLUE here to update every modal title,
6
- * button, and the "Vibes" logotype across the entire TUI.
7
- */
8
-
9
- /** Magenta-pink used for modal titles and the "Vibes" logotype. */
10
- export const BRAND_PINK = '#f06292'; // Light magenta — Pink 300
11
-
12
- /** Indigo blue used for default button backgrounds and primary accents. */
13
- export const BRAND_BLUE = '#3949ab';
1
+ /**
2
+ * AgentVibes TUI — Brand Color Constants
3
+ *
4
+ * Single source of truth for the two primary brand colors.
5
+ * Change BRAND_PINK or BRAND_BLUE here to update every modal title,
6
+ * button, and the "Vibes" logotype across the entire TUI.
7
+ */
8
+
9
+ /** Magenta-pink used for modal titles and the "Vibes" logotype. */
10
+ export const BRAND_PINK = '#f06292'; // Light magenta — Pink 300
11
+
12
+ /** Indigo blue used for default button backgrounds and primary accents. */
13
+ export const BRAND_BLUE = '#3949ab';
@@ -1,44 +1,44 @@
1
- /**
2
- * AgentVibes — Canonical personality constants.
3
- *
4
- * Single source of truth for personality names and their associated emoji
5
- * glyphs. All TUI modules (settings-tab, agents-tab, personality-picker …)
6
- * import from here; src/installer.js maintains its own copy because it
7
- * predates the TUI and uses a different module-load path.
8
- *
9
- * Exported:
10
- * PERSONALITY_EMOJIS — Map of personality name → emoji string
11
- * PERSONALITIES — Ordered array of personality names (canonical order
12
- * used for picker lists)
13
- */
14
-
15
- export const PERSONALITY_EMOJIS = Object.freeze({
16
- angry: '😠',
17
- annoying: '😤',
18
- crass: '🤬',
19
- dramatic: '🎭',
20
- 'dry-humor': '😐',
21
- flirty: '😘',
22
- funny: '😂',
23
- grandpa: '👴',
24
- millennial: '🙄',
25
- moody: '😒',
26
- none: '😊',
27
- normal: '😊',
28
- pirate: '⚓',
29
- poetic: '📜',
30
- professional: '👔',
31
- rapper: '🎤',
32
- robot: '🤖',
33
- sarcastic: '😏',
34
- sassy: '💁',
35
- 'surfer-dude':'🏄',
36
- zen: '🧘',
37
- });
38
-
39
- export const PERSONALITIES = Object.freeze([
40
- 'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
41
- 'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
42
- 'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
43
- 'sassy', 'surfer-dude', 'zen',
44
- ]);
1
+ /**
2
+ * AgentVibes — Canonical personality constants.
3
+ *
4
+ * Single source of truth for personality names and their associated emoji
5
+ * glyphs. All TUI modules (settings-tab, agents-tab, personality-picker …)
6
+ * import from here; src/installer.js maintains its own copy because it
7
+ * predates the TUI and uses a different module-load path.
8
+ *
9
+ * Exported:
10
+ * PERSONALITY_EMOJIS — Map of personality name → emoji string
11
+ * PERSONALITIES — Ordered array of personality names (canonical order
12
+ * used for picker lists)
13
+ */
14
+
15
+ export const PERSONALITY_EMOJIS = Object.freeze({
16
+ angry: '😠',
17
+ annoying: '😤',
18
+ crass: '🤬',
19
+ dramatic: '🎭',
20
+ 'dry-humor': '😐',
21
+ flirty: '😘',
22
+ funny: '😂',
23
+ grandpa: '👴',
24
+ millennial: '🙄',
25
+ moody: '😒',
26
+ none: '😊',
27
+ normal: '😊',
28
+ pirate: '⚓',
29
+ poetic: '📜',
30
+ professional: '👔',
31
+ rapper: '🎤',
32
+ robot: '🤖',
33
+ sarcastic: '😏',
34
+ sassy: '💁',
35
+ 'surfer-dude':'🏄',
36
+ zen: '🧘',
37
+ });
38
+
39
+ export const PERSONALITIES = Object.freeze([
40
+ 'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
41
+ 'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
42
+ 'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
43
+ 'sassy', 'surfer-dude', 'zen',
44
+ ]);
@@ -17,6 +17,7 @@ import { formatReverbState, formatTrackName, formatVoiceName } from '../widgets/
17
17
  import {
18
18
  PIPER_VOICES_DIR, SAMPLE_PHRASES,
19
19
  parseMultiSpeaker, scanInstalledVoices, getVoiceMeta, genderIconTag,
20
+ getFavorites, getThumbsDown, toggleThumbsUp, toggleThumbsDown,
20
21
  } from './voices-tab.js';
21
22
  import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
22
23
  import { destroyList } from '../widgets/destroy-list.js';
@@ -153,7 +154,7 @@ const COL_MUSIC = 11;
153
154
  const COL_VOL = 5; // e.g. "70%" or "100%"
154
155
 
155
156
  // Inline hint appended to the selected row when list is focused
156
- const _ROW_HINT_BMAD = ` {bright-black-fg}[Space] Preview [Enter] Configure{/bright-black-fg}`;
157
+ const _ROW_HINT_BMAD = ` {white-fg}[Space] Preview [Enter] Configure{/white-fg}`;
157
158
 
158
159
  // ---------------------------------------------------------------------------
159
160
 
@@ -694,7 +695,7 @@ ${_tl('bmadDesc')}
694
695
  left: 2,
695
696
  right: 2,
696
697
  tags: true,
697
- content: '{white-fg}[↑↓] Navigate [Enter] Edit [Tab] → Preview/Save [Esc] Cancel{/white-fg}',
698
+ content: '{white-fg}[↑↓] Navigate [Enter] Edit [Tab] → Buttons [Esc] Close{/white-fg}',
698
699
  style: { bg: COLORS.contentBg },
699
700
  });
700
701
 
@@ -754,10 +755,12 @@ ${_tl('bmadDesc')}
754
755
  refreshDisplay();
755
756
  });
756
757
 
757
- const closeBtn = _modalBtn('Cancel', 42, _closeModal);
758
+ const saveBtn = _modalBtn('Save', 41, () => { try { _autoSaveAgent(); } catch {} _closeModal(); });
759
+
760
+ const closeBtn = _modalBtn('Close', 50, _closeModal);
758
761
 
759
762
  // Blinking █ cursor + preview spinner — reusable across all modal buttons
760
- const btnBlink = attachBtnBlink([previewBtn, resetBtn, closeBtn], screen);
763
+ const btnBlink = attachBtnBlink([previewBtn, resetBtn, saveBtn, closeBtn], screen);
761
764
 
762
765
  function _closeModal() {
763
766
  if (_closed) return;
@@ -868,6 +871,7 @@ ${_tl('bmadDesc')}
868
871
  fieldList.key(['escape', 'q', 'Q'], _closeModal);
869
872
  previewBtn.key(['escape', 'q', 'Q'], _closeModal);
870
873
  resetBtn.key(['escape', 'q', 'Q'], _closeModal);
874
+ saveBtn.key(['escape', 'q', 'Q'], _closeModal);
871
875
  closeBtn.key(['escape', 'q', 'Q'], _closeModal);
872
876
 
873
877
  // Tab + arrow navigation within modal
@@ -895,18 +899,27 @@ ${_tl('bmadDesc')}
895
899
  // Wrap: up on buttons → back to field list
896
900
  previewBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
897
901
  resetBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
902
+ saveBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
898
903
  closeBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
899
904
 
900
905
  previewBtn.key(['tab', 'right'], () => { resetBtn.focus(); screen.render(); });
901
906
  previewBtn.key(['left'], () => { closeBtn.focus(); screen.render(); });
902
907
 
903
- resetBtn.key(['tab', 'right'], () => { closeBtn.focus(); screen.render(); });
908
+ resetBtn.key(['tab', 'right'], () => { saveBtn.focus(); screen.render(); });
904
909
  resetBtn.key(['left'], () => { previewBtn.focus(); screen.render(); });
905
910
 
911
+ saveBtn.key(['tab', 'right'], () => { closeBtn.focus(); screen.render(); });
912
+ saveBtn.key(['left'], () => { resetBtn.focus(); screen.render(); });
913
+
906
914
  closeBtn.key(['tab', 'right'], () => { fieldList.focus(); screen.render(); });
907
- closeBtn.key(['left'], () => { resetBtn.focus(); screen.render(); });
915
+ closeBtn.key(['left'], () => { saveBtn.focus(); screen.render(); });
908
916
 
909
917
  fieldList.focus();
918
+ try {
919
+ for (let r = 0; r < screen.height; r++)
920
+ for (let c = 0; c < screen.width; c++)
921
+ if (screen.olines[r]?.[c]) screen.olines[r][c][0] = -1;
922
+ } catch {}
910
923
  screen.render();
911
924
  }
912
925
 
@@ -984,33 +997,38 @@ ${_tl('bmadDesc')}
984
997
  },
985
998
  });
986
999
 
987
- const vpInfoLine = blessed.text({
988
- parent: vpModal, bottom: 4, left: 2, right: 2, tags: true,
989
- content: '', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
990
- });
991
-
992
1000
  const vpPreviewLine = blessed.text({
993
- parent: vpModal, bottom: 3, left: 2, right: 2, tags: true,
1001
+ parent: vpModal, bottom: 4, left: 2, right: 2, height: 1, tags: true,
994
1002
  content: '', style: { fg: 'bright-cyan', bg: COLORS.contentBg },
995
1003
  });
996
1004
 
997
1005
  blessed.text({
998
- parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
999
- content: '{#455a64-fg}[↑↓] Nav [PgUp/PgDn] Page [Home/End] [a-z] Jump [Enter] Select [Space] Preview [Esc] Cancel{/#455a64-fg}',
1006
+ parent: vpModal, bottom: 3, left: 2, right: 2, height: 1, tags: true,
1007
+ content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [a-z] Jump{/white-fg}',
1008
+ style: { bg: COLORS.contentBg },
1009
+ });
1010
+ blessed.text({
1011
+ parent: vpModal, bottom: 2, left: 2, right: 2, height: 1, tags: true,
1012
+ content: '{white-fg}[Enter] Select [Space] Preview [+] 👍 [-] 👎 [Esc] Cancel{/white-fg}',
1000
1013
  style: { bg: COLORS.contentBg },
1001
1014
  });
1002
1015
 
1003
1016
  function _buildVoiceItems(voices) {
1017
+ const favs = getFavorites(configService);
1018
+ const td = getThumbsDown(configService);
1004
1019
  return voices.map(v => {
1005
1020
  const isActive = v === draft.voice;
1006
1021
  const isPrev = v === _previewVoiceId;
1022
+ const isUp = favs.includes(v);
1023
+ const isDown = td.includes(v);
1007
1024
  const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
1025
+ const star = isUp ? '{green-fg}👍{/green-fg}' : (isDown ? '{red-fg}👎{/red-fg}' : ' ');
1008
1026
  const meta = getVoiceMeta(v);
1009
1027
  const name = meta.displayName.length > COL_N
1010
1028
  ? meta.displayName.slice(0, COL_N - 1) + '…'
1011
1029
  : meta.displayName.padEnd(COL_N);
1012
1030
  // genderIconTag has invisible color tags — pad with literal spaces (1 visible char + 3 spaces = 4)
1013
- return ` ${dot} ${name}${genderIconTag(meta.gender)} ${meta.provider}`;
1031
+ return ` ${dot}${star} ${name}${genderIconTag(meta.gender)} ${meta.provider}`;
1014
1032
  });
1015
1033
  }
1016
1034
 
@@ -1034,7 +1052,26 @@ ${_tl('bmadDesc')}
1034
1052
  _killVP();
1035
1053
  if (_previewMinTimer) { clearTimeout(_previewMinTimer); _previewMinTimer = null; }
1036
1054
 
1037
- const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
1055
+ const phrase = `Hi, my name is ${getVoiceMeta(voiceId).displayName}.`;
1056
+ const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
1057
+
1058
+ if (_isWin) {
1059
+ // Windows: route through play-tts.ps1 (same pattern as non-Windows bash route)
1060
+ const playTtsScript = path.join(_projectRoot, '.claude', 'hooks-windows', 'play-tts.ps1');
1061
+ if (!fs.existsSync(playTtsScript)) return;
1062
+ _previewVoiceId = voiceId;
1063
+ if (!_vpClosed) { vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}...{/bright-cyan-fg}`); _refreshVP(); }
1064
+ _previewProc = spawn('powershell', [
1065
+ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTtsScript, phrase, voiceId,
1066
+ ], { stdio: 'ignore', detached: false, windowsHide: true, env: _spawnEnv });
1067
+ _previewProc.on('exit', () => {
1068
+ if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
1069
+ });
1070
+ _previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
1071
+ return;
1072
+ }
1073
+
1074
+ // Non-Windows: use bash play-tts.sh
1038
1075
  const playTtsScript = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
1039
1076
  if (!fs.existsSync(playTtsScript)) return;
1040
1077
 
@@ -1043,34 +1080,23 @@ ${_tl('bmadDesc')}
1043
1080
  if (remoteLlm) args.push('--llm', remoteLlm);
1044
1081
 
1045
1082
  _previewProc = spawn('bash', args, {
1046
- stdio: 'ignore',
1047
- detached: true,
1083
+ stdio: 'ignore', detached: true,
1048
1084
  env: { ..._spawnEnv, CLAUDE_PROJECT_DIR: _projectRoot },
1049
1085
  cwd: _projectRoot,
1050
1086
  });
1051
1087
  _previewVoiceId = voiceId;
1052
-
1053
- if (!_vpClosed) {
1054
- vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}...{/bright-cyan-fg}`);
1055
- _refreshVP();
1056
- }
1088
+ if (!_vpClosed) { vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}...{/bright-cyan-fg}`); _refreshVP(); }
1057
1089
 
1058
1090
  const _clearAfterMinDisplay = () => {
1059
1091
  if (_previewVoiceId === voiceId) {
1060
- _previewVoiceId = null;
1061
- _previewProc = null;
1092
+ _previewVoiceId = null; _previewProc = null;
1062
1093
  if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); }
1063
1094
  }
1064
1095
  _previewMinTimer = null;
1065
1096
  };
1066
-
1067
1097
  // Keep indicator visible for at least 2s (ssh-remote exits immediately)
1068
- _previewProc.on('exit', () => {
1069
- _previewMinTimer = setTimeout(_clearAfterMinDisplay, 2000);
1070
- });
1071
- _previewProc.on('error', () => {
1072
- _previewMinTimer = setTimeout(_clearAfterMinDisplay, 2000);
1073
- });
1098
+ _previewProc.on('exit', () => { _previewMinTimer = setTimeout(_clearAfterMinDisplay, 2000); });
1099
+ _previewProc.on('error', () => { _previewMinTimer = setTimeout(_clearAfterMinDisplay, 2000); });
1074
1100
  }
1075
1101
 
1076
1102
  vpList.key(['enter'], () => {
@@ -1081,7 +1107,15 @@ ${_tl('bmadDesc')}
1081
1107
  const sel = _allVoices[vpList.selected];
1082
1108
  if (sel) _previewVoice(sel);
1083
1109
  });
1084
- vpList.key(['escape', 'q'], _closeVP);
1110
+ vpList.key(['*', '+'], () => {
1111
+ const sel = _allVoices[vpList.selected];
1112
+ if (sel) { toggleThumbsUp(configService, sel); _refreshVP(); }
1113
+ });
1114
+ vpList.key(['-'], () => {
1115
+ const sel = _allVoices[vpList.selected];
1116
+ if (sel) { toggleThumbsDown(configService, sel); _refreshVP(); }
1117
+ });
1118
+ vpList.key(['escape', 'q', 'Q'], _closeVP);
1085
1119
 
1086
1120
  // PageUp / PageDown / Home / End navigation
1087
1121
  const _pageSize = () => Math.max(1, (vpList.height ?? 10) - 2);
@@ -1357,7 +1391,7 @@ ${_tl('bmadDesc')}
1357
1391
  });
1358
1392
  }
1359
1393
 
1360
- /** Windows full-effects preview: temporarily patches config files then calls play-tts.ps1 */
1394
+ /** Windows full-effects preview: temporarily patches personality/reverb, passes music via env vars */
1361
1395
  function _sampleWithFullEffectsWindows(gen, agent, profile, phrase, onComplete) {
1362
1396
  const _spawnEnv = buildAudioEnv();
1363
1397
  const homeDir = process.env.USERPROFILE || os.homedir();
@@ -1365,38 +1399,35 @@ ${_tl('bmadDesc')}
1365
1399
  const claudeDir = fs.existsSync(path.join(_projectRoot, '.claude'))
1366
1400
  ? path.join(_projectRoot, '.claude')
1367
1401
  : path.join(homeDir, '.claude');
1368
- const configDir = path.join(claudeDir, 'config');
1369
- const hooksDir = path.join(claudeDir, 'hooks-windows');
1370
- const playTts = path.join(hooksDir, 'play-tts.ps1');
1402
+ const configDir = path.join(claudeDir, 'config');
1403
+ const hooksDir = path.join(claudeDir, 'hooks-windows');
1404
+ const playTts = path.join(hooksDir, 'play-tts.ps1');
1371
1405
  if (!fs.existsSync(playTts)) { _sampleWithPiperDirect(gen, profile.voice || '', phrase); return; }
1372
1406
 
1373
- // Files to temporarily patch
1374
- const personalityFile = path.join(configDir, 'personality.txt');
1375
- const reverbFile = path.join(configDir, 'reverb-level.txt');
1376
- const bgEnabledFile = path.join(configDir, 'background-music-enabled.txt');
1377
- const audioEffectsCfg = path.join(configDir, 'audio-effects.cfg');
1407
+ const personalityFile = path.join(configDir, 'personality.txt');
1408
+ const reverbFile = path.join(configDir, 'reverb-level.txt');
1378
1409
 
1379
- // Save originals
1380
1410
  const _read = f => { try { return fs.readFileSync(f, 'utf8'); } catch { return null; } };
1381
1411
  const origPersonality = _read(personalityFile);
1382
1412
  const origReverb = _read(reverbFile);
1383
- const origBgEnabled = _read(bgEnabledFile);
1384
- const origAudioEffects = _read(audioEffectsCfg);
1385
1413
 
1386
- const bgMusic = profile.backgroundMusic;
1387
- let tempCfgLine = '';
1414
+ const bgMusic = profile.backgroundMusic;
1415
+
1416
+ // Build spawn env — pass per-agent music via AGENTVIBES_OVERRIDE_* env vars.
1417
+ // play-tts.ps1 reads these directly and forces BgEnabled=true when set,
1418
+ // so no config file patching is needed for background music.
1419
+ const spawnEnv = { ..._spawnEnv, AGENTVIBES_AGENT_NAME: agent.id, CLAUDE_PROJECT_DIR: _projectRoot };
1420
+ if (bgMusic?.enabled && bgMusic?.track) {
1421
+ const vol = ((bgMusic.volume ?? 20) / 100).toFixed(2);
1422
+ spawnEnv.AGENTVIBES_OVERRIDE_MUSIC = bgMusic.track;
1423
+ spawnEnv.AGENTVIBES_OVERRIDE_VOLUME = vol;
1424
+ }
1388
1425
 
1389
1426
  try {
1390
1427
  if (profile.personality && profile.personality !== 'none')
1391
1428
  fs.writeFileSync(personalityFile, profile.personality);
1392
1429
  if (profile.reverbPreset)
1393
1430
  fs.writeFileSync(reverbFile, profile.reverbPreset);
1394
- if (bgMusic?.enabled && bgMusic?.track) {
1395
- fs.writeFileSync(bgEnabledFile, 'true');
1396
- const vol = ((bgMusic.volume ?? 20) / 100).toFixed(2);
1397
- tempCfgLine = `${agent.id}||${bgMusic.track}|${vol}`;
1398
- fs.writeFileSync(audioEffectsCfg, `${tempCfgLine}\n${origAudioEffects || ''}`);
1399
- }
1400
1431
  } catch { /* degrade gracefully */ }
1401
1432
 
1402
1433
  const voiceId = profile.voice || '';
@@ -1404,10 +1435,7 @@ ${_tl('bmadDesc')}
1404
1435
  if (voiceId) psArgs.push(voiceId);
1405
1436
 
1406
1437
  const proc = spawn('powershell', psArgs, {
1407
- stdio: 'ignore', detached: false, windowsHide: true,
1408
- // CLAUDE_PROJECT_DIR tells play-tts.ps1 to use the project's .claude/config
1409
- // rather than falling back to ~/.claude where our patches don't exist.
1410
- env: { ..._spawnEnv, AGENTVIBES_AGENT_NAME: agent.id, CLAUDE_PROJECT_DIR: _projectRoot },
1438
+ stdio: 'ignore', detached: false, windowsHide: true, env: spawnEnv,
1411
1439
  });
1412
1440
  _playingProcess = proc;
1413
1441
 
@@ -1416,11 +1444,6 @@ ${_tl('bmadDesc')}
1416
1444
  if (origPersonality !== null) fs.writeFileSync(personalityFile, origPersonality);
1417
1445
  else try { fs.unlinkSync(personalityFile); } catch {}
1418
1446
  if (origReverb !== null) fs.writeFileSync(reverbFile, origReverb);
1419
- if (bgMusic?.enabled && bgMusic?.track) {
1420
- if (origBgEnabled !== null) fs.writeFileSync(bgEnabledFile, origBgEnabled);
1421
- else try { fs.unlinkSync(bgEnabledFile); } catch {}
1422
- if (origAudioEffects !== null) fs.writeFileSync(audioEffectsCfg, origAudioEffects);
1423
- }
1424
1447
  } catch {}
1425
1448
  }
1426
1449