agentvibes 5.11.2 → 5.12.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 (54) hide show
  1. package/.claude/config/audio-effects.cfg +1 -1
  2. package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
  3. package/.claude/github-star-reminder.txt +1 -1
  4. package/.claude/hooks/audio-processor.sh +1 -1
  5. package/.claude/hooks/background-music-manager.sh +2 -1
  6. package/.claude/hooks/bmad-speak-enhanced.sh +37 -22
  7. package/.claude/hooks/bmad-speak.sh +26 -8
  8. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +1 -1
  9. package/.claude/hooks/play-tts-kokoro.sh +9 -0
  10. package/.claude/hooks/play-tts-piper.sh +13 -1
  11. package/.claude/hooks/play-tts-ssh-remote.sh +117 -7
  12. package/.claude/hooks/play-tts-termux-ssh.sh +1 -1
  13. package/.claude/hooks/play-tts.sh +106 -13
  14. package/.claude/hooks-windows/background-music-manager.ps1 +2 -1
  15. package/.claude/hooks-windows/play-tts-kokoro.ps1 +14 -0
  16. package/.claude/hooks-windows/play-tts.ps1 +163 -30
  17. package/.claude/hooks-windows/tts-watcher.ps1 +55 -0
  18. package/.claude/hooks-windows/voice-manager-windows.ps1 +101 -1
  19. package/README.md +11 -6
  20. package/RELEASE_NOTES.md +67 -0
  21. package/bin/mcp-server.sh +6 -19
  22. package/bin/resolve-utterance.js +137 -0
  23. package/mcp-server/server.py +280 -99
  24. package/mcp-server/test_mcp_correctness.py +486 -0
  25. package/mcp-server/test_windows_script_parity.py +341 -336
  26. package/package.json +3 -2
  27. package/setup-windows.ps1 +867 -815
  28. package/src/console/app.js +14 -11
  29. package/src/console/footer-config.js +0 -4
  30. package/src/console/navigation.js +0 -1
  31. package/src/console/tabs/agents-tab.js +96 -11
  32. package/src/console/tabs/music-tab.js +108 -3
  33. package/src/console/tabs/placeholder-tab.js +0 -2
  34. package/src/console/tabs/settings-tab.js +41 -2
  35. package/src/console/tabs/setup-tab.js +34 -6
  36. package/src/console/tabs/voices-tab.js +19 -9
  37. package/src/console/widgets/track-picker.js +3 -3
  38. package/src/i18n/de.js +0 -1
  39. package/src/i18n/en.js +0 -1
  40. package/src/i18n/es.js +0 -1
  41. package/src/i18n/fr.js +0 -1
  42. package/src/i18n/hi.js +0 -1
  43. package/src/i18n/ja.js +0 -1
  44. package/src/i18n/ko.js +0 -1
  45. package/src/i18n/pt.js +0 -1
  46. package/src/i18n/zh-CN.js +0 -1
  47. package/src/installer.js +205 -15
  48. package/src/services/config-service.js +264 -264
  49. package/src/services/llm-provider-service.js +7 -7
  50. package/src/services/navigation-service.js +1 -1
  51. package/src/services/utterance-loader.js +280 -0
  52. package/src/services/utterance-resolver.js +526 -0
  53. package/templates/agentvibes-receiver.sh +74 -12
  54. package/voice-assignments.json +8245 -0
