agentvibes 4.6.0 → 4.6.2

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/package.json CHANGED
@@ -1,110 +1,110 @@
1
- {
2
- "$schema": "https://json.schemastore.org/package.json",
3
- "name": "agentvibes",
4
- "version": "4.6.0",
5
- "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
- "homepage": "https://agentvibes.org",
7
- "keywords": [
8
- "tts",
9
- "text-to-speech",
10
- "piper-tts",
11
- "claude-code",
12
- "claude-desktop",
13
- "clawdbot",
14
- "mcp",
15
- "model-context-protocol",
16
- "voice",
17
- "ai",
18
- "narration",
19
- "agent-vibes"
20
- ],
21
- "repository": {
22
- "type": "git",
23
- "url": "git+https://github.com/paulpreibisch/AgentVibes.git"
24
- },
25
- "license": "Apache-2.0",
26
- "author": "Paul Preibisch <paul@paulpreibisch.com>",
27
- "type": "module",
28
- "main": "src/installer.js",
29
- "bin": {
30
- "agentvibes": "bin/agentvibes.js",
31
- "agent-vibes": "bin/agent-vibes",
32
- "agentvibes-mcp-server": "bin/mcp-server.js",
33
- "agentvibes-voice-browser": "bin/agentvibes-voice-browser.js",
34
- "test-bmad-pr": "bin/test-bmad-pr"
35
- },
36
- "files": [
37
- "bin/",
38
- "src/",
39
- "templates/*.sh",
40
- "templates/*.md",
41
- "templates/audio/*.mp3",
42
- "mcp-server/*.py",
43
- "mcp-server/*.js",
44
- "mcp-server/*.md",
45
- "mcp-server/*.txt",
46
- "mcp-server/*.toml",
47
- "mcp-server/*.json",
48
- "mcp-server/docs/",
49
- "mcp-server/examples/",
50
- ".claude/commands/agent-vibes/",
51
- ".claude/commands/agent-vibes-bmad-voices.md",
52
- ".claude/commands/agent-vibes-rdp.md",
53
- ".claude/hooks/",
54
- ".claude/hooks-windows/",
55
- ".claude/personalities/",
56
- ".claude/output-styles/",
57
- ".claude/audio/tracks/",
58
- ".claude/config/",
59
- ".claude/docs/",
60
- ".claude/language-voices.yaml",
61
- ".claude/settings.json",
62
- ".claude/activation-instructions",
63
- ".claude/piper-voices-dir.txt",
64
- ".claude/verbosity.txt",
65
- ".claude/github-star-reminder.txt",
66
- ".claude/audio/voice-samples/",
67
- "voice-assignments.json",
68
- ".agentvibes/",
69
- ".clawdbot/",
70
- ".mcp.json",
71
- "setup-windows.ps1",
72
- "WINDOWS-SETUP.md",
73
- "README.md",
74
- "RELEASE_NOTES.md",
75
- "LICENSE",
76
- "CLAUDE.md"
77
- ],
78
- "scripts": {
79
- "install-local": "node src/installer.js install",
80
- "postinstall": "node mcp-server/install-deps.js",
81
- "install-mcp-deps": "node mcp-server/install-deps.js",
82
- "voice-browser": "node bin/agentvibes-voice-browser.js",
83
- "test": "npm run test:syntax && AGENTVIBES_TEST_MODE=true bats test/unit/*.bats && npm run test:coverage",
84
- "test:syntax": "node -c src/installer.js && node -c mcp-server/install-deps.js",
85
- "test:bats": "AGENTVIBES_TEST_MODE=true bats test/unit/*.bats",
86
- "test:node": "node --test test/unit/*.test.js",
87
- "test:coverage": "c8 --reporter=lcov --reporter=text node --test test/unit/*.test.js",
88
- "test:verbose": "AGENTVIBES_TEST_MODE=true bats -t test/unit/*.bats"
89
- },
90
- "dependencies": {
91
- "@inquirer/search": "^3.1.3",
92
- "blessed": "^0.1.81",
93
- "boxen": "^7.0.0",
94
- "chalk": "^5.0.0",
95
- "commander": "^10.0.0",
96
- "figlet": "^1.6.0",
97
- "inquirer": "^12.0.0",
98
- "js-yaml": "^4.1.1",
99
- "ora": "^6.0.0"
100
- },
101
- "engines": {
102
- "node": ">=16.0.0"
103
- },
104
- "publishConfig": {
105
- "access": "public"
106
- },
107
- "devDependencies": {
108
- "c8": "^10.1.3"
109
- }
110
- }
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "agentvibes",
4
+ "version": "4.6.2",
5
+ "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
+ "homepage": "https://agentvibes.org",
7
+ "keywords": [
8
+ "tts",
9
+ "text-to-speech",
10
+ "piper-tts",
11
+ "claude-code",
12
+ "claude-desktop",
13
+ "clawdbot",
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "voice",
17
+ "ai",
18
+ "narration",
19
+ "agent-vibes"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/paulpreibisch/AgentVibes.git"
24
+ },
25
+ "license": "Apache-2.0",
26
+ "author": "Paul Preibisch <paul@paulpreibisch.com>",
27
+ "type": "module",
28
+ "main": "src/installer.js",
29
+ "bin": {
30
+ "agentvibes": "bin/agentvibes.js",
31
+ "agent-vibes": "bin/agent-vibes",
32
+ "agentvibes-mcp-server": "bin/mcp-server.js",
33
+ "agentvibes-voice-browser": "bin/agentvibes-voice-browser.js",
34
+ "test-bmad-pr": "bin/test-bmad-pr"
35
+ },
36
+ "files": [
37
+ "bin/",
38
+ "src/",
39
+ "templates/*.sh",
40
+ "templates/*.md",
41
+ "templates/audio/*.mp3",
42
+ "mcp-server/*.py",
43
+ "mcp-server/*.js",
44
+ "mcp-server/*.md",
45
+ "mcp-server/*.txt",
46
+ "mcp-server/*.toml",
47
+ "mcp-server/*.json",
48
+ "mcp-server/docs/",
49
+ "mcp-server/examples/",
50
+ ".claude/commands/agent-vibes/",
51
+ ".claude/commands/agent-vibes-bmad-voices.md",
52
+ ".claude/commands/agent-vibes-rdp.md",
53
+ ".claude/hooks/",
54
+ ".claude/hooks-windows/",
55
+ ".claude/personalities/",
56
+ ".claude/output-styles/",
57
+ ".claude/audio/tracks/",
58
+ ".claude/config/",
59
+ ".claude/docs/",
60
+ ".claude/language-voices.yaml",
61
+ ".claude/settings.json",
62
+ ".claude/activation-instructions",
63
+ ".claude/piper-voices-dir.txt",
64
+ ".claude/verbosity.txt",
65
+ ".claude/github-star-reminder.txt",
66
+ ".claude/audio/voice-samples/",
67
+ "voice-assignments.json",
68
+ ".agentvibes/",
69
+ ".clawdbot/",
70
+ ".mcp.json",
71
+ "setup-windows.ps1",
72
+ "WINDOWS-SETUP.md",
73
+ "README.md",
74
+ "RELEASE_NOTES.md",
75
+ "LICENSE",
76
+ "CLAUDE.md"
77
+ ],
78
+ "scripts": {
79
+ "install-local": "node src/installer.js install",
80
+ "postinstall": "node mcp-server/install-deps.js",
81
+ "install-mcp-deps": "node mcp-server/install-deps.js",
82
+ "voice-browser": "node bin/agentvibes-voice-browser.js",
83
+ "test": "npm run test:syntax && AGENTVIBES_TEST_MODE=true bats test/unit/*.bats && npm run test:coverage",
84
+ "test:syntax": "node -c src/installer.js && node -c mcp-server/install-deps.js",
85
+ "test:bats": "AGENTVIBES_TEST_MODE=true bats test/unit/*.bats",
86
+ "test:node": "node --test test/unit/*.test.js",
87
+ "test:coverage": "c8 --reporter=lcov --reporter=text node --test test/unit/*.test.js",
88
+ "test:verbose": "AGENTVIBES_TEST_MODE=true bats -t test/unit/*.bats"
89
+ },
90
+ "dependencies": {
91
+ "@inquirer/search": "^3.1.3",
92
+ "blessed": "^0.1.81",
93
+ "boxen": "^7.0.0",
94
+ "chalk": "^5.0.0",
95
+ "commander": "^10.0.0",
96
+ "figlet": "^1.6.0",
97
+ "inquirer": "^12.0.0",
98
+ "js-yaml": "^4.1.1",
99
+ "ora": "^6.0.0"
100
+ },
101
+ "engines": {
102
+ "node": ">=16.0.0"
103
+ },
104
+ "publishConfig": {
105
+ "access": "public"
106
+ },
107
+ "devDependencies": {
108
+ "c8": "^10.1.3"
109
+ }
110
+ }
@@ -31,6 +31,84 @@ import { spawn } from 'node:child_process';
31
31
  // Max pretext length to prevent excessively long TTS utterances
