agentvibes 4.6.8 → 5.1.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 (40) hide show
  1. package/.agentvibes/bmad-voice-map.json +104 -0
  2. package/.agentvibes/config.json +13 -12
  3. package/.agentvibes/copilot-sessions.log +4 -0
  4. package/.claude/audio/tracks/Drifting Down the Hall.mp3 +0 -0
  5. package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
  6. package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -0
  7. package/.claude/audio/tracks/README.md +51 -52
  8. package/.claude/config/audio-effects-bmad.cfg +50 -0
  9. package/.claude/config/audio-effects.cfg +4 -4
  10. package/.claude/config/background-music-enabled.txt +1 -0
  11. package/.claude/config/personality.txt +1 -0
  12. package/.claude/hooks/play-tts-piper.sh +3 -1
  13. package/.claude/hooks/play-tts.sh +380 -301
  14. package/.claude/hooks/session-start-tts.sh +81 -81
  15. package/.claude/hooks-windows/audio-processor.ps1 +181 -0
  16. package/.claude/hooks-windows/play-tts-piper.ps1 +259 -245
  17. package/.claude/hooks-windows/play-tts.ps1 +28 -6
  18. package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
  19. package/README.md +112 -6
  20. package/RELEASE_NOTES.md +83 -0
  21. package/bin/bmad-speak.js +16 -8
  22. package/mcp-server/server.py +15 -8
  23. package/package.json +1 -1
  24. package/src/console/app.js +899 -897
  25. package/src/console/footer-config.js +50 -50
  26. package/src/console/navigation.js +65 -65
  27. package/src/console/tabs/agents-tab.js +1899 -1886
  28. package/src/console/tabs/music-tab.js +1076 -1039
  29. package/src/console/tabs/placeholder-tab.js +81 -80
  30. package/src/console/tabs/settings-tab.js +941 -3988
  31. package/src/console/tabs/setup-tab.js +2071 -0
  32. package/src/console/tabs/voices-tab.js +1843 -1714
  33. package/src/console/widgets/format-utils.js +92 -89
  34. package/src/console/widgets/track-picker.js +325 -322
  35. package/src/installer.js +6147 -6092
  36. package/src/services/llm-provider-service.js +486 -0
  37. package/src/services/navigation-service.js +123 -123
  38. package/src/services/tts-engine-service.js +69 -0
  39. package/.claude/audio/tracks/dreamy_house_loop.mp3 +0 -0
  40. package/src/console/tabs/install-tab.js +0 -1081