@@ -0,0 +1,280 @@
1
+ /**
2
+ * File: src/services/utterance-loader.js
3
+ *
4
+ * AgentVibes — Utterance Loader (Story AVI-S8.5, Stage 2 bridge).
5
+ *
6
+ * The resolver (`utterance-resolver.js`) is a PURE function: (normalized inputs)
7
+ * → plan. It reads nothing. This loader is the impure half: it reads every
8
+ * config source once (files + env + argv), maps them onto the resolver's input
9
+ * bag, and hands the result to `resolveUtterance()`. The `bin/resolve-utterance.js`
10
+ * CLI wraps this so bash/PowerShell/Python players can obtain a finished plan as
11
+ * JSON and execute it verbatim — the players stop reading config themselves.
12
+ *
13
+ * Testability: `gatherInputs(ctx)` takes explicit `projectDir`/`homeDir`/`env`,
14
+ * so tests drive it with temp dirs and fake envs — no global-state coupling.
15
+ *
16
+ * Config-source precedence for each field is documented in
17
+ * docs/implementation-artifacts/8-5-precedence-map.md §A/§D. This loader only
18
+ * GATHERS candidates in that order; the resolver DECIDES.
19
+ *
20
+ * @related utterance-resolver.js, bin/resolve-utterance.js
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+
28
+ /** Read a file's trimmed contents, or undefined if missing/unreadable/empty. */
29
+ function readTrim(filePath) {
30
+ try {
31
+ const s = fs.readFileSync(filePath, 'utf8').trim();
32
+ return s === '' ? undefined : s;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ /** First existing file's trimmed content across an ordered path list. */
39
+ function firstFile(paths) {
40
+ for (const p of paths) {
41
+ const v = readTrim(p);
42
+ if (v !== undefined) return v;
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ /** True if the path exists (any type). */
48
+ function exists(p) {
49
+ try { fs.accessSync(p); return true; } catch { return false; }
50
+ }
51
+
52
+ /**
53
+ * Resolve the project root the same way the bash player does: an explicit
54
+ * --project-dir / CLAUDE_PROJECT_DIR wins; otherwise walk up from cwd looking
55
+ * for a `.claude` dir; else fall back to home.
56
+ */
57
+ function resolveProjectDir({ projectDir, env = {}, cwd, homeDir }) {
58
+ const explicit = projectDir || env.CLAUDE_PROJECT_DIR;
59
+ if (explicit && exists(path.join(explicit, '.claude'))) return explicit;
60
+ if (explicit) return explicit;
61
+ let dir = cwd || homeDir;
62
+ for (let i = 0; i < 40 && dir; i++) {
63
+ if (exists(path.join(dir, '.claude'))) return dir;
64
+ const parent = path.dirname(dir);
65
+ if (parent === dir) break;
66
+ dir = parent;
67
+ }
68
+ return homeDir;
69
+ }
70
+
71
+ /**
72
+ * Parse the `llm:<key>` row from audio-effects.cfg (7 pipe columns):
73
+ * llm:<key>|REVERB|BGFILE|BGVOL|VOICE|PRETEXT|ENGINE
74
+ * Searches the given ordered cfg paths; first file containing the row wins.
75
+ * Returns an object of the six value columns (or {} if not found).
76
+ */
77
+ function parseLlmRow(cfgPaths, llmKey) {
78
+ const key = llmKey.startsWith('llm:') ? llmKey : `llm:${llmKey}`;
79
+ for (const cfg of cfgPaths) {
80
+ let text;
81
+ try { text = fs.readFileSync(cfg, 'utf8'); } catch { continue; }
82
+ for (const line of text.split(/\r?\n/)) {
83
+ if (!line.startsWith(key + '|') && line !== key) continue;
84
+ const cols = line.split('|');
85
+ // cols[0] = 'llm:<key>'
86
+ return {
87
+ reverb: cols[1] || undefined,
88
+ bgFile: cols[2] || undefined,
89
+ bgVolume: cols[3] || undefined,
90
+ voice: cols[4] || undefined,
91
+ pretext: cols[5] || undefined,
92
+ engine: cols[6] || undefined,
93
+ };
94
+ }
95
+ }
96
+ return {};
97
+ }
98
+
99
+ /** Read a per-agent BMAD profile from bmad-voice-map.json (project then global). */
100
+ function readAgentProfile(agentName, projectDir, homeDir) {
101
+ if (!agentName) return {};
102
+ const candidates = [
103
+ path.join(projectDir, '.agentvibes', 'bmad-voice-map.json'),
104
+ path.join(homeDir, '.agentvibes', 'bmad-voice-map.json'),
105
+ ];
106
+ for (const p of candidates) {
107
+ try {
108
+ const map = JSON.parse(fs.readFileSync(p, 'utf8'));
109
+ const prof = map && map[agentName];
110
+ if (prof && typeof prof === 'object') return prof;
111
+ } catch { /* try next */ }
112
+ }
113
+ return {};
114
+ }
115
+
116
+ /** The single canonical LLM-key resolution (mirrors play-tts.sh / server.py). */
117
+ function resolveLlmKey({ llm, env = {} }) {
118
+ const clean = (v) => (v && /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(v) ? v : '');
119
+ let key = clean((llm || '').replace(/^llm:/, ''));
120
+ if (!key) key = clean((env.AGENTVIBES_LLM_KEY || '').replace(/^llm:/, ''));
121
+ if (!key) key = clean((env.AGENTVIBES_LLM || '').replace(/^llm:/, ''));
122
+ if (!key && String(env.CLAUDECODE || '').trim() === '1') key = 'claude-code';
123
+ if (!key) key = clean(env.AGENTVIBES_MCP_FALLBACK || '');
124
+ return `llm:${key || 'default'}`;
125
+ }
126
+
127
+ /**
128
+ * Gather the resolver input bag from all config sources.
129
+ *
130
+ * @param {object} ctx
131
+ * @param {string} ctx.text the utterance text
132
+ * @param {string} [ctx.voice] explicit voice arg (positional / -VoiceOverride)
133
+ * @param {string} [ctx.voiceSource] provenance of `voice` (see resolver VOICE_SOURCES);
134
+ * defaults to 'llm-echo' (the hook path).
135
+ * @param {string} [ctx.llm] --llm value
136
+ * @param {string} [ctx.projectDir] explicit project dir
137
+ * @param {string} ctx.homeDir user home (global config root)
138
+ * @param {string} [ctx.cwd] current dir (for project walk-up)
139
+ * @param {object} [ctx.env] environment map (defaults to {})
140
+ * @returns {object} normalized inputs for resolveUtterance()
141
+ */
142
+ function gatherInputs(ctx) {
143
+ const env = ctx.env || {};
144
+ const homeDir = ctx.homeDir;
145
+ const projectDir = resolveProjectDir(ctx);
146
+ const projClaude = path.join(projectDir, '.claude');
147
+ const homeClaude = path.join(homeDir, '.claude');
148
+ const projCfg = path.join(projClaude, 'config');
149
+ const homeCfg = path.join(homeClaude, 'config');
150
+
151
+ // The FULL 3-tier search the legacy bash player uses: the real user project
152
+ // (CLAUDE_PROJECT_DIR, = projectDir here) → the package/hooks project root →
153
+ // home. When the CLI is invoked with CLAUDE_PROJECT_DIR != PROJECT_ROOT (the
154
+ // MCP-from-elsewhere case), a per-LLM row (esp. its ENGINE column) configured
155
+ // only under PROJECT_ROOT would otherwise be invisible and the engine would
156
+ // silently revert to piper (loader 2-tier vs legacy 3-tier gap). packageRoot
157
+ // is the middle tier; it's skipped when unset or equal to projectDir.
158
+ const packageRoot = (ctx.packageRoot && ctx.packageRoot !== projectDir) ? ctx.packageRoot : null;
159
+ const pkgClaude = packageRoot ? path.join(packageRoot, '.claude') : null;
160
+ const pkgCfg = packageRoot ? path.join(pkgClaude, 'config') : null;
161
+
162
+ const llmKey = resolveLlmKey({ llm: ctx.llm, env });
163
+
164
+ const cfgPaths = [
165
+ path.join(projCfg, 'audio-effects.cfg'),
166
+ ...(pkgCfg ? [path.join(pkgCfg, 'audio-effects.cfg')] : []),
167
+ path.join(homeCfg, 'audio-effects.cfg'),
168
+ ];
169
+ const row = parseLlmRow(cfgPaths, llmKey);
170
+
171
+ const agentName = env.AGENTVIBES_AGENT_NAME;
172
+ const profile = readAgentProfile(agentName, projectDir, homeDir);
173
+ const profMusic = (profile && profile.backgroundMusic) || {};
174
+
175
+ // 3-tier file readers: project .claude → package-root .claude → home .claude
176
+ const tier = (name, sub = '') => firstFile([
177
+ path.join(projClaude, sub, name),
178
+ ...(pkgClaude ? [path.join(pkgClaude, sub, name)] : []),
179
+ path.join(homeClaude, sub, name),
180
+ ]);
181
+ const cfgTier = (name) => firstFile([
182
+ path.join(projCfg, name),
183
+ ...(pkgCfg ? [path.join(pkgCfg, name)] : []),
184
+ path.join(homeCfg, name),
185
+ ]);
186
+
187
+ return {
188
+ text: ctx.text ?? '',
189
+ voiceSource: ctx.voiceSource || 'llm-echo',
190
+
191
+ // ---- voice ----
192
+ explicitVoice: ctx.voice || undefined,
193
+ perLlmVoice: row.voice,
194
+ providerVoice: tier('tts-voice.txt'),
195
+ voiceModel: tier('tts-piper-model.txt'),
196
+ speakerId: tier('tts-piper-speaker-id.txt'),
197
+
198
+ // ---- engine / provider ----
199
+ perLlmEngine: row.engine,
200
+ forceProvider: env.AGENTVIBES_FORCE_PROVIDER || undefined,
201
+ providerEngine: tier('tts-provider.txt'),
202
+ receiverProvider: cfgTier('receiver-provider.txt'),
203
+
204
+ // ---- transport ----
205
+ providerTransport: (() => {
206
+ const p = tier('tts-provider.txt');
207
+ return (p === 'ssh-remote' || p === 'agentvibes-receiver' || p === 'termux-ssh') ? p : undefined;
208
+ })(),
209
+ // (per-LLM transport-config.json mode is read by the CLI when present; omitted
210
+ // here until the SSH port stage wires the full transport-config parse.)
211
+ sshHostEnv: env.AGENTVIBES_SSH_HOST || undefined,
212
+ sshHostLegacy: firstFile([
213
+ path.join(projClaude, 'ssh-remote-host.txt'),
214
+ path.join(homeClaude, 'ssh-remote-host.txt'),
215
+ ]),
216
+ sshPortEnv: env.AGENTVIBES_SSH_PORT || undefined,
217
+ sshKeyEnv: env.AGENTVIBES_SSH_KEY || undefined,
218
+
219
+ // ---- music ----
220
+ overrideMusicEnabled: env.AGENTVIBES_OVERRIDE_MUSIC !== undefined ? true : undefined,
221
+ overrideTrack: env.AGENTVIBES_OVERRIDE_MUSIC || undefined,
222
+ overrideVolume: env.AGENTVIBES_OVERRIDE_VOLUME || undefined,
223
+ profileMusicEnabled: profMusic.enabled,
224
+ profileTrack: profMusic.track,
225
+ profileVolume: profMusic.volume,
226
+ perLlmBgFile: row.bgFile,
227
+ perLlmBgVolume: row.bgVolume,
228
+ bgTrackFile: cfgTier('background-music-default.txt'),
229
+ bgVolumeFile: cfgTier('background-music-volume.txt'),
230
+ globalMusicEnabled: cfgTier('background-music-enabled.txt'),
231
+ testTrack: env.AGENTVIBES_TEST_TRACK || undefined,
232
+
233
+ // ---- reverb / effects ----
234
+ reverbOverride: env.AGENTVIBES_REVERB_OVERRIDE || undefined,
235
+ perLlmReverb: row.reverb,
236
+ reverbFile: cfgTier('reverb-level.txt'),
237
+ perLlmEffects: undefined,
238
+ effectsOverride: env.AGENTVIBES_OVERRIDE_EFFECTS || undefined,
239
+
240
+ // ---- pretext ----
241
+ noPretext: env.AGENTVIBES_NO_PRETEXT === '1' || undefined,
242
+ perLlmPretext: row.pretext,
243
+ pretextFile: cfgTier('tts-pretext.txt'),
244
+ introTextFile: cfgTier('intro-text.txt'),
245
+
246
+ // ---- speed ----
247
+ speechRate: firstFile([
248
+ path.join(projClaude, 'tts-speech-rate.txt'),
249
+ path.join(homeClaude, 'tts-speech-rate.txt'),
250
+ path.join(projCfg, 'tts-speed.txt'),
251
+ path.join(homeCfg, 'tts-speed.txt'),
252
+ ]),
253
+
254
+ // ---- personality / language ----
255
+ personalityOverride: ctx.personality || undefined,
256
+ personalityFile: tier('tts-personality.txt'),
257
+ languageFile: tier('tts-language.txt'),
258
+
259
+ // ---- mute (3-level) ----
260
+ projectUnmute: exists(path.join(projClaude, 'agentvibes-unmuted')) || undefined,
261
+ projectMute: exists(path.join(projClaude, 'agentvibes-muted')) || undefined,
262
+ globalMute: exists(path.join(homeDir, '.agentvibes-muted')) || undefined,
263
+
264
+ // ---- misc ----
265
+ rdpModeExplicit: env.AGENTVIBES_RDP_MODE || undefined,
266
+ noPlay: env.AGENTVIBES_NO_PLAY || undefined,
267
+ noPlayback: env.AGENTVIBES_NO_PLAYBACK || undefined,
268
+ testModeEnv: env.AGENTVIBES_TEST_MODE || undefined,
269
+ projectDir,
270
+ llmKey,
271
+ };
272
+ }
273
+
274
+ export {
275
+ gatherInputs,
276
+ resolveProjectDir,
277
+ resolveLlmKey,
278
+ parseLlmRow,
279
+ readAgentProfile,
280
+ };