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.
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-processor.sh +1 -1
- package/.claude/hooks/background-music-manager.sh +2 -1
- package/.claude/hooks/bmad-speak-enhanced.sh +37 -22
- package/.claude/hooks/bmad-speak.sh +26 -8
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +1 -1
- package/.claude/hooks/play-tts-kokoro.sh +9 -0
- package/.claude/hooks/play-tts-piper.sh +13 -1
- package/.claude/hooks/play-tts-ssh-remote.sh +117 -7
- package/.claude/hooks/play-tts-termux-ssh.sh +1 -1
- package/.claude/hooks/play-tts.sh +106 -13
- package/.claude/hooks-windows/background-music-manager.ps1 +2 -1
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +14 -0
- package/.claude/hooks-windows/play-tts.ps1 +163 -30
- package/.claude/hooks-windows/tts-watcher.ps1 +55 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +101 -1
- package/README.md +11 -6
- package/RELEASE_NOTES.md +67 -0
- package/bin/mcp-server.sh +6 -19
- package/bin/resolve-utterance.js +137 -0
- package/mcp-server/server.py +280 -99
- package/mcp-server/test_mcp_correctness.py +486 -0
- package/mcp-server/test_windows_script_parity.py +341 -336
- package/package.json +3 -2
- package/setup-windows.ps1 +867 -815
- package/src/console/app.js +14 -11
- package/src/console/footer-config.js +0 -4
- package/src/console/navigation.js +0 -1
- package/src/console/tabs/agents-tab.js +96 -11
- package/src/console/tabs/music-tab.js +108 -3
- package/src/console/tabs/placeholder-tab.js +0 -2
- package/src/console/tabs/settings-tab.js +41 -2
- package/src/console/tabs/setup-tab.js +34 -6
- package/src/console/tabs/voices-tab.js +19 -9
- package/src/console/widgets/track-picker.js +3 -3
- package/src/i18n/de.js +0 -1
- package/src/i18n/en.js +0 -1
- package/src/i18n/es.js +0 -1
- package/src/i18n/fr.js +0 -1
- package/src/i18n/hi.js +0 -1
- package/src/i18n/ja.js +0 -1
- package/src/i18n/ko.js +0 -1
- package/src/i18n/pt.js +0 -1
- package/src/i18n/zh-CN.js +0 -1
- package/src/installer.js +205 -15
- package/src/services/config-service.js +264 -264
- package/src/services/llm-provider-service.js +7 -7
- package/src/services/navigation-service.js +1 -1
- package/src/services/utterance-loader.js +280 -0
- package/src/services/utterance-resolver.js +526 -0
- package/templates/agentvibes-receiver.sh +74 -12
- 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
|
+
};
|