@@ -0,0 +1,486 @@
1
+ /**
2
+ * AgentVibes — LLM Provider Service
3
+ *
4
+ * Extracted from llm-providers-tab.js: all provider logic as a standalone service.
5
+ * Config format: llm:key|effects|bgTrack|bgVolume|voice|pretext|ttsEngine
6
+ */
7
+
8
+ import path from 'node:path';
9
+ import fs from 'node:fs/promises';
10
+ import fsSync from 'node:fs';
11
+
12
+ // ── Provider definitions ────────────────────────────────────────────────────
13
+
14
+ export const PROVIDERS = [
15
+ {
16
+ id: 'claude-code',
17
+ name: 'Claude Code',
18
+ desc: 'Anthropic CLI agent — hooks + MCP server',
19
+ },
20
+ {
21
+ id: 'github-copilot',
22
+ name: 'GitHub Copilot',
23
+ desc: 'VS Code Copilot Chat — .vscode/mcp.json + instructions',
24
+ },
25
+ {
26
+ id: 'openai-codex',
27
+ name: 'OpenAI Codex',
28
+ desc: 'OpenAI CLI agent — .codex/config.toml + AGENTS.md',
29
+ },
30
+ ];
31
+
32
+ // ── Provider install-checks ─────────────────────────────────────────────────
33
+
34
+ export async function checkClaudeInstalled(targetDir) {
35
+ try {
36
+ await fs.access(path.join(targetDir, '.claude', 'hooks'));
37
+ return true;
38
+ } catch {
39
+ try {
40
+ await fs.access(path.join(targetDir, '.claude', 'hooks-windows'));
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+ }
47
+
48
+ export async function checkCopilotInstalled(targetDir) {
49
+ try {
50
+ const content = await fs.readFile(path.join(targetDir, '.vscode', 'mcp.json'), 'utf8');
51
+ const parsed = JSON.parse(content);
52
+ return !!(parsed?.servers?.agentvibes);
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ export async function checkCodexInstalled(targetDir) {
59
+ try {
60
+ const content = await fs.readFile(path.join(targetDir, '.codex', 'config.toml'), 'utf8');
61
+ return content.includes('[mcp_servers.agentvibes]');
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ // ── Claude Code install ────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Create .mcp.json in target directory if it doesn't exist.
71
+ * Also copies hooks, commands, config, personality, plugin, and bmad config files.
72
+ */
73
+ export async function installClaudeMcp(targetDir) {
74
+ const mcpConfigPath = path.join(targetDir, '.mcp.json');
75
+
76
+ const mcpConfig = {
77
+ mcpServers: {
78
+ agentvibes: {
79
+ command: 'npx',
80
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
81
+ },
82
+ },
83
+ };
84
+
85
+ try {
86
+ let mcpCreated = false;
87
+ try {
88
+ await fs.access(mcpConfigPath);
89
+ // Already exists — merge agentvibes key if missing
90
+ try {
91
+ const existing = JSON.parse(await fs.readFile(mcpConfigPath, 'utf8'));
92
+ if (!existing.mcpServers?.agentvibes) {
93
+ existing.mcpServers = existing.mcpServers || {};
94
+ existing.mcpServers.agentvibes = mcpConfig.mcpServers.agentvibes;
95
+ await fs.writeFile(mcpConfigPath, JSON.stringify(existing, null, 2) + '\n');
96
+ mcpCreated = true;
97
+ }
98
+ } catch { /* parse error — don't corrupt */ }
99
+ } catch {
100
+ // File doesn't exist — create it
101
+ await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
102
+ mcpCreated = true;
103
+ }
104
+
105
+ // Copy hooks, commands, config, personality, plugin, bmad config files
106
+ const silentSpinner = { start: () => {}, succeed: () => {}, fail: () => {} };
107
+ const installer = await import('../installer.js');
108
+ await installer.copyHookFiles(targetDir, silentSpinner);
109
+ await installer.copyCommandFiles(targetDir, silentSpinner);
110
+ await installer.copyConfigFiles(targetDir, silentSpinner);
111
+ await installer.copyPersonalityFiles(targetDir, silentSpinner);
112
+ await installer.copyPluginFiles(targetDir, silentSpinner);
113
+ await installer.copyBmadConfigFiles(targetDir, silentSpinner);
114
+ await installer.copyBackgroundMusicFiles(targetDir, silentSpinner);
115
+
116
+ return { success: true, mcpCreated };
117
+ } catch (err) {
118
+ return { success: false, error: err.message };
119
+ }
120
+ }
121
+
122
+ export async function removeClaudeMcp(targetDir) {
123
+ const mcpConfigPath = path.join(targetDir, '.mcp.json');
124
+ try {
125
+ const content = await fs.readFile(mcpConfigPath, 'utf8');
126
+ const parsed = JSON.parse(content);
127
+ if (parsed.mcpServers?.agentvibes) {
128
+ delete parsed.mcpServers.agentvibes;
129
+ // Only delete file if mcpServers is empty AND no other top-level keys
130
+ const noServers = Object.keys(parsed.mcpServers).length === 0;
131
+ const noOtherKeys = Object.keys(parsed).length === 1;
132
+ if (noServers && noOtherKeys) {
133
+ await fs.unlink(mcpConfigPath);
134
+ } else {
135
+ await fs.writeFile(mcpConfigPath, JSON.stringify(parsed, null, 2) + '\n');
136
+ }
137
+ }
138
+ } catch { /* file doesn't exist or can't parse — nothing to remove */ }
139
+ return { success: true };
140
+ }
141
+
142
+ /**
143
+ * Full uninstall: remove MCP entry + all AgentVibes files from the project.
144
+ * Does NOT touch user's own .claude/ settings (settings.json, CLAUDE.md etc.).
145
+ */
146
+ export async function uninstallClaude(targetDir) {
147
+ const removed = [];
148
+
149
+ // 1. Remove MCP entry
150
+ await removeClaudeMcp(targetDir);
151
+ removed.push('.mcp.json (agentvibes entry)');
152
+
153
+ // 2. Remove AgentVibes directories
154
+ const dirs = [
155
+ ['.claude', 'commands', 'agent-vibes'],
156
+ ['.claude', 'hooks'],
157
+ ['.claude', 'hooks-windows'],
158
+ ['.claude', 'personalities'],
159
+ ['.claude', 'output-styles'],
160
+ ['.claude', 'plugins'],
161
+ ['.claude', 'audio'],
162
+ ['.claude', 'config'],
163
+ ['.agentvibes'],
164
+ ];
165
+
166
+ for (const parts of dirs) {
167
+ const dirPath = path.join(targetDir, ...parts);
168
+ try {
169
+ await fs.rm(dirPath, { recursive: true, force: true });
170
+ removed.push(parts.join('/'));
171
+ } catch { /* doesn't exist */ }
172
+ }
173
+
174
+ // 3. Remove AgentVibes config files from .claude/
175
+ const configFiles = [
176
+ 'tts-voice.txt', 'tts-provider.txt', 'tts-personality.txt',
177
+ 'tts-verbosity.txt', 'tts-translate.txt', 'tts-target-voice.txt',
178
+ 'tts-target-language.txt', 'tts-language.txt', 'tts-speech-rate.txt',
179
+ 'tts-target-speech-rate.txt', 'piper-speech-rate.txt',
180
+ 'piper-target-speech-rate.txt', 'personalities.json',
181
+ 'github-star-reminder.txt', 'piper-voices-dir.txt',
182
+ 'verbosity.txt', 'personality.txt', 'intro-text.txt',
183
+ 'reverb-level.txt', 'background-music-enabled.txt',
184
+ 'background-music-volume.txt',
185
+ ];
186
+
187
+ for (const file of configFiles) {
188
+ try {
189
+ await fs.unlink(path.join(targetDir, '.claude', file));
190
+ } catch { /* doesn't exist */ }
191
+ }
192
+
193
+ // 4. Remove settings.json hook entries if present
194
+ const settingsPath = path.join(targetDir, '.claude', 'settings.json');
195
+ try {
196
+ const content = await fs.readFile(settingsPath, 'utf8');
197
+ const settings = JSON.parse(content);
198
+ let changed = false;
199
+ if (settings.hooks) {
200
+ for (const hookKey of Object.keys(settings.hooks)) {
201
+ const hooks = settings.hooks[hookKey];
202
+ if (Array.isArray(hooks)) {
203
+ const filtered = hooks.filter(h =>
204
+ !(h.command && (h.command.includes('agentvibes') || h.command.includes('play-tts') || h.command.includes('bmad-speak'))));
205
+ if (filtered.length !== hooks.length) {
206
+ settings.hooks[hookKey] = filtered;
207
+ changed = true;
208
+ }
209
+ }
210
+ }
211
+ }
212
+ if (changed) {
213
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
214
+ removed.push('.claude/settings.json (hooks cleaned)');
215
+ }
216
+ } catch { /* no settings or parse error */ }
217
+
218
+ return { success: true, removed };
219
+ }
220
+
221
+ // ── Copilot install/remove ──────────────────────────────────────────────────
222
+
223
+ export async function installCopilotMcp(targetDir) {
224
+ const vscodeDir = path.join(targetDir, '.vscode');
225
+ const mcpJsonPath = path.join(vscodeDir, 'mcp.json');
226
+
227
+ const agentvibesServer = {
228
+ type: 'stdio',
229
+ command: 'npx',
230
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
231
+ };
232
+
233
+ try {
234
+ await fs.mkdir(vscodeDir, { recursive: true });
235
+ let mcpConfig = { servers: {} };
236
+ try {
237
+ const existing = await fs.readFile(mcpJsonPath, 'utf8');
238
+ const parsed = JSON.parse(existing);
239
+ if (parsed && typeof parsed === 'object') {
240
+ mcpConfig = parsed;
241
+ if (!mcpConfig.servers) mcpConfig.servers = {};
242
+ }
243
+ } catch { /* new file */ }
244
+
245
+ mcpConfig.servers.agentvibes = agentvibesServer;
246
+ await fs.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
247
+ return { success: true };
248
+ } catch (err) {
249
+ return { success: false, error: err.message };
250
+ }
251
+ }
252
+
253
+ export async function removeCopilotMcp(targetDir) {
254
+ const mcpJsonPath = path.join(targetDir, '.vscode', 'mcp.json');
255
+ try {
256
+ const content = await fs.readFile(mcpJsonPath, 'utf8');
257
+ const parsed = JSON.parse(content);
258
+ if (parsed?.servers?.agentvibes) {
259
+ delete parsed.servers.agentvibes;
260
+ if (Object.keys(parsed.servers).length === 0) {
261
+ await fs.unlink(mcpJsonPath);
262
+ } else {
263
+ await fs.writeFile(mcpJsonPath, JSON.stringify(parsed, null, 2) + '\n');
264
+ }
265
+ }
266
+ return { success: true };
267
+ } catch {
268
+ return { success: true }; // Already gone
269
+ }
270
+ }
271
+
272
+ export async function installCopilotInstructions(targetDir, packageDir) {
273
+ const destPath = path.join(targetDir, '.github', 'copilot-instructions.md');
274
+ const srcPath = path.join(packageDir, '.github', 'copilot-instructions.md');
275
+ try {
276
+ await fs.mkdir(path.join(targetDir, '.github'), { recursive: true });
277
+ const content = await fs.readFile(srcPath, 'utf8');
278
+ await fs.writeFile(destPath, content);
279
+ } catch { /* best effort */ }
280
+ }
281
+
282
+ export async function removeCopilotInstructions(targetDir) {
283
+ try {
284
+ await fs.unlink(path.join(targetDir, '.github', 'copilot-instructions.md'));
285
+ } catch { /* already gone */ }
286
+ }
287
+
288
+ // ── Codex install/remove ────────────────────────────────────────────────────
289
+
290
+ export async function installCodexMcp(targetDir) {
291
+ const codexDir = path.join(targetDir, '.codex');
292
+ const tomlPath = path.join(codexDir, 'config.toml');
293
+
294
+ try {
295
+ await fs.mkdir(codexDir, { recursive: true });
296
+ let existing = '';
297
+ try { existing = await fs.readFile(tomlPath, 'utf8'); } catch { /* new file */ }
298
+ const content = buildCodexToml(existing);
299
+ await fs.writeFile(tomlPath, content);
300
+ return { success: true };
301
+ } catch (err) {
302
+ return { success: false, error: err.message };
303
+ }
304
+ }
305
+
306
+ export async function removeCodexMcp(targetDir) {
307
+ const tomlPath = path.join(targetDir, '.codex', 'config.toml');
308
+ try {
309
+ const content = await fs.readFile(tomlPath, 'utf8');
310
+ const lines = content.split('\n');
311
+ const filtered = [];
312
+ let skipping = false;
313
+ for (const line of lines) {
314
+ if (line.trim() === '[mcp_servers.agentvibes]') {
315
+ skipping = true;
316
+ continue;
317
+ }
318
+ if (skipping && line.startsWith('[')) {
319
+ skipping = false;
320
+ }
321
+ if (!skipping) filtered.push(line);
322
+ }
323
+ const result = filtered.join('\n').trim();
324
+ if (!result) {
325
+ await fs.unlink(tomlPath);
326
+ } else {
327
+ await fs.writeFile(tomlPath, result + '\n');
328
+ }
329
+ return { success: true };
330
+ } catch {
331
+ return { success: true }; // Already gone
332
+ }
333
+ }
334
+
335
+ export function buildCodexToml(existingContent = '') {
336
+ const serverBlock = [
337
+ '[mcp_servers.agentvibes]',
338
+ 'command = "npx"',
339
+ 'args = ["-y", "--package=agentvibes", "agentvibes-mcp-server"]',
340
+ ].join('\n');
341
+
342
+ if (!existingContent.trim()) return serverBlock + '\n';
343
+
344
+ // Remove existing agentvibes block if present, then append fresh
345
+ const lines = existingContent.split('\n');
346
+ const filtered = [];
347
+ let skipping = false;
348
+ for (const line of lines) {
349
+ if (line.trim() === '[mcp_servers.agentvibes]') {
350
+ skipping = true;
351
+ continue;
352
+ }
353
+ if (skipping && line.startsWith('[')) {
354
+ skipping = false;
355
+ }
356
+ if (!skipping) filtered.push(line);
357
+ }
358
+
359
+ let result = filtered.join('\n').trimEnd();
360
+ if (result.length) result += '\n\n';
361
+ return result + serverBlock + '\n';
362
+ }
363
+
364
+ export async function installCodexInstructions(targetDir, packageDir) {
365
+ const srcPath = path.join(packageDir, '.codex', 'AGENTS.md');
366
+ try {
367
+ const content = await fs.readFile(srcPath, 'utf8');
368
+ await fs.mkdir(path.join(targetDir, '.codex'), { recursive: true });
369
+ await fs.writeFile(path.join(targetDir, '.codex', 'AGENTS.md'), content);
370
+ await fs.writeFile(path.join(targetDir, 'AGENTS.md'), content);
371
+ } catch { /* best effort */ }
372
+ }
373
+
374
+ export async function installCodexHooks(targetDir, packageDir) {
375
+ const destDir = path.join(targetDir, '.codex', 'hooks');
376
+ const srcDir = path.join(packageDir, '.codex', 'hooks');
377
+ try {
378
+ await fs.mkdir(destDir, { recursive: true });
379
+ for (const file of ['init-agentvibes.sh', 'init-agentvibes.ps1']) {
380
+ try {
381
+ const content = await fs.readFile(path.join(srcDir, file), 'utf8');
382
+ await fs.writeFile(path.join(destDir, file), content);
383
+ } catch { /* best effort */ }
384
+ }
385
+ } catch { /* best effort */ }
386
+ }
387
+
388
+ export async function removeCodexInstructions(targetDir) {
389
+ try {
390
+ await fs.unlink(path.join(targetDir, '.codex', 'AGENTS.md'));
391
+ } catch { /* already gone */ }
392
+ try {
393
+ await fs.unlink(path.join(targetDir, 'AGENTS.md'));
394
+ } catch { /* already gone */ }
395
+ }
396
+
397
+ export async function removeCodexHooks(targetDir) {
398
+ const hooksDir = path.join(targetDir, '.codex', 'hooks');
399
+ try {
400
+ await fs.unlink(path.join(hooksDir, 'init-agentvibes.sh'));
401
+ } catch { /* already gone */ }
402
+ try {
403
+ await fs.unlink(path.join(hooksDir, 'init-agentvibes.ps1'));
404
+ } catch { /* already gone */ }
405
+ try {
406
+ await fs.rmdir(hooksDir);
407
+ } catch { /* not empty or gone */ }
408
+ }
409
+
410
+ // ── Config path resolution ──────────────────────────────────────────────────
411
+
412
+ export function resolveCfgPath(targetDir) {
413
+ const localCfg = path.join(targetDir, '.claude', 'config', 'audio-effects.cfg');
414
+ const homeDir = process.env.USERPROFILE || process.env.HOME || '';
415
+ const globalCfg = path.join(homeDir, '.claude', 'config', 'audio-effects.cfg');
416
+ return fsSync.existsSync(localCfg) ? localCfg : globalCfg;
417
+ }
418
+
419
+ // ── LLM config read/write ───────────────────────────────────────────────────
420
+
421
+ /**
422
+ * Read per-LLM audio config from audio-effects.cfg.
423
+ * Format: llm:key|effects|bgTrack|bgVolume|voice|pretext|ttsEngine
424
+ * Handles old 6-field format gracefully (ttsEngine defaults to '').
425
+ */
426
+ export function loadLlmConfigSync(llmKey, targetDir) {
427
+ const cfgKey = `llm:${llmKey}`;
428
+ const resolvedTargetDir = targetDir || process.env.INIT_CWD || process.cwd();
429
+ const cfgPaths = [
430
+ path.join(resolvedTargetDir, '.claude', 'config', 'audio-effects.cfg'),
431
+ path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude', 'config', 'audio-effects.cfg'),
432
+ ];
433
+
434
+ for (const cfgPath of cfgPaths) {
435
+ try {
436
+ const content = fsSync.readFileSync(cfgPath, 'utf8');
437
+ for (const line of content.split('\n')) {
438
+ if (line.startsWith(cfgKey + '|')) {
439
+ const parts = line.split('|');
440
+ return {
441
+ effects: (parts[1] || '').trim(),
442
+ bgTrack: (parts[2] || '').trim(),
443
+ bgVolume: (parts[3] || '0.15').trim(),
444
+ voice: (parts[4] || '').trim(),
445
+ pretext: (parts[5] || '').trim(),
446
+ ttsEngine: (parts[6] || '').trim(), // new field — empty if old format
447
+ sourcePath: cfgPath,
448
+ };
449
+ }
450
+ }
451
+ } catch { /* file not found */ }
452
+ }
453
+ return { effects: '', bgTrack: '', bgVolume: '0.15', voice: '', pretext: '', ttsEngine: '', sourcePath: '' };
454
+ }
455
+
456
+ /**
457
+ * Write per-LLM audio config to audio-effects.cfg.
458
+ * Format: llm:key|effects|bgTrack|bgVolume|voice|pretext|ttsEngine
459
+ */
460
+ export function saveLlmConfigSync(llmKey, config, targetDir) {
461
+ const cfgKey = `llm:${llmKey}`;
462
+ // Sanitize pipe chars in user-editable fields to prevent config format corruption
463
+ const sanitize = (v) => (v || '').replace(/\|/g, '');
464
+ const cfgLine = `${cfgKey}|${sanitize(config.effects)}|${sanitize(config.bgTrack)}|${config.bgVolume}|${sanitize(config.voice)}|${sanitize(config.pretext)}|${sanitize(config.ttsEngine)}`;
465
+ const resolvedTargetDir = targetDir || process.env.INIT_CWD || process.cwd();
466
+ const cfgPath = config.sourcePath || resolveCfgPath(resolvedTargetDir);
467
+
468
+ try {
469
+ let content = '';
470
+ try { content = fsSync.readFileSync(cfgPath, 'utf8'); } catch { /* new file */ }
471
+
472
+ const lines = content.split('\n');
473
+ let found = false;
474
+ for (let i = 0; i < lines.length; i++) {
475
+ if (lines[i].startsWith(cfgKey + '|')) {
476
+ lines[i] = cfgLine;
477
+ found = true;
478
+ break;
479
+ }
480
+ }
481
+ if (!found) lines.push(cfgLine);
482
+
483
+ fsSync.mkdirSync(path.dirname(cfgPath), { recursive: true });
484
+ fsSync.writeFileSync(cfgPath, lines.join('\n'));
485
+ } catch { /* best effort */ }
486
+ }