agentvibes 5.2.0 → 5.3.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 (52) hide show
  1. package/.claude/config/audio-effects.cfg +1 -1
  2. package/.claude/hooks/audio-cache-utils.sh +246 -246
  3. package/.claude/hooks/background-music-manager.sh +404 -404
  4. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  5. package/.claude/hooks/bmad-speak.sh +290 -290
  6. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  7. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  8. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  9. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  10. package/.claude/hooks/clean-audio-cache.sh +22 -22
  11. package/.claude/hooks/cleanup-cache.sh +106 -106
  12. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  13. package/.claude/hooks/download-extra-voices.sh +244 -244
  14. package/.claude/hooks/effects-manager.sh +268 -268
  15. package/.claude/hooks/github-star-reminder.sh +154 -154
  16. package/.claude/hooks/language-manager.sh +362 -362
  17. package/.claude/hooks/learn-manager.sh +492 -492
  18. package/.claude/hooks/macos-voice-manager.sh +205 -205
  19. package/.claude/hooks/migrate-background-music.sh +125 -125
  20. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  21. package/.claude/hooks/optimize-background-music.sh +87 -87
  22. package/.claude/hooks/path-resolver.sh +60 -60
  23. package/.claude/hooks/personality-manager.sh +448 -448
  24. package/.claude/hooks/piper-installer.sh +292 -292
  25. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  26. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  27. package/.claude/hooks/play-tts-ssh-remote.sh +104 -10
  28. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  29. package/.claude/hooks/play-tts.sh +31 -11
  30. package/.claude/hooks/prepare-release.sh +54 -54
  31. package/.claude/hooks/provider-commands.sh +617 -617
  32. package/.claude/hooks/provider-manager.sh +399 -399
  33. package/.claude/hooks/replay-target-audio.sh +95 -95
  34. package/.claude/hooks/sentiment-manager.sh +201 -201
  35. package/.claude/hooks/speed-manager.sh +291 -291
  36. package/.claude/hooks/stop-tts.sh +84 -84
  37. package/.claude/hooks/termux-installer.sh +261 -261
  38. package/.claude/hooks/translate-manager.sh +341 -341
  39. package/.claude/hooks/tts-queue-worker.sh +145 -145
  40. package/.claude/hooks/tts-queue.sh +165 -165
  41. package/.claude/hooks/voice-manager.sh +552 -548
  42. package/.claude/hooks-windows/bmad-party-speak.ps1 +5 -1
  43. package/.claude/hooks-windows/play-tts.ps1 +91 -59
  44. package/README.md +21 -2
  45. package/RELEASE_NOTES.md +130 -0
  46. package/bin/mcp-server.sh +206 -206
  47. package/mcp-server/server.py +35 -6
  48. package/package.json +1 -1
  49. package/src/console/tabs/setup-tab.js +68 -29
  50. package/src/console/tabs/voices-tab.js +9 -3
  51. package/src/installer.js +79 -213
  52. package/src/services/llm-provider-service.js +139 -75
@@ -125,53 +125,78 @@ export async function checkCodexInstalled(targetDir) {
125
125
  // ── Claude Code install ────────────────────────────────────────────────────
126
126
 
127
127
  /**
128
- * Create .mcp.json in target directory if it doesn't exist.
128
+ * Install AgentVibes for Claude Code.
129
+ *
130
+ * Writes .mcp.json to register the AgentVibes MCP server (enables natural
131
+ * language control: text_to_speech, get_config, set_voice, etc.).
132
+ *
133
+ * .mcp.json does NOT set AGENTVIBES_LLM because Copilot also reads it.
134
+ * Claude Code is auto-detected via CLAUDECODE=1 env var at runtime.
135
+ *
129
136
  * Also copies hooks, commands, config, personality, plugin, and bmad config files.
130
137
  */
131
138
  export async function installClaudeMcp(targetDir) {
132
139
  const mcpConfigPath = path.join(targetDir, '.mcp.json');
133
140
 
134
- // The agentvibes server entry for Claude Code's .mcp.json.
135
- //
136
- // IMPORTANT: no `env.AGENTVIBES_LLM` block here. GitHub Copilot CLI
137
- // also reads project-level `.mcp.json` with precedence over its own
138
- // `~/.copilot/mcp-config.json` — so if we set `AGENTVIBES_LLM=claude-code`
139
- // in `.mcp.json`, Copilot CLI picks up that value too and mis-routes.
140
- // Instead, the MCP server (mcp-server/server.py) auto-detects Claude
141
- // Code via the `CLAUDECODE=1` env var that Claude Code sets on every
142
- // subprocess it spawns. Copilot CLI does NOT set that var, so its
143
- // spawned MCP server correctly falls back to its own config.
141
+ // AGENTVIBES_MCP_FALLBACK=copilot is the fallback identity for non-Claude-Code
142
+ // tools reading .mcp.json (primarily VS Code Copilot, which reads .mcp.json
143
+ // with precedence over .vscode/mcp.json). Claude Code auto-detects via
144
+ // CLAUDECODE=1 which takes priority over the fallback in server.py.
144
145
  const agentvibesServer = {
145
146
  command: 'npx',
146
147
  args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
148
+ env: { AGENTVIBES_MCP_FALLBACK: 'copilot' },
147
149
  };
148
150
 
149
- const mcpConfig = {
150
- mcpServers: {
151
- agentvibes: agentvibesServer,
152
- },
153
- };
154
-
151
+ // MCP config and file copies are independent — report partial success
152
+ // when one succeeds but the other fails.
153
+ let mcpCreated = false;
154
+ let mcpError = null;
155
155
  try {
156
- let mcpCreated = false;
156
+ let existing = null;
157
+ let parseError = null;
157
158
  try {
158
- await fs.access(mcpConfigPath);
159
- // Already exists — merge / upgrade the agentvibes entry. This also
160
- // STRIPS any stale AGENTVIBES_LLM env block left over from v5.1.2..4
161
- // so Copilot CLI stops mis-routing.
162
- try {
163
- const existing = JSON.parse(await fs.readFile(mcpConfigPath, 'utf8'));
164
- existing.mcpServers = existing.mcpServers || {};
165
- existing.mcpServers.agentvibes = { ...agentvibesServer };
166
- await fs.writeFile(mcpConfigPath, JSON.stringify(existing, null, 2) + '\n');
167
- mcpCreated = true;
168
- } catch { /* parse error — don't corrupt */ }
169
- } catch {
170
- // File doesn't exist create it
171
- await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
172
- mcpCreated = true;
159
+ const raw = await fs.readFile(mcpConfigPath, 'utf8');
160
+ existing = JSON.parse(raw);
161
+ } catch (err) {
162
+ // ENOENT = new file (fine); anything else = malformed (report to caller)
163
+ if (err.code !== 'ENOENT') parseError = err;
164
+ }
165
+
166
+ if (parseError) {
167
+ throw new Error(`Existing ${mcpConfigPath} is malformed: ${parseError.message}`);
168
+ }
169
+
170
+ // Guard: non-object root
171
+ if (existing && (typeof existing !== 'object' || Array.isArray(existing))) {
172
+ throw new Error(`${mcpConfigPath} has a non-object root — please fix manually.`);
173
173
  }
174
174
 
175
+ if (existing) {
176
+ // Guard: mcpServers must be a plain object
177
+ if (!existing.mcpServers || typeof existing.mcpServers !== 'object' || Array.isArray(existing.mcpServers)) {
178
+ existing.mcpServers = {};
179
+ }
180
+ const current = existing.mcpServers.agentvibes;
181
+ // Strip any stale AGENTVIBES_LLM (from older versions — causes collisions)
182
+ if (current?.env?.AGENTVIBES_LLM) delete current.env.AGENTVIBES_LLM;
183
+ // Preserve user's other env keys, ensure AGENTVIBES_MCP_FALLBACK is set
184
+ const mergedEnv = { ...(current?.env ?? {}), AGENTVIBES_MCP_FALLBACK: 'copilot' };
185
+ existing.mcpServers.agentvibes = {
186
+ command: 'npx',
187
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
188
+ env: mergedEnv,
189
+ };
190
+ await fs.writeFile(mcpConfigPath, JSON.stringify(existing, null, 2) + '\n');
191
+ } else {
192
+ await fs.writeFile(mcpConfigPath, JSON.stringify({ mcpServers: { agentvibes: agentvibesServer } }, null, 2) + '\n');
193
+ }
194
+ mcpCreated = true;
195
+ } catch (err) {
196
+ mcpError = err.message;
197
+ }
198
+
199
+ try {
175
200
  // Copy hooks, commands, config, personality, plugin, bmad config files
176
201
  const silentSpinner = { start: () => {}, succeed: () => {}, fail: () => {} };
177
202
  const installer = await import('../installer.js');
@@ -184,20 +209,33 @@ export async function installClaudeMcp(targetDir) {
184
209
  await installer.copyBackgroundMusicFiles(targetDir, silentSpinner);
185
210
  ensureDefaultLlmConfigSync('claude-code', targetDir);
186
211
 
187
- return { success: true, mcpCreated };
212
+ // Explicitly write tts-provider.txt so `get_active_provider()` in
213
+ // provider-manager.sh doesn't silently fall back to "piper". Without
214
+ // this, headless servers with no audio device hit a confusing failure
215
+ // mode where TTS tries to synth locally and fails silently. Users
216
+ // can still change the provider via the Setup TUI or slash command.
217
+ const ttsProviderPath = path.join(targetDir, '.claude', 'tts-provider.txt');
218
+ try {
219
+ await fs.access(ttsProviderPath);
220
+ // Already exists — user has explicitly set a provider, don't clobber
221
+ } catch {
222
+ await fs.writeFile(ttsProviderPath, 'piper\n');
223
+ }
224
+
225
+ return { success: true, mcpCreated, mcpError };
188
226
  } catch (err) {
189
- return { success: false, error: err.message };
227
+ return { success: false, error: err.message, mcpError };
190
228
  }
191
229
  }
192
230
 
193
231
  export async function removeClaudeMcp(targetDir) {
194
- const mcpConfigPath = path.join(targetDir, '.mcp.json');
232
+ // Clean up .mcp.json agentvibes entry (legacy from older versions)
195
233
  try {
234
+ const mcpConfigPath = path.join(targetDir, '.mcp.json');
196
235
  const content = await fs.readFile(mcpConfigPath, 'utf8');
197
236
  const parsed = JSON.parse(content);
198
237
  if (parsed.mcpServers?.agentvibes) {
199
238
  delete parsed.mcpServers.agentvibes;
200
- // Only delete file if mcpServers is empty AND no other top-level keys
201
239
  const noServers = Object.keys(parsed.mcpServers).length === 0;
202
240
  const noOtherKeys = Object.keys(parsed).length === 1;
203
241
  if (noServers && noOtherKeys) {
@@ -217,9 +255,9 @@ export async function removeClaudeMcp(targetDir) {
217
255
  export async function uninstallClaude(targetDir) {
218
256
  const removed = [];
219
257
 
220
- // 1. Remove MCP entry
258
+ // 1. Remove legacy .mcp.json agentvibes entry if present
221
259
  await removeClaudeMcp(targetDir);
222
- removed.push('.mcp.json (agentvibes entry)');
260
+ removed.push('.mcp.json agentvibes entry (if present)');
223
261
 
224
262
  // 2. Remove AgentVibes directories
225
263
  const dirs = [
@@ -304,6 +342,7 @@ export async function installCopilotMcp(targetDir) {
304
342
  env: { AGENTVIBES_LLM: 'copilot' },
305
343
  };
306
344
 
345
+ let mcpError = null;
307
346
  try {
308
347
  await fs.mkdir(vscodeDir, { recursive: true });
309
348
  let mcpConfig = { servers: {} };
@@ -316,6 +355,8 @@ export async function installCopilotMcp(targetDir) {
316
355
  }
317
356
  } catch { /* new file */ }
318
357
 
358
+ // Clean up any old "agentvibes-copilot" entry from a prior attempt.
359
+ delete mcpConfig.servers['agentvibes-copilot'];
319
360
  mcpConfig.servers.agentvibes = agentvibesServer;
320
361
  await fs.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
321
362
 
@@ -325,34 +366,45 @@ export async function installCopilotMcp(targetDir) {
325
366
  // CLI reads ONLY from ~/.copilot/mcp-config.json per docs:
326
367
  // https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
327
368
  try {
328
- const copilotHome = process.env.COPILOT_HOME ||
329
- path.join(process.env.USERPROFILE || process.env.HOME || '', '.copilot');
330
- const copilotMcpPath = path.join(copilotHome, 'mcp-config.json');
331
- await fs.mkdir(copilotHome, { recursive: true });
332
- let cliConfig = { mcpServers: {} };
333
- try {
334
- const existingCli = await fs.readFile(copilotMcpPath, 'utf8');
335
- const parsedCli = JSON.parse(existingCli);
336
- if (parsedCli && typeof parsedCli === 'object') {
337
- cliConfig = parsedCli;
338
- if (!cliConfig.mcpServers) cliConfig.mcpServers = {};
339
- }
340
- } catch { /* new file */ }
341
- cliConfig.mcpServers.agentvibes = {
342
- type: 'local',
343
- command: 'npx',
344
- args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
345
- env: { AGENTVIBES_LLM: 'copilot' },
346
- tools: ['*'],
347
- };
348
- await fs.writeFile(copilotMcpPath, JSON.stringify(cliConfig, null, 2) + '\n');
349
- } catch { /* best effort — CLI might not be installed */ }
350
-
351
- ensureDefaultLlmConfigSync('copilot', targetDir);
352
- return { success: true };
369
+ // If neither USERPROFILE nor HOME is set, skip — writing to a
370
+ // relative `.copilot/` path would pollute the project dir.
371
+ const home = process.env.COPILOT_HOME ||
372
+ process.env.USERPROFILE || process.env.HOME;
373
+ if (home) {
374
+ const copilotHome = process.env.COPILOT_HOME || path.join(home, '.copilot');
375
+ const copilotMcpPath = path.join(copilotHome, 'mcp-config.json');
376
+ await fs.mkdir(copilotHome, { recursive: true });
377
+ let cliConfig = { mcpServers: {} };
378
+ try {
379
+ const existingCli = await fs.readFile(copilotMcpPath, 'utf8');
380
+ const parsedCli = JSON.parse(existingCli);
381
+ if (parsedCli && typeof parsedCli === 'object' && !Array.isArray(parsedCli)) {
382
+ cliConfig = parsedCli;
383
+ if (!cliConfig.mcpServers || typeof cliConfig.mcpServers !== 'object' || Array.isArray(cliConfig.mcpServers)) {
384
+ cliConfig.mcpServers = {};
385
+ }
386
+ }
387
+ } catch { /* new file or malformed — start fresh */ }
388
+ cliConfig.mcpServers.agentvibes = {
389
+ type: 'local',
390
+ command: 'npx',
391
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
392
+ env: { AGENTVIBES_LLM: 'copilot' },
393
+ tools: ['*'],
394
+ };
395
+ await fs.writeFile(copilotMcpPath, JSON.stringify(cliConfig, null, 2) + '\n');
396
+ }
397
+ } catch (err) {
398
+ // Best effort — CLI might not be installed. Log to stderr so users
399
+ // with COPILOT_HOME set but write failures (EACCES) can diagnose.
400
+ console.error(`[agentvibes] Warning: could not write ~/.copilot/mcp-config.json: ${err.message}`);
401
+ }
353
402
  } catch (err) {
354
- return { success: false, error: err.message };
403
+ mcpError = err.message;
355
404
  }
405
+
406
+ ensureDefaultLlmConfigSync('copilot', targetDir);
407
+ return { success: true, mcpError };
356
408
  }
357
409
 
358
410
  export async function removeCopilotMcp(targetDir) {
@@ -360,13 +412,23 @@ export async function removeCopilotMcp(targetDir) {
360
412
  try {
361
413
  const content = await fs.readFile(mcpJsonPath, 'utf8');
362
414
  const parsed = JSON.parse(content);
363
- if (parsed?.servers?.agentvibes) {
364
- delete parsed.servers.agentvibes;
365
- if (Object.keys(parsed.servers).length === 0) {
366
- await fs.unlink(mcpJsonPath);
367
- } else {
368
- await fs.writeFile(mcpJsonPath, JSON.stringify(parsed, null, 2) + '\n');
369
- }
415
+ // Guard against non-object root or non-object servers (malformed config)
416
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
417
+ return { success: true };
418
+ }
419
+ const servers = parsed.servers;
420
+ if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
421
+ return { success: true };
422
+ }
423
+ // Remove both old ("agentvibes") and new ("agentvibes-copilot") names
424
+ delete servers.agentvibes;
425
+ delete servers['agentvibes-copilot'];
426
+ const noServers = Object.keys(servers).length === 0;
427
+ const noOtherKeys = Object.keys(parsed).length === 1; // only "servers"
428
+ if (noServers && noOtherKeys) {
429
+ await fs.unlink(mcpJsonPath);
430
+ } else {
431
+ await fs.writeFile(mcpJsonPath, JSON.stringify(parsed, null, 2) + '\n');
370
432
  }
371
433
  return { success: true };
372
434
  } catch {
@@ -396,17 +458,19 @@ export async function installCodexMcp(targetDir) {
396
458
  const codexDir = path.join(targetDir, '.codex');
397
459
  const tomlPath = path.join(codexDir, 'config.toml');
398
460
 
461
+ let mcpError = null;
399
462
  try {
400
463
  await fs.mkdir(codexDir, { recursive: true });
401
464
  let existing = '';
402
465
  try { existing = await fs.readFile(tomlPath, 'utf8'); } catch { /* new file */ }
403
466
  const content = buildCodexToml(existing);
404
467
  await fs.writeFile(tomlPath, content);
405
- ensureDefaultLlmConfigSync('codex', targetDir);
406
- return { success: true };
407
468
  } catch (err) {
408
- return { success: false, error: err.message };
469
+ mcpError = err.message;
409
470
  }
471
+
472
+ ensureDefaultLlmConfigSync('codex', targetDir);
473
+ return { success: true, mcpError };
410
474
  }
411
475
 
412
476
  export async function removeCodexMcp(targetDir) {