32
32
  const MAX_PRETEXT_LENGTH = 200;
33
33
 
34
+ /**
35
+ * Attach a blinking █ cursor to a set of blessed buttons.
36
+ * Works alongside existing focus/blur handlers (e.g. ►..◄ indicators).
37
+ * While a spinner is active on a button, blink is paused for that button.
38
+ * Returns { cleanup, startSpinner(btn, screen), stopSpinner(btn, screen) }.
39
+ */
40
+ export function attachBtnBlink(btns, screen) {
41
+ let _interval = null;
42
+ let _on = true;
43
+ let _active = null;
44
+ let _spinning = null; // button currently showing a spinner
45
+
46
+ const _SPIN = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
47
+ let _spinIdx = 0;
48
+ let _spinInterval = null;
49
+
50
+ // Store original label on each button at attach time — never derive from current content
51
+ btns.forEach(btn => { btn._blinkBase = btn.content; });
52
+
53
+ // Focused with indicator ch right after ► e.g. ►█Preview◄ / ► Preview◄ (same width)
54
+ function _focused(base, ch) { return `►${ch}${base}◄`; }
55
+
56
+ function _tick() {
57
+ if (!_active || _active === _spinning) return;
58
+ _on = !_on;
59
+ _active.setContent(_focused(_active._blinkBase, _on ? '█' : ' '));
60
+ screen.render();
61
+ }
62
+
63
+ btns.forEach(btn => {
64
+ btn.on('focus', () => {
65
+ _active = btn;
66
+ _on = true;
67
+ if (btn !== _spinning) {
68
+ btn.setContent(_focused(btn._blinkBase, '█'));
69
+ screen.render();
70
+ }
71
+ if (!_interval) _interval = setInterval(_tick, 500);
72
+ });
73
+ btn.on('blur', () => {
74
+ if (_active !== btn) return;
75
+ _active = null;
76
+ if (_interval) { clearInterval(_interval); _interval = null; _on = true; }
77
+ if (btn !== _spinning) {
78
+ btn.setContent(btn._blinkBase);
79
+ screen.render();
80
+ }
81
+ });
82
+ });
83
+
84
+ function startSpinner(btn) {
85
+ _spinning = btn;
86
+ _spinIdx = 0;
87
+ if (_spinInterval) clearInterval(_spinInterval);
88
+ _spinInterval = setInterval(() => {
89
+ _spinIdx = (_spinIdx + 1) % _SPIN.length;
90
+ btn.setContent(_active === btn
91
+ ? _focused(btn._blinkBase, _SPIN[_spinIdx])
92
+ : `${_SPIN[_spinIdx]}${btn._blinkBase}`);
93
+ screen.render();
94
+ }, 80);
95
+ }
96
+
97
+ function stopSpinner(btn) {
98
+ if (_spinInterval) { clearInterval(_spinInterval); _spinInterval = null; }
99
+ _spinning = null;
100
+ btn.setContent(_active === btn ? _focused(btn._blinkBase, '█') : btn._blinkBase);
101
+ screen.render();
102
+ }
103
+
104
+ function cleanup() {
105
+ if (_interval) { clearInterval(_interval); _interval = null; }
106
+ if (_spinInterval){ clearInterval(_spinInterval); _spinInterval = null; }
107
+ }
108
+
109
+ return { cleanup, startSpinner, stopSpinner };
110
+ }
111
+
34
112
  const IS_TEST = process.env.AGENTVIBES_TEST_MODE === 'true';
