agentvibes 4.2.0 → 4.4.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 (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +2 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -433
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -269
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  79. package/.claude/hooks/play-tts-macos.sh +368 -368
  80. package/.claude/hooks/play-tts-piper.sh +679 -679
  81. package/.claude/hooks/play-tts-soprano.sh +356 -356
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -301
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -81
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -84
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -145
  99. package/.claude/hooks/tts-queue.sh +165 -165
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -548
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -170
  149. package/README.md +2029 -2007
  150. package/RELEASE_NOTES.md +1310 -1203
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1840
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1453
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -110
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -824
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -44
  182. package/src/console/footer-config.js +50 -50
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -62
  185. package/src/console/tabs/agents-tab.js +1684 -1516
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -53
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -1212
  192. package/src/console/tabs/settings-tab.js +152 -79
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -25
  195. package/src/console/widgets/format-utils.js +89 -89
  196. package/src/console/widgets/notice.js +55 -55
  197. package/src/console/widgets/personality-picker.js +185 -185
  198. package/src/console/widgets/reverb-picker.js +94 -94
  199. package/src/console/widgets/track-picker.js +285 -285
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5829
  202. package/src/services/agent-voice-store.js +423 -423
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -285
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -482
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. package/.claude/config/background-music-position.txt +0 -1
@@ -1,123 +1,123 @@
1
- /**
2
- * AgentVibes TUI Console — Navigation Service
3
- * Story 6.2: Tab Bar & Global Keyboard Navigation
4
- *
5
- * Manages tab state, cycling, modal overlay state, and focus stack.
6
- * Used by navigation.js (key bindings) and app.js (wiring).
7
- */
8
-
9
- /** Ordered list of all tab IDs — used for cycling and validation */
10
- export const TAB_ORDER = ['install', 'settings', 'voices', 'music', 'agents', 'receiver', 'readme', 'help'];
11
-
12
- export class NavigationService {
13
- /**
14
- * @param {string} [initialTab='settings'] - Tab to activate on launch
15
- */
16
- constructor(initialTab = 'settings') {
17
- this._activeTab = TAB_ORDER.includes(initialTab) ? initialTab : 'settings';
18
- this._switchCallbacks = [];
19
- this._focusStack = [];
20
- this._modalOpen = false;
21
- }
22
-
23
- // ---------------------------------------------------------------------------
24
- // Tab navigation
25
-
26
- /** Returns the currently active tab ID */
27
- getActiveTab() {
28
- return this._activeTab;
29
- }
30
-
31
- /**
32
- * Switch to the given tab.
33
- * Ignores invalid tab IDs. Fires all registered onSwitch callbacks.
34
- * @param {string} tabId
35
- */
36
- switchTab(tabId) {
37
- if (!TAB_ORDER.includes(tabId)) return;
38
- if (tabId === this._activeTab) return; // no-op: already on this tab
39
- this._activeTab = tabId;
40
- this._switchCallbacks.forEach(cb => cb(tabId));
41
- }
42
-
43
- /**
44
- * Activate a tab unconditionally, bypassing the same-tab no-op guard.
45
- * Used for initial UI setup: the constructor pre-sets _activeTab but
46
- * onSwitch callbacks must still fire to render the initial state.
47
- * @param {string} tabId
48
- */
49
- forceActivate(tabId) {
50
- if (!TAB_ORDER.includes(tabId)) return;
51
- this._activeTab = tabId;
52
- this._switchCallbacks.forEach(cb => cb(tabId));
53
- }
54
-
55
- /**
56
- * Cycle to the next tab in TAB_ORDER, wrapping from last back to first.
57
- */
58
- cycleTab() {
59
- const idx = TAB_ORDER.indexOf(this._activeTab);
60
- const nextIdx = (idx + 1) % TAB_ORDER.length;
61
- this.switchTab(TAB_ORDER[nextIdx]);
62
- }
63
-
64
- /**
65
- * Cycle to the previous tab in TAB_ORDER, wrapping from first back to last.
66
- */
67
- cycleTabPrev() {
68
- const idx = TAB_ORDER.indexOf(this._activeTab);
69
- const prevIdx = (idx - 1 + TAB_ORDER.length) % TAB_ORDER.length;
70
- this.switchTab(TAB_ORDER[prevIdx]);
71
- }
72
-
73
- /**
74
- * Register a callback fired whenever the active tab changes.
75
- * @param {(tabId: string) => void} callback
76
- */
77
- onSwitch(callback) {
78
- this._switchCallbacks.push(callback);
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Modal state (story 6.4 will expand this)
83
-
84
- /** Returns true if a modal is currently open */
85
- isModalOpen() {
86
- return this._modalOpen;
87
- }
88
-
89
- /**
90
- * Open a modal. Sets modal-open state and calls the factory fn if provided.
91
- * @param {Function|null} fn - Optional factory/callback invoked immediately
92
- */
93
- openModal(fn) {
94
- this._modalOpen = true;
95
- fn?.();
96
- }
97
-
98
- /** Close the current modal, restoring modal-closed state */
99
- closeModal() {
100
- this._modalOpen = false;
101
- }
102
-
103
-
104
- // ---------------------------------------------------------------------------
105
- // Focus stack (story 7.6 will use this for button-level focus)
106
-
107
- /**
108
- * Push a Blessed element onto the focus stack
109
- * @param {object} element - Blessed widget
110
- */
111
- pushFocus(element) {
112
- this._focusStack.push(element);
113
- }
114
-
115
- /**
116
- * Pop the last element from the focus stack.
117
- * Returns undefined if the stack is empty.
118
- * @returns {object|undefined}
119
- */
120
- popFocus() {
121
- return this._focusStack.pop();
122
- }
123
- }
1
+ /**
2
+ * AgentVibes TUI Console — Navigation Service
3
+ * Story 6.2: Tab Bar & Global Keyboard Navigation
4
+ *
5
+ * Manages tab state, cycling, modal overlay state, and focus stack.
6
+ * Used by navigation.js (key bindings) and app.js (wiring).
7
+ */
8
+
9
+ /** Ordered list of all tab IDs — used for cycling and validation */
10
+ export const TAB_ORDER = ['install', 'settings', 'voices', 'music', 'agents', 'receiver', 'readme', 'help'];
11
+
12
+ export class NavigationService {
13
+ /**
14
+ * @param {string} [initialTab='settings'] - Tab to activate on launch
15
+ */
16
+ constructor(initialTab = 'settings') {
17
+ this._activeTab = TAB_ORDER.includes(initialTab) ? initialTab : 'settings';
18
+ this._switchCallbacks = [];
19
+ this._focusStack = [];
20
+ this._modalOpen = false;
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Tab navigation
25
+
26
+ /** Returns the currently active tab ID */
27
+ getActiveTab() {
28
+ return this._activeTab;
29
+ }
30
+
31
+ /**
32
+ * Switch to the given tab.
33
+ * Ignores invalid tab IDs. Fires all registered onSwitch callbacks.
34
+ * @param {string} tabId
35
+ */
36
+ switchTab(tabId) {
37
+ if (!TAB_ORDER.includes(tabId)) return;
38
+ if (tabId === this._activeTab) return; // no-op: already on this tab
39
+ this._activeTab = tabId;
40
+ this._switchCallbacks.forEach(cb => cb(tabId));
41
+ }
42
+
43
+ /**
44
+ * Activate a tab unconditionally, bypassing the same-tab no-op guard.
45
+ * Used for initial UI setup: the constructor pre-sets _activeTab but
46
+ * onSwitch callbacks must still fire to render the initial state.
47
+ * @param {string} tabId
48
+ */
49
+ forceActivate(tabId) {
50
+ if (!TAB_ORDER.includes(tabId)) return;
51
+ this._activeTab = tabId;
52
+ this._switchCallbacks.forEach(cb => cb(tabId));
53
+ }
54
+
55
+ /**
56
+ * Cycle to the next tab in TAB_ORDER, wrapping from last back to first.
57
+ */
58
+ cycleTab() {
59
+ const idx = TAB_ORDER.indexOf(this._activeTab);
60
+ const nextIdx = (idx + 1) % TAB_ORDER.length;
61
+ this.switchTab(TAB_ORDER[nextIdx]);
62
+ }
63
+
64
+ /**
65
+ * Cycle to the previous tab in TAB_ORDER, wrapping from first back to last.
66
+ */
67
+ cycleTabPrev() {
68
+ const idx = TAB_ORDER.indexOf(this._activeTab);
69
+ const prevIdx = (idx - 1 + TAB_ORDER.length) % TAB_ORDER.length;
70
+ this.switchTab(TAB_ORDER[prevIdx]);
71
+ }
72
+
73
+ /**
74
+ * Register a callback fired whenever the active tab changes.
75
+ * @param {(tabId: string) => void} callback
76
+ */
77
+ onSwitch(callback) {
78
+ this._switchCallbacks.push(callback);
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Modal state (story 6.4 will expand this)
83
+
84
+ /** Returns true if a modal is currently open */
85
+ isModalOpen() {
86
+ return this._modalOpen;
87
+ }
88
+
89
+ /**
90
+ * Open a modal. Sets modal-open state and calls the factory fn if provided.
91
+ * @param {Function|null} fn - Optional factory/callback invoked immediately
92
+ */
93
+ openModal(fn) {
94
+ this._modalOpen = true;
95
+ fn?.();
96
+ }
97
+
98
+ /** Close the current modal, restoring modal-closed state */
99
+ closeModal() {
100
+ this._modalOpen = false;
101
+ }
102
+
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Focus stack (story 7.6 will use this for button-level focus)
106
+
107
+ /**
108
+ * Push a Blessed element onto the focus stack
109
+ * @param {object} element - Blessed widget
110
+ */
111
+ pushFocus(element) {
112
+ this._focusStack.push(element);
113
+ }
114
+
115
+ /**
116
+ * Pop the last element from the focus stack.
117
+ * Returns undefined if the stack is empty.
118
+ * @returns {object|undefined}
119
+ */
120
+ popFocus() {
121
+ return this._focusStack.pop();
122
+ }
123
+ }
@@ -1,132 +1,132 @@
1
- /**
2
- * AgentVibes Provider Service
3
- * Story 7.1: Provider & Voice Settings Group
4
- *
5
- * Detects installed TTS providers, reads/writes active provider and voice
6
- * through ConfigService. Gracefully degrades when detection fails.
7
- */
8
-
9
- import { execFileSync } from 'node:child_process';
10
- import fs from 'node:fs';
11
- import path from 'node:path';
12
- import os from 'node:os';
13
-
14
- export class ProviderService {
15
- /**
16
- * @param {import('./config-service.js').ConfigService} configService
17
- */
18
- constructor(configService) {
19
- this._config = configService;
20
- this._installedProviders = null; // cached after first detection
21
- }
22
-
23
- // ---------------------------------------------------------------------------
24
- // Provider
25
-
26
- /**
27
- * Returns the currently active TTS provider from config.
28
- * Defaults to 'piper' if not configured.
29
- * @returns {string}
30
- */
31
- getActiveProvider() {
32
- return this._config.getConfig().provider ?? 'piper';
33
- }
34
-
35
- /**
36
- * Sets the active TTS provider in config AND syncs to .claude/tts-provider.txt
37
- * so the shell hooks (play-tts.sh → provider-manager.sh) pick up the change.
38
- * @param {string} provider
39
- */
40
- setActiveProvider(provider) {
41
- this._config.set('provider', provider);
42
- this._syncProviderFile(provider);
43
- }
44
-
45
- /**
46
- * Write provider to .claude/tts-provider.txt so shell hooks stay in sync.
47
- * Writes to projectRoot/.claude/tts-provider.txt if .claude/ exists there,
48
- * otherwise falls back to ~/.claude/tts-provider.txt.
49
- * @param {string} provider
50
- */
51
- _syncProviderFile(provider) {
52
- try {
53
- const projectClaudeDir = path.resolve(this._config.getProjectRoot(), '.claude');
54
- const targetDir = fs.existsSync(projectClaudeDir)
55
- ? projectClaudeDir
56
- : path.resolve(os.homedir(), '.claude');
57
- const targetFile = path.resolve(targetDir, 'tts-provider.txt');
58
- // Verify resolved path stays within targetDir (path traversal guard)
59
- if (!targetFile.startsWith(targetDir + path.sep) && targetFile !== targetDir) return;
60
- fs.mkdirSync(targetDir, { recursive: true });
61
- fs.writeFileSync(targetFile, provider, 'utf8');
62
- } catch {
63
- // Non-fatal — config.json is the authoritative source
64
- }
65
- }
66
-
67
- /**
68
- * Returns an array of installed/available TTS providers.
69
- * Detection uses `which` binary check. Always returns at least ['piper']
70
- * as graceful degradation (piper is the primary supported provider).
71
- * @returns {string[]}
72
- */
73
- getInstalledProviders() {
74
- if (this._installedProviders) return this._installedProviders;
75
-
76
- const providers = [];
77
-
78
- if (this._isAvailable('piper')) providers.push('piper');
79
- if (this._isAvailable('soprano')) providers.push('soprano');
80
-
81
- // macOS Say (darwin only)
82
- if (process.platform === 'darwin' && this._isAvailable('say')) {
83
- providers.push('macos');
84
- }
85
-
86
- // Graceful degradation: always return at least piper
87
- if (providers.length === 0) providers.push('piper');
88
-
89
- this._installedProviders = providers;
90
- return providers;
91
- }
92
-
93
- // ---------------------------------------------------------------------------
94
- // Voice
95
-
96
- /**
97
- * Returns the currently active voice ID from config.
98
- * Defaults to 'en_US-amy-medium' if not configured.
99
- * @returns {string}
100
- */
101
- getActiveVoiceId() {
102
- return this._config.getConfig().voice ?? 'en_US-amy-medium';
103
- }
104
-
105
- /**
106
- * Sets the active voice ID in config.
107
- * @param {string} voiceId
108
- */
109
- setActiveVoice(voiceId) {
110
- this._config.set('voice', voiceId);
111
- }
112
-
113
- // ---------------------------------------------------------------------------
114
- // Private
115
-
116
- /**
117
- * Check if a binary is available in PATH using `which`.
118
- * Binary names are all hardcoded (not user input) — safe from injection.
119
- * @param {string} binary - hardcoded binary name ('piper', 'soprano', 'say')
120
- * @returns {boolean}
121
- */
122
- _isAvailable(binary) {
123
- try {
124
- execFileSync('which', [binary], { stdio: 'ignore', timeout: 2000 });
125
- return true;
126
- } catch {
127
- return false;
128
- }
129
- }
130
- }
131
-
132
- export default ProviderService;
1
+ /**
2
+ * AgentVibes Provider Service
3
+ * Story 7.1: Provider & Voice Settings Group
4
+ *
5
+ * Detects installed TTS providers, reads/writes active provider and voice
6
+ * through ConfigService. Gracefully degrades when detection fails.
7
+ */
8
+
9
+ import { execFileSync } from 'node:child_process';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import os from 'node:os';
13
+
14
+ export class ProviderService {
15
+ /**
16
+ * @param {import('./config-service.js').ConfigService} configService
17
+ */
18
+ constructor(configService) {
19
+ this._config = configService;
20
+ this._installedProviders = null; // cached after first detection
21
+ }
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Provider
25
+
26
+ /**
27
+ * Returns the currently active TTS provider from config.
28
+ * Defaults to 'piper' if not configured.
29
+ * @returns {string}
30
+ */
31
+ getActiveProvider() {
32
+ return this._config.getConfig().provider ?? 'piper';
33
+ }
34
+
35
+ /**
36
+ * Sets the active TTS provider in config AND syncs to .claude/tts-provider.txt
37
+ * so the shell hooks (play-tts.sh → provider-manager.sh) pick up the change.
38
+ * @param {string} provider
39
+ */
40
+ setActiveProvider(provider) {
41
+ this._config.set('provider', provider);
42
+ this._syncProviderFile(provider);
43
+ }
44
+
45
+ /**
46
+ * Write provider to .claude/tts-provider.txt so shell hooks stay in sync.
47
+ * Writes to projectRoot/.claude/tts-provider.txt if .claude/ exists there,
48
+ * otherwise falls back to ~/.claude/tts-provider.txt.
49
+ * @param {string} provider
50
+ */
51
+ _syncProviderFile(provider) {
52
+ try {
53
+ const projectClaudeDir = path.resolve(this._config.getProjectRoot(), '.claude');
54
+ const targetDir = fs.existsSync(projectClaudeDir)
55
+ ? projectClaudeDir
56
+ : path.resolve(os.homedir(), '.claude');
57
+ const targetFile = path.resolve(targetDir, 'tts-provider.txt');
58
+ // Verify resolved path stays within targetDir (path traversal guard)
59
+ if (!targetFile.startsWith(targetDir + path.sep) && targetFile !== targetDir) return;
60
+ fs.mkdirSync(targetDir, { recursive: true });
61
+ fs.writeFileSync(targetFile, provider, 'utf8');
62
+ } catch {
63
+ // Non-fatal — config.json is the authoritative source
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Returns an array of installed/available TTS providers.
69
+ * Detection uses `which` binary check. Always returns at least ['piper']
70
+ * as graceful degradation (piper is the primary supported provider).
71
+ * @returns {string[]}
72
+ */
73
+ getInstalledProviders() {
74
+ if (this._installedProviders) return this._installedProviders;
75
+
76
+ const providers = [];
77
+
78
+ if (this._isAvailable('piper')) providers.push('piper');
79
+ if (this._isAvailable('soprano')) providers.push('soprano');
80
+
81
+ // macOS Say (darwin only)
82
+ if (process.platform === 'darwin' && this._isAvailable('say')) {
83
+ providers.push('macos');
84
+ }
85
+
86
+ // Graceful degradation: always return at least piper
87
+ if (providers.length === 0) providers.push('piper');
88
+
89
+ this._installedProviders = providers;
90
+ return providers;
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Voice
95
+
96
+ /**
97
+ * Returns the currently active voice ID from config.
98
+ * Defaults to 'en_US-amy-medium' if not configured.
99
+ * @returns {string}
100
+ */
101
+ getActiveVoiceId() {
102
+ return this._config.getConfig().voice ?? 'en_US-amy-medium';
103
+ }
104
+
105
+ /**
106
+ * Sets the active voice ID in config.
107
+ * @param {string} voiceId
108
+ */
109
+ setActiveVoice(voiceId) {
110
+ this._config.set('voice', voiceId);
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Private
115
+
116
+ /**
117
+ * Check if a binary is available in PATH using `which`.
118
+ * Binary names are all hardcoded (not user input) — safe from injection.
119
+ * @param {string} binary - hardcoded binary name ('piper', 'soprano', 'say')
120
+ * @returns {boolean}
121
+ */
122
+ _isAvailable(binary) {
123
+ try {
124
+ execFileSync('which', [binary], { stdio: 'ignore', timeout: 2000 });
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ }
131
+
132
+ export default ProviderService;