agentvibes 5.1.4 → 5.2.1

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 (69) hide show
  1. package/.agentvibes/config.json +23 -13
  2. package/.claude/commands/agent-vibes/verbosity.md +98 -89
  3. package/.claude/config/audio-effects.cfg +4 -1
  4. package/.claude/hooks/audio-cache-utils.sh +246 -246
  5. package/.claude/hooks/background-music-manager.sh +404 -404
  6. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  7. package/.claude/hooks/bmad-speak.sh +290 -290
  8. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  9. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  10. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  11. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  12. package/.claude/hooks/clean-audio-cache.sh +22 -22
  13. package/.claude/hooks/cleanup-cache.sh +106 -106
  14. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  15. package/.claude/hooks/download-extra-voices.sh +244 -244
  16. package/.claude/hooks/effects-manager.sh +268 -268
  17. package/.claude/hooks/github-star-reminder.sh +154 -154
  18. package/.claude/hooks/language-manager.sh +362 -362
  19. package/.claude/hooks/learn-manager.sh +492 -492
  20. package/.claude/hooks/macos-voice-manager.sh +205 -205
  21. package/.claude/hooks/migrate-background-music.sh +125 -125
  22. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  23. package/.claude/hooks/optimize-background-music.sh +87 -87
  24. package/.claude/hooks/path-resolver.sh +60 -60
  25. package/.claude/hooks/personality-manager.sh +448 -448
  26. package/.claude/hooks/piper-download-voices.sh +233 -225
  27. package/.claude/hooks/piper-installer.sh +292 -292
  28. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  29. package/.claude/hooks/piper-voice-manager.sh +125 -0
  30. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +97 -90
  31. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  32. package/.claude/hooks/play-tts-piper.sh +16 -5
  33. package/.claude/hooks/play-tts-ssh-remote.sh +168 -167
  34. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  35. package/.claude/hooks/play-tts.sh +35 -14
  36. package/.claude/hooks/prepare-release.sh +54 -54
  37. package/.claude/hooks/provider-commands.sh +617 -617
  38. package/.claude/hooks/provider-manager.sh +399 -399
  39. package/.claude/hooks/replay-target-audio.sh +95 -95
  40. package/.claude/hooks/sentiment-manager.sh +201 -201
  41. package/.claude/hooks/session-start-tts.sh +4 -1
  42. package/.claude/hooks/speed-manager.sh +291 -291
  43. package/.claude/hooks/stop-tts.sh +84 -84
  44. package/.claude/hooks/termux-installer.sh +261 -261
  45. package/.claude/hooks/translate-manager.sh +341 -341
  46. package/.claude/hooks/tts-queue-worker.sh +145 -145
  47. package/.claude/hooks/tts-queue.sh +165 -165
  48. package/.claude/hooks/verbosity-manager.sh +185 -178
  49. package/.claude/hooks/voice-manager.sh +552 -548
  50. package/.claude/hooks-windows/download-extra-voices.ps1 +243 -185
  51. package/.claude/hooks-windows/play-tts-piper.ps1 +7 -2
  52. package/.claude/hooks-windows/play-tts.ps1 +9 -3
  53. package/.claude/hooks-windows/session-start-tts.ps1 +2 -1
  54. package/.claude/hooks-windows/verbosity-manager.ps1 +126 -119
  55. package/README.md +19 -2
  56. package/RELEASE_NOTES.md +74 -0
  57. package/bin/agentvibes-voice-browser.js +1939 -1840
  58. package/bin/mcp-server.sh +206 -206
  59. package/mcp-server/server.py +87 -15
  60. package/package.json +1 -1
  61. package/src/console/tabs/receiver-tab.js +1527 -1483
  62. package/src/console/tabs/settings-tab.js +2 -2
  63. package/src/console/tabs/setup-tab.js +112 -31
  64. package/src/console/tabs/voices-tab.js +130 -13
  65. package/src/i18n/en.js +202 -202
  66. package/src/installer.js +79 -213
  67. package/src/services/llm-provider-service.js +126 -75
  68. package/src/services/verbosity-service.js +159 -157
  69. package/templates/agentvibes-receiver.sh +3 -2