35
113
 
36
114
  let blessed;
@@ -547,7 +625,7 @@ ${_tl('bmadDesc')}
547
625
  top: 'center',
548
626
  left: 'center',
549
627
  width: 72,
550
- height: 18,
628
+ height: 19,
551
629
  border: { type: 'line' },
552
630
  tags: true,
553
631
  label: _modalTitle(`${agent.icon || '🧙'} ${agent.displayName} (${agent.title || 'Agent'})`),
@@ -569,9 +647,13 @@ ${_tl('bmadDesc')}
569
647
  const emoji = PERSONALITY_EMOJIS[p] || '';
570
648
  return `${emoji} ${p === 'none' ? 'None' : p.charAt(0).toUpperCase() + p.slice(1)}`;
571
649
  }},
572
- { key: 'music', label: 'Music', getValue: () => {
650
+ { key: 'musicTrack', label: 'Music Track', getValue: () => {
651
+ if (!draft.backgroundMusic.enabled) return '(disabled)';
652
+ return formatTrackName(draft.backgroundMusic.track) || '(none)';
653
+ }},
654
+ { key: 'musicVol', label: 'Music Vol', getValue: () => {
573
655
  if (!draft.backgroundMusic.enabled) return '(disabled)';
574
- return `${formatTrackName(draft.backgroundMusic.track)} Vol:${draft.backgroundMusic.volume}%`;
656
+ return `${draft.backgroundMusic.volume ?? 20}%`;
575
657
  }},
576
658
  ];
577
659
 
@@ -612,7 +694,7 @@ ${_tl('bmadDesc')}
612
694
  left: 2,
613
695
  right: 2,
614
696
  tags: true,
615
- content: '{#455a64-fg}[↑↓] Navigate fields [Enter] Edit field [Space] Sample [Esc] Cancel{/#455a64-fg}',
697
+ content: '{white-fg}[↑↓] Navigate [Enter] Edit [Tab] → Preview/Save [Esc] Cancel{/white-fg}',
616
698
  style: { bg: COLORS.contentBg },
617
699
  });
618
700
 
@@ -634,22 +716,20 @@ ${_tl('bmadDesc')}
634
716
  hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
635
717
  },
636
718
  });
637
- btn.on('focus', () => {
638
- const raw = btn.content.replace(/[►◄]/g, '').trim();
639
- btn.setContent(`►${raw}◄`);
640
- screen.render();
641
- });
642
- btn.on('blur', () => {
643
- const raw = btn.content.replace(/[►◄]/g, '').trim();
644
- btn.setContent(raw);
645
- screen.render();
646
- });
719
+ // Focus indicator handled by attachBtnBlink
647
720
  btn.key(['enter', 'space'], () => onClick());
648
721
  btn.on('click', () => onClick());
649
722
  return btn;
650
723
  }
651
724
 
652
- const saveBtn = _modalBtn('Save', 4, () => {
725
+ const previewBtn = _modalBtn('Preview', 4, () => {
726
+ _sampleAgentWithDraft({ ...agent }, draft, () => {
727
+ btnBlink.stopSpinner(previewBtn);
728
+ });
729
+ btnBlink.startSpinner(previewBtn);
730
+ });
731
+
732
+ const saveBtn = _modalBtn('Save', 18, () => {
653
733
  // Only save fields that differ from global
654
734
  const toSave = {};
655
735
  if (draft.voice && draft.voice !== globalCfg.voice) toSave.voice = draft.voice;
@@ -668,20 +748,26 @@ ${_tl('bmadDesc')}
668
748
  _showSavedToast(agent.displayName);
669
749
  });
670
750
 
671
- const resetAllBtn = _modalBtn('Reset to Defaults', 14, () => {
751
+ const resetAllBtn = _modalBtn('Reset to Defaults', 26, () => {
672
752
  voiceStore.resetAgentProfile(agent.id);
673
753
  _closeModal();
674
754
  refreshDisplay();
675
755
  });
676
756
 
677
- const cancelBtn = _modalBtn('Cancel', 38, _closeModal);
757
+ const cancelBtn = _modalBtn('Cancel', 50, _closeModal);
758
+
759
+ // Blinking █ cursor + preview spinner — reusable across all modal buttons
760
+ const btnBlink = attachBtnBlink([previewBtn, saveBtn, resetAllBtn, cancelBtn], screen);
678
761
 
679
762
  function _closeModal() {
680
763
  if (_closed) return;
681
764
  _closed = true;
682
765
  _killPreview();
766
+ btnBlink.cleanup();
683
767
  navigationService?.closeModal();
684
768
  destroyList(modal, screen);
769
+ agentList.focus();
770
+ screen.render();
685
771
  }
686
772
 
687
773
  // Field editing via Enter
@@ -735,10 +821,9 @@ ${_tl('bmadDesc')}
735
821
  });
736
822
  break;
737
823
 
738
- case 'music':
739
- openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track, volume) => {
824
+ case 'musicTrack':
825
+ openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track) => {
740
826
  draft.backgroundMusic.track = track;
741
- draft.backgroundMusic.volume = volume;
742
827
  draft.backgroundMusic.enabled = true;
743
828
  fieldList.setItems(_fieldItems());
744
829
  fieldList.select(idx);
@@ -747,6 +832,20 @@ ${_tl('bmadDesc')}
747
832
  }, () => {
748
833
  fieldList.focus();
749
834
  screen.render();
835
+ }, { skipVolume: true });
836
+ break;
837
+
838
+ case 'musicVol':
839
+ openVolumeInput(screen, draft.backgroundMusic.volume, (volume) => {
840
+ draft.backgroundMusic.volume = volume;
841
+ if (draft.backgroundMusic.track) draft.backgroundMusic.enabled = true;
842
+ fieldList.setItems(_fieldItems());
843
+ fieldList.select(idx);
844
+ fieldList.focus();
845
+ screen.render();
846
+ }, () => {
847
+ fieldList.focus();
848
+ screen.render();
750
849
  });
751
850
  break;
752
851
  }
@@ -761,15 +860,50 @@ ${_tl('bmadDesc')}
761
860
 
762
861
  // Escape = close
763
862
  fieldList.key(['escape', 'q'], _closeModal);
863
+ previewBtn.key(['escape'], _closeModal);
764
864
  saveBtn.key(['escape'], _closeModal);
765
865
  resetAllBtn.key(['escape'], _closeModal);
766
866
  cancelBtn.key(['escape'], _closeModal);
767
867
 
768
- // Tab navigation within modal
769
- fieldList.key(['tab'], () => { saveBtn.focus(); screen.render(); });
770
- saveBtn.key(['tab'], () => { resetAllBtn.focus(); screen.render(); });
771
- resetAllBtn.key(['tab'], () => { cancelBtn.focus(); screen.render(); });
772
- cancelBtn.key(['tab'], () => { fieldList.focus(); screen.render(); });
868
+ // Tab + arrow navigation within modal
869
+ fieldList.key(['tab'], () => { previewBtn.focus(); screen.render(); });
870
+
871
+ // Wrap: down on last field → focus first button; up on first field → focus first button
872
+ // One extra arrow press at boundary moves to button row.
873
+ // Track previous selection so arriving at boundary doesn't immediately jump.
874
+ let _prevFieldSel = 0;
875
+ fieldList.key(['down'], () => {
876
+ const cur = fieldList.selected ?? 0;
877
+ if (cur === FIELDS.length - 1 && _prevFieldSel === FIELDS.length - 1) {
878
+ previewBtn.focus(); screen.render();
879
+ }
880
+ _prevFieldSel = cur;
881
+ });
882
+ fieldList.key(['up'], () => {
883
+ const cur = fieldList.selected ?? 0;
884
+ if (cur === 0 && _prevFieldSel === 0) {
885
+ previewBtn.focus(); screen.render();
886
+ }
887
+ _prevFieldSel = cur;
888
+ });
889
+
890
+ // Wrap: up on buttons → back to field list (last/first field respectively)
891
+ previewBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
892
+ saveBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
893
+ resetAllBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
894
+ cancelBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
895
+
896
+ previewBtn.key(['tab', 'right'], () => { saveBtn.focus(); screen.render(); });
897
+ previewBtn.key(['left'], () => { cancelBtn.focus(); screen.render(); });
898
+
899
+ saveBtn.key(['tab', 'right'], () => { resetAllBtn.focus(); screen.render(); });
900
+ saveBtn.key(['left'], () => { previewBtn.focus(); screen.render(); });
901
+
902
+ resetAllBtn.key(['tab', 'right'], () => { cancelBtn.focus(); screen.render(); });
903
+ resetAllBtn.key(['left'], () => { saveBtn.focus(); screen.render(); });
904
+
905
+ cancelBtn.key(['tab', 'right'], () => { fieldList.focus(); screen.render(); });
906
+ cancelBtn.key(['left'], () => { resetAllBtn.focus(); screen.render(); });
773
907
 
774
908
  fieldList.focus();
775
909
  screen.render();
@@ -1047,8 +1181,8 @@ ${_tl('bmadDesc')}
1047
1181
  // -------------------------------------------------------------------------
1048
1182
  // Sample agent with a draft profile (no save) — same full pipeline
1049
1183
 
1050
- function _sampleAgentWithDraft(agent, draft) {
1051
- _sampleWithFullProfile(agent, draft);
1184
+ function _sampleAgentWithDraft(agent, draft, onComplete) {
1185
+ _sampleWithFullProfile(agent, draft, onComplete);
1052
1186
  }
1053
1187
 
1054
1188
  // -------------------------------------------------------------------------
@@ -1056,7 +1190,7 @@ ${_tl('bmadDesc')}
1056
1190
  // Writes a temp agent profile JSON, then calls the enhanced TTS pipeline
1057
1191
  // which applies voice + reverb + background music.
1058
1192
 
1059
- function _sampleWithFullProfile(agent, profile) {
1193
+ function _sampleWithFullProfile(agent, profile, onComplete) {
1060
1194
  _killPreview();
1061
1195
  const gen = ++_playGeneration;
1062
1196
 
@@ -1071,9 +1205,9 @@ ${_tl('bmadDesc')}
1071
1205
  const isWindows = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
1072
1206
 
1073
1207
  if (isWindows) {
1074
- // On Windows, synthesize with piper.exe directly then play the wav,
1075
- // avoiding bash/wsl.exe which opens a visible console window.
1076
- _sampleWithPiperDirect(gen, voiceId, phrase);
1208
+ // On Windows, call play-tts.ps1 via PowerShell with draft settings patched
1209
+ // in so reverb, background music, and personality are applied.
1210
+ _sampleWithFullEffectsWindows(gen, agent, profile, phrase, onComplete);
1077
1211
  } else {
1078
1212
  // On Linux/macOS/WSL, use play-tts.sh
1079
1213
  const _spawnEnv = buildAudioEnv();
@@ -1178,6 +1312,77 @@ ${_tl('bmadDesc')}
1178
1312
  });
1179
1313
  }
1180
1314
 
1315
+ /** Windows full-effects preview: temporarily patches config files then calls play-tts.ps1 */
1316
+ function _sampleWithFullEffectsWindows(gen, agent, profile, phrase, onComplete) {
1317
+ const _spawnEnv = buildAudioEnv();
1318
+ const homeDir = process.env.USERPROFILE || os.homedir();
1319
+ // Use project-local .claude if it exists, else global ~/.claude
1320
+ const claudeDir = fs.existsSync(path.join(_projectRoot, '.claude'))
1321
+ ? path.join(_projectRoot, '.claude')
1322
+ : path.join(homeDir, '.claude');
1323
+ const configDir = path.join(claudeDir, 'config');
1324
+ const hooksDir = path.join(claudeDir, 'hooks-windows');
1325
+ const playTts = path.join(hooksDir, 'play-tts.ps1');
1326
+ if (!fs.existsSync(playTts)) { _sampleWithPiperDirect(gen, profile.voice || '', phrase); return; }
1327
+
1328
+ // Files to temporarily patch
1329
+ const personalityFile = path.join(configDir, 'personality.txt');
1330
+ const reverbFile = path.join(configDir, 'reverb-level.txt');
1331
+ const bgEnabledFile = path.join(configDir, 'background-music-enabled.txt');
1332
+ const audioEffectsCfg = path.join(configDir, 'audio-effects.cfg');
1333
+
1334
+ // Save originals
1335
+ const _read = f => { try { return fs.readFileSync(f, 'utf8'); } catch { return null; } };
1336
+ const origPersonality = _read(personalityFile);
1337
+ const origReverb = _read(reverbFile);
1338
+ const origBgEnabled = _read(bgEnabledFile);
1339
+ const origAudioEffects = _read(audioEffectsCfg);
1340
+
1341
+ const bgMusic = profile.backgroundMusic;
1342
+ let tempCfgLine = '';
1343
+
1344
+ try {
1345
+ if (profile.personality && profile.personality !== 'none')
1346
+ fs.writeFileSync(personalityFile, profile.personality);
1347
+ if (profile.reverbPreset)
1348
+ fs.writeFileSync(reverbFile, profile.reverbPreset);
1349
+ if (bgMusic?.enabled && bgMusic?.track) {
1350
+ fs.writeFileSync(bgEnabledFile, 'true');
1351
+ const vol = ((bgMusic.volume ?? 20) / 100).toFixed(2);
1352
+ tempCfgLine = `${agent.id}||${bgMusic.track}|${vol}`;
1353
+ fs.writeFileSync(audioEffectsCfg, `${tempCfgLine}\n${origAudioEffects || ''}`);
1354
+ }
1355
+ } catch { /* degrade gracefully */ }
1356
+
1357
+ const voiceId = profile.voice || '';
1358
+ const psArgs = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTts, phrase];
1359
+ if (voiceId) psArgs.push(voiceId);
1360
+
1361
+ const proc = spawn('powershell', psArgs, {
1362
+ stdio: 'ignore', detached: false, windowsHide: true,
1363
+ // CLAUDE_PROJECT_DIR tells play-tts.ps1 to use the project's .claude/config
1364
+ // rather than falling back to ~/.claude where our patches don't exist.
1365
+ env: { ..._spawnEnv, AGENTVIBES_AGENT_NAME: agent.id, CLAUDE_PROJECT_DIR: _projectRoot },
1366
+ });
1367
+ _playingProcess = proc;
1368
+
1369
+ function _restore() {
1370
+ try {
1371
+ if (origPersonality !== null) fs.writeFileSync(personalityFile, origPersonality);
1372
+ else try { fs.unlinkSync(personalityFile); } catch {}
1373
+ if (origReverb !== null) fs.writeFileSync(reverbFile, origReverb);
1374
+ if (bgMusic?.enabled && bgMusic?.track) {
1375
+ if (origBgEnabled !== null) fs.writeFileSync(bgEnabledFile, origBgEnabled);
1376
+ else try { fs.unlinkSync(bgEnabledFile); } catch {}
1377
+ if (origAudioEffects !== null) fs.writeFileSync(audioEffectsCfg, origAudioEffects);
1378
+ }
1379
+ } catch {}
1380
+ }
1381
+
1382
+ proc.on('exit', () => { if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); } _restore(); if (onComplete) onComplete(); });
1383
+ proc.on('error', () => { if (gen === _playGeneration) { _playingProcess = null; _stopSpinner(); } _restore(); if (onComplete) onComplete(); });
1384
+ }
1385
+
1181
1386
  // -------------------------------------------------------------------------
1182
1387
  // Auto-assign helpers
1183
1388
 
@@ -1199,9 +1404,10 @@ ${_tl('bmadDesc')}
1199
1404
  grace: 'Female', heather: 'Female', ivy: 'Female', jane: 'Female',
1200
1405
  jenny: 'Female', julia: 'Female', kate: 'Female', laura: 'Female',
1201
1406
  lily: 'Female', maria: 'Female', mary: 'Female', nina: 'Female',
1202
- olivia: 'Female', paige: 'Female', rachel: 'Female', sally: 'Female',
1203
- sara: 'Female', sarah: 'Female', sophie: 'Female', tina: 'Female',
1204
- wendy: 'Female', zoe: 'Female',
1407
+ olivia: 'Female', paige: 'Female', quinn: 'Female', rachel: 'Female',
1408
+ sally: 'Female', sara: 'Female', sarah: 'Female', sophie: 'Female',
1409
+ tina: 'Female', wendy: 'Female', zoe: 'Female',
1410
+ freya: 'Female', saga: 'Female',
1205
1411
  // Male
1206
1412
  alan: 'Male', barry: 'Male', bob: 'Male', carl: 'Male',
1207
1413
  charlie: 'Male', dan: 'Male', david: 'Male', eric: 'Male',