package/src/installer.js CHANGED
@@ -3771,18 +3771,31 @@ async function copyConfigFiles(targetDir, spinner) {
3771
3771
  const stat = await fs.stat(srcPath);
3772
3772
 
3773
3773
  if (stat.isFile()) {
3774
- // Don't overwrite existing config files (except audio-effects.cfg which is required)
3775
- try {
3776
- await fs.access(destPath);
3777
- if (file !== 'audio-effects.cfg') {
3778
- continue; // Skip if file exists and it's not audio-effects.cfg
3774
+ // For .sample files: copy as the real config name if it doesn't exist yet
3775
+ // e.g. audio-effects.cfg.sample → audio-effects.cfg (only if absent)
3776
+ let finalDest = destPath;
3777
+ let finalName = file;
3778
+ if (file.endsWith('.sample')) {
3779
+ finalName = file.replace(/\.sample$/, '');
3780
+ finalDest = path.join(destConfigDir, finalName);
3781
+ try {
3782
+ await fs.access(finalDest);
3783
+ continue; // Real config already exists, don't overwrite
3784
+ } catch {
3785
+ // Real config doesn't exist, install from sample
3786
+ }
3787
+ } else {
3788
+ // Non-sample files: skip if already exists
3789
+ try {
3790
+ await fs.access(destPath);
3791
+ continue;
3792
+ } catch {
3793
+ // File doesn't exist, proceed with copy
3779
3794
  }
3780
- } catch {
3781
- // File doesn't exist, proceed with copy
3782
3795
  }
3783
3796
 
3784
- await fs.copyFile(srcPath, destPath);
3785
- copiedFiles.push(file);
3797
+ await fs.copyFile(srcPath, finalDest);
3798
+ copiedFiles.push(finalName);
3786
3799
  }
3787
3800
  }
3788
3801
 
@@ -4297,239 +4310,92 @@ function isPathSafe(targetPath, basePath) {
4297
4310
  async function handleMcpConfiguration(targetDir, options) {
4298
4311
  const mcpConfigPath = path.join(targetDir, '.mcp.json');
4299
4312
 
4300
- // MCP server configuration for AgentVibes.
4313
+ // .mcp.json registers the AgentVibes MCP server for Claude Code, enabling
4314
+ // natural language control (text_to_speech, get_config, set_voice, etc.).
4301
4315
  //
4302
- // No `env.AGENTVIBES_LLM` block: GitHub Copilot CLI also reads project
4303
- // `.mcp.json` with precedence over its own `~/.copilot/mcp-config.json`,
4304
- // so setting `claude-code` here would mis-route Copilot CLI too.
4305
- // Instead, the MCP server auto-detects Claude Code via the `CLAUDECODE=1`
4306
- // env var that Claude Code sets on every subprocess it spawns.
4316
+ // AGENTVIBES_MCP_FALLBACK=copilot is the identity for non-Claude-Code tools
4317
+ // that read .mcp.json (primarily VS Code Copilot, which reads .mcp.json
4318
+ // with precedence over its own .vscode/mcp.json). Claude Code is
4319
+ // auto-detected via CLAUDECODE=1 which takes priority over the fallback.
4307
4320
  const mcpConfig = {
4308
4321
  mcpServers: {
4309
4322
  agentvibes: {
4310
4323
  command: 'npx',
4311
- args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server']
4324
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
4325
+ env: { AGENTVIBES_MCP_FALLBACK: 'copilot' }
4312
4326
  }
4313
4327
  }
4314
4328
  };
4315
4329
 
4316
- // Check if .mcp.json already exists
4317
4330
  let mcpExists = false;
4318
4331
  try {
4319
4332
  await fs.access(mcpConfigPath);
4320
4333
  mcpExists = true;
4321
- } catch {
4322
- // File doesn't exist
4323
- }
4334
+ } catch { /* doesn't exist */ }
4324
4335
 
4325
4336
  if (mcpExists) {
4326
- // Existing config: upgrade it in-place. Two jobs:
4327
- // 1. Ensure the agentvibes server entry exists.
4328
- // 2. STRIP any stale `env.AGENTVIBES_LLM` from earlier versions
4329
- // (v5.1.2..v5.1.4) — setting it in `.mcp.json` broke Copilot CLI
4330
- // routing because Copilot CLI also reads `.mcp.json` and would
4331
- // adopt claude-code's env. Claude Code is now auto-detected
4332
- // downstream via the CLAUDECODE=1 env var.
4333
- let migrated = false;
4334
- let migrationError = null;
4337
+ // Upgrade: ensure agentvibes entry exists with fallback env
4338
+ let parseFailed = false;
4335
4339
  try {
4336
- const existingRaw = await fs.readFile(mcpConfigPath, 'utf8');
4337
- const existingCfg = JSON.parse(existingRaw);
4338
- if (existingCfg && typeof existingCfg === 'object') {
4339
- if (!existingCfg.mcpServers || typeof existingCfg.mcpServers !== 'object') {
4340
- existingCfg.mcpServers = {};
4341
- }
4342
- const current = existingCfg.mcpServers.agentvibes;
4343
- const hasStaleEnv = current?.env?.AGENTVIBES_LLM !== undefined;
4344
- const needsWrite = !current || hasStaleEnv;
4345
- if (needsWrite) {
4346
- // Preserve any OTHER env keys the user added manually (rare) but
4347
- // drop AGENTVIBES_LLM so Copilot CLI doesn't mis-route.
4348
- const cleanEnv = { ...(current?.env ?? {}) };
4349
- delete cleanEnv.AGENTVIBES_LLM;
4350
- const newEntry = {
4351
- command: 'npx',
4352
- args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
4353
- };
4354
- if (Object.keys(cleanEnv).length > 0) {
4355
- newEntry.env = cleanEnv;
4356
- }
4357
- existingCfg.mcpServers.agentvibes = newEntry;
4358
- await fs.writeFile(mcpConfigPath, JSON.stringify(existingCfg, null, 2) + '\n');
4359
- migrated = true;
4360
- }
4340
+ const existing = JSON.parse(await fs.readFile(mcpConfigPath, 'utf8'));
4341
+ // Guard: non-object root (arrays/primitives are valid JSON but wrong shape)
4342
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
4343
+ console.log(chalk.yellow(
4344
+ `⚠️ ${mcpConfigPath} has a non-object root — skipping MCP registration. Fix the file manually and re-run.`
4345
+ ));
4346
+ return;
4347
+ }
4348
+ // Guard: mcpServers must be a plain object
4349
+ if (!existing.mcpServers || typeof existing.mcpServers !== 'object' || Array.isArray(existing.mcpServers)) {
4350
+ existing.mcpServers = {};
4361
4351
  }
4352
+ const current = existing.mcpServers.agentvibes;
4353
+ // Strip AGENTVIBES_LLM if present (causes identity collisions)
4354
+ if (current?.env?.AGENTVIBES_LLM) {
4355
+ delete current.env.AGENTVIBES_LLM;
4356
+ }
4357
+ // Ensure fallback is set
4358
+ const mergedEnv = { ...(current?.env ?? {}), AGENTVIBES_MCP_FALLBACK: 'copilot' };
4359
+ existing.mcpServers.agentvibes = {
4360
+ command: 'npx',
4361
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
4362
+ env: mergedEnv,
4363
+ };
4364
+ await fs.writeFile(mcpConfigPath, JSON.stringify(existing, null, 2) + '\n');
4362
4365
  } catch (err) {
4363
- migrationError = err;
4364
- }
4365
-
4366
- if (migrated) {
4367
- console.log(
4368
- boxen(
4369
- chalk.green.bold('✅ MCP Configuration Updated\n\n') +
4370
- chalk.white('Your existing ') + chalk.cyan('.mcp.json') + chalk.white(' has been updated.\n') +
4371
- chalk.white('Claude Code is auto-detected via ') + chalk.cyan('CLAUDECODE=1') + chalk.white(' at runtime.'),
4372
- {
4373
- padding: 1,
4374
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
4375
- borderStyle: 'double',
4376
- borderColor: 'green',
4377
- }
4378
- )
4379
- );
4380
- return;
4366
+ parseFailed = true;
4367
+ console.log(chalk.yellow(
4368
+ `⚠️ Could not update ${mcpConfigPath}: ${err.message}\n` +
4369
+ ` AgentVibes MCP server was NOT registered. Fix the file manually and re-run.`
4370
+ ));
4381
4371
  }
4382
-
4383
- // Migration was not needed (already correct) or failed — fall through
4384
- // to the manual-instructions box.
4385
- console.log(
4386
- boxen(
4387
- chalk.yellow.bold('ℹ️ MCP Configuration Already Exists\n\n') +
4388
- chalk.white('An ') + chalk.cyan('.mcp.json') + chalk.white(' file already exists in this project.\n\n') +
4389
- (migrationError
4390
- ? chalk.red('Could not auto-update it: ' + migrationError.message + '\n\n')
4391
- : chalk.gray('It already has the correct AgentVibes entry.\n\n')) +
4392
- chalk.white('To add or fix the AgentVibes MCP server manually, use:'),
4393
- {
4394
- padding: 1,
4395
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
4396
- borderStyle: 'round',
4397
- borderColor: migrationError ? 'red' : 'yellow',
4398
- }
4399
- )
4400
- );
4401
-
4402
- // Display the snippet to add
4403
- console.log(
4404
- '\n"agentvibes": {\n' +
4405
- ' "command": "npx",\n' +
4406
- ' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]\n' +
4407
- '}\n'
4408
- );
4409
-
4410
- console.log(
4411
- boxen(
4412
- chalk.cyan('To use with Claude Code:\n') +
4413
- chalk.white(' claude --mcp-config .mcp.json\n\n') +
4414
- chalk.cyan('📖 Full Guide:\n') +
4415
- chalk.cyan.bold('https://github.com/paulpreibisch/AgentVibes#mcp-server'),
4416
- {
4417
- padding: 1,
4418
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
4419
- borderStyle: 'round',
4420
- borderColor: 'cyan',
4421
- }
4422
- )
4423
- );
4372
+ if (!parseFailed) return;
4424
4373
  return;
4425
4374
  }
4426
4375
 
4427
- // Scenario 1 & 2: Config doesn't exist - offer to create
4428
- console.log(
4429
- boxen(
4430
- chalk.cyan.bold('🎙️ MCP Server Configuration\n\n') +
4431
- chalk.white.bold('AgentVibes MCP Server - Control TTS with Natural Language!\n\n') +
4432
- chalk.gray('Use natural language instead of slash commands:\n') +
4433
- chalk.gray(' "Switch to Aria voice" instead of /agent-vibes:switch "Aria"\n') +
4434
- chalk.gray(' "Set personality to sarcastic" instead of /agent-vibes:personality sarcastic\n\n') +
4435
- chalk.white('No ') + chalk.cyan('.mcp.json') + chalk.white(' found in this project.'),
4436
- {
4437
- padding: 1,
4438
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
4439
- borderStyle: 'round',
4440
- borderColor: 'cyan',
4441
- }
4442
- )
4443
- );
4444
-
4445
- let createConfig = options.yes; // Auto-create if --yes flag
4446
-
4376
+ // New install create .mcp.json
4447
4377
  if (!options.yes) {
4448
- const { confirmCreate } = await inquirer.prompt([
4449
- {
4450
- type: 'confirm',
4451
- name: 'confirmCreate',
4452
- message: chalk.cyan('Would you like to create .mcp.json for this project?'),
4453
- default: true,
4454
- },
4455
- ]);
4456
- createConfig = confirmCreate;
4457
- }
4458
-
4459
- if (createConfig) {
4460
- // Scenario 1: User says YES - create the config
4461
- try {
4462
- await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
4463
-
4464
- console.log(
4465
- boxen(
4466
- chalk.green.bold('✅ MCP Configuration Created!\n\n') +
4467
- chalk.white('Your ') + chalk.cyan('.mcp.json') + chalk.white(' has been created in this project.\n\n') +
4468
- chalk.white('To use AgentVibes MCP server with Claude, run:\n') +
4469
- chalk.cyan.bold(' claude --mcp-config .mcp.json\n\n') +
4470
- chalk.green('The MCP server is now installed and ready to use!'),
4471
- {
4472
- padding: 1,
4473
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
4474
- borderStyle: 'double',
4475
- borderColor: 'green',
4476
- }
4477
- )
4478
- );
4479
-
4480
- // Show the installed JSON so users can see exactly what was written
4481
- console.log(chalk.gray(JSON.stringify(mcpConfig, null, 2)) + '\n');
4482
- } catch (error) {
4483
- console.log(chalk.red(`\n✗ Failed to create .mcp.json: ${error.message}`));
4484
- console.log(chalk.gray(' You can create it manually with the config shown below.\n'));
4485
- // Fall through to show manual instructions
4486
- createConfig = false;
4487
- }
4378
+ const { confirmCreate } = await inquirer.prompt([{
4379
+ type: 'confirm',
4380
+ name: 'confirmCreate',
4381
+ message: chalk.cyan('Create .mcp.json for AgentVibes MCP server? (enables natural language voice control)'),
4382
+ default: true,
4383
+ }]);
4384
+ if (!confirmCreate) return;
4488
4385
  }
4489
4386
 
4490
- if (!createConfig) {
4491
- // Scenario 2: User says NO - show manual instructions
4492
- console.log(
4493
- boxen(
4494
- chalk.cyan.bold('📋 Manual MCP Configuration\n\n') +
4495
- chalk.white('Create a ') + chalk.cyan('.mcp.json') + chalk.white(' file in your project with:'),
4496
- {
4497
- padding: 1,
4498
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
4499
- borderStyle: 'round',
4500
- borderColor: 'cyan',
4501
- }
4502
- )
4503
- );
4504
-
4505
- // Display JSON config
4506
- console.log(
4507
- '\n{\n' +
4508
- ' "mcpServers": {\n' +
4509
- ' "agentvibes": {\n' +
4510
- ' "command": "npx",\n' +
4511
- ' "args": ["-y", "--package=agentvibes", "agentvibes-mcp-server"]\n' +
4512
- ' }\n' +
4513
- ' }\n' +
4514
- '}\n'
4515
- );
4516
-
4387
+ try {
4388
+ await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
4517
4389
  console.log(
4518
4390
  boxen(
4519
- chalk.cyan('To use with Claude Code:\n') +
4520
- chalk.white(' claude --mcp-config .mcp.json\n\n') +
4521
- chalk.cyan('📱 Claude Desktop / Warp Terminal:\n') +
4522
- chalk.white(' npx agentvibes setup-mcp-for-claude-desktop\n\n') +
4523
- chalk.cyan('📖 Full Guide:\n') +
4524
- chalk.cyan.bold('https://github.com/paulpreibisch/AgentVibes#mcp-server'),
4525
- {
4526
- padding: 1,
4527
- margin: { top: 1, bottom: 1, left: 0, right: 0 },
4528
- borderStyle: 'round',
4529
- borderColor: 'cyan',
4530
- }
4391
+ chalk.green.bold(' MCP Configuration Created!\n\n') +
4392
+ chalk.white('AgentVibes MCP server registered in ') + chalk.cyan('.mcp.json') + chalk.white('.\n') +
4393
+ chalk.green('Natural language voice control is ready!'),
4394
+ { padding: 1, margin: { top: 1, bottom: 1 }, borderStyle: 'double', borderColor: 'green' }
4531
4395
  )
4532
4396
  );
4397
+ } catch (err) {
4398
+ console.log(chalk.red(`\n✗ Failed to create .mcp.json: ${err.message}`));
4533
4399
  }
4534
4400
  }
4535
4401
 
@@ -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
+ }
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');
173
193
  }
194
+ mcpCreated = true;
195
+ } catch (err) {
196
+ mcpError = err.message;
197
+ }
174
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,20 @@ 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
+ return { success: true, mcpCreated, mcpError };
188
213
  } catch (err) {
189
- return { success: false, error: err.message };
214
+ return { success: false, error: err.message, mcpError };
190
215
  }
191
216
  }
192
217
 
193
218
  export async function removeClaudeMcp(targetDir) {
194
- const mcpConfigPath = path.join(targetDir, '.mcp.json');
219
+ // Clean up .mcp.json agentvibes entry (legacy from older versions)
195
220
  try {
221
+ const mcpConfigPath = path.join(targetDir, '.mcp.json');
196
222
  const content = await fs.readFile(mcpConfigPath, 'utf8');
197
223
  const parsed = JSON.parse(content);
198
224
  if (parsed.mcpServers?.agentvibes) {
199
225
  delete parsed.mcpServers.agentvibes;
200
- // Only delete file if mcpServers is empty AND no other top-level keys
201
226
  const noServers = Object.keys(parsed.mcpServers).length === 0;
202
227
  const noOtherKeys = Object.keys(parsed).length === 1;
203
228
  if (noServers && noOtherKeys) {
@@ -217,9 +242,9 @@ export async function removeClaudeMcp(targetDir) {
217
242
  export async function uninstallClaude(targetDir) {
218
243
  const removed = [];
219
244
 
220
- // 1. Remove MCP entry
245
+ // 1. Remove legacy .mcp.json agentvibes entry if present
221
246
  await removeClaudeMcp(targetDir);
222
- removed.push('.mcp.json (agentvibes entry)');
247
+ removed.push('.mcp.json agentvibes entry (if present)');
223
248
 
224
249
  // 2. Remove AgentVibes directories
225
250
  const dirs = [
@@ -304,6 +329,7 @@ export async function installCopilotMcp(targetDir) {
304
329
  env: { AGENTVIBES_LLM: 'copilot' },
305
330
  };
306
331
 
332
+ let mcpError = null;
307
333
  try {
308
334
  await fs.mkdir(vscodeDir, { recursive: true });
309
335
  let mcpConfig = { servers: {} };
@@ -316,6 +342,8 @@ export async function installCopilotMcp(targetDir) {
316
342
  }
317
343
  } catch { /* new file */ }
318
344
 
345
+ // Clean up any old "agentvibes-copilot" entry from a prior attempt.
346
+ delete mcpConfig.servers['agentvibes-copilot'];
319
347
  mcpConfig.servers.agentvibes = agentvibesServer;
320
348
  await fs.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n');
321
349
 
@@ -325,34 +353,45 @@ export async function installCopilotMcp(targetDir) {
325
353
  // CLI reads ONLY from ~/.copilot/mcp-config.json per docs:
326
354
  // https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
327
355
  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 };
356
+ // If neither USERPROFILE nor HOME is set, skip — writing to a
357
+ // relative `.copilot/` path would pollute the project dir.
358
+ const home = process.env.COPILOT_HOME ||
359
+ process.env.USERPROFILE || process.env.HOME;
360
+ if (home) {
361
+ const copilotHome = process.env.COPILOT_HOME || path.join(home, '.copilot');
362
+ const copilotMcpPath = path.join(copilotHome, 'mcp-config.json');
363
+ await fs.mkdir(copilotHome, { recursive: true });
364
+ let cliConfig = { mcpServers: {} };
365
+ try {
366
+ const existingCli = await fs.readFile(copilotMcpPath, 'utf8');
367
+ const parsedCli = JSON.parse(existingCli);
368
+ if (parsedCli && typeof parsedCli === 'object' && !Array.isArray(parsedCli)) {
369
+ cliConfig = parsedCli;
370
+ if (!cliConfig.mcpServers || typeof cliConfig.mcpServers !== 'object' || Array.isArray(cliConfig.mcpServers)) {
371
+ cliConfig.mcpServers = {};
372
+ }
373
+ }
374
+ } catch { /* new file or malformed — start fresh */ }
375
+ cliConfig.mcpServers.agentvibes = {
376
+ type: 'local',
377
+ command: 'npx',
378
+ args: ['-y', '--package=agentvibes', 'agentvibes-mcp-server'],
379
+ env: { AGENTVIBES_LLM: 'copilot' },
380
+ tools: ['*'],
381
+ };
382
+ await fs.writeFile(copilotMcpPath, JSON.stringify(cliConfig, null, 2) + '\n');
383
+ }
384
+ } catch (err) {
385
+ // Best effort — CLI might not be installed. Log to stderr so users
386
+ // with COPILOT_HOME set but write failures (EACCES) can diagnose.
387
+ console.error(`[agentvibes] Warning: could not write ~/.copilot/mcp-config.json: ${err.message}`);
388
+ }
353
389
  } catch (err) {
354
- return { success: false, error: err.message };
390
+ mcpError = err.message;
355
391
  }
392
+
393
+ ensureDefaultLlmConfigSync('copilot', targetDir);
394
+ return { success: true, mcpError };
356
395
  }
357
396
 
358
397
  export async function removeCopilotMcp(targetDir) {
@@ -360,13 +399,23 @@ export async function removeCopilotMcp(targetDir) {
360
399
  try {
361
400
  const content = await fs.readFile(mcpJsonPath, 'utf8');
362
401
  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
- }
402
+ // Guard against non-object root or non-object servers (malformed config)
403
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
404
+ return { success: true };
405
+ }
406
+ const servers = parsed.servers;
407
+ if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
408
+ return { success: true };
409
+ }
410
+ // Remove both old ("agentvibes") and new ("agentvibes-copilot") names
411
+ delete servers.agentvibes;
412
+ delete servers['agentvibes-copilot'];
413
+ const noServers = Object.keys(servers).length === 0;
414
+ const noOtherKeys = Object.keys(parsed).length === 1; // only "servers"
415
+ if (noServers && noOtherKeys) {
416
+ await fs.unlink(mcpJsonPath);
417
+ } else {
418
+ await fs.writeFile(mcpJsonPath, JSON.stringify(parsed, null, 2) + '\n');
370
419
  }
371
420
  return { success: true };
372
421
  } catch {
@@ -396,17 +445,19 @@ export async function installCodexMcp(targetDir) {
396
445
  const codexDir = path.join(targetDir, '.codex');
397
446
  const tomlPath = path.join(codexDir, 'config.toml');
398
447
 
448
+ let mcpError = null;
399
449
  try {
400
450
  await fs.mkdir(codexDir, { recursive: true });
401
451
  let existing = '';
402
452
  try { existing = await fs.readFile(tomlPath, 'utf8'); } catch { /* new file */ }
403
453
  const content = buildCodexToml(existing);
404
454
  await fs.writeFile(tomlPath, content);
405
- ensureDefaultLlmConfigSync('codex', targetDir);
406
- return { success: true };
407
455
  } catch (err) {
408
- return { success: false, error: err.message };
456
+ mcpError = err.message;
409
457
  }
458
+
459
+ ensureDefaultLlmConfigSync('codex', targetDir);
460
+ return { success: true, mcpError };
410
461
  }
411
462
 
412
463
  export async function removeCodexMcp(targetDir) {