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
package/src/i18n/de.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[Stimme wechseln]",
144
144
  voicesFavoriteBtn: "[★ Favorit]",
145
145
  voicesDownloadBtn: "[Stimme herunterladen]",
146
- voicesInstallAllBtn: "[BMAD-Stimmen installieren]",
147
146
  voicesRowHintInstalled: "[Space] Vorschau [Enter] Auswählen [*] Favorit",
148
147
  voicesRowHintUninstalled: "[Enter] Herunterladen und installieren",
149
148
  musicRowHint: "[Space] Abspielen [Enter] Auswählen [*] Favorit",
package/src/i18n/en.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[Switch Voice]",
144
144
  voicesFavoriteBtn: "[+ Thumbs Up]",
145
145
  voicesDownloadBtn: "[Download Voice]",
146
- voicesInstallAllBtn: "[Install BMAD Voices]",
147
146
  voicesRowHintInstalled: "[Space] Preview [Enter] Select [+/*] Up [-] Down",
148
147
  voicesRowHintUninstalled: "[Enter] Download & Install",
149
148
  musicRowHint: "[Space] Play [Enter] Select [+/*] Favorite",
package/src/i18n/es.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[Cambiar Voz]",
144
144
  voicesFavoriteBtn: "[★ Favorito]",
145
145
  voicesDownloadBtn: "[Descargar Voz]",
146
- voicesInstallAllBtn: "[Instalar Voces BMAD]",
147
146
  voicesRowHintInstalled: "[Space] Vista previa [Enter] Seleccionar [*] Favorito",
148
147
  voicesRowHintUninstalled: "[Enter] Descargar e instalar",
149
148
  musicRowHint: "[Space] Reproducir [Enter] Seleccionar [*] Favorito",
package/src/i18n/fr.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[Changer la voix]",
144
144
  voicesFavoriteBtn: "[★ Favori]",
145
145
  voicesDownloadBtn: "[Télécharger la voix]",
146
- voicesInstallAllBtn: "[Installer les voix BMAD]",
147
146
  voicesRowHintInstalled: "[Space] Aperçu [Enter] Sélectionner [*] Favori",
148
147
  voicesRowHintUninstalled: "[Enter] Télécharger et installer",
149
148
  musicRowHint: "[Space] Lire [Enter] Sélectionner [*] Favori",
package/src/i18n/hi.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[आवाज़ बदलें]",
144
144
  voicesFavoriteBtn: "[★ पसंदीदा]",
145
145
  voicesDownloadBtn: "[आवाज़ डाउनलोड]",
146
- voicesInstallAllBtn: "[BMAD आवाज़ें इंस्टॉल करें]",
147
146
  voicesRowHintInstalled: "[Space] पूर्वावलोकन [Enter] चुनें [*] पसंदीदा",
148
147
  voicesRowHintUninstalled: "[Enter] डाउनलोड और इंस्टॉल करें",
149
148
  musicRowHint: "[Space] चलाएं [Enter] चुनें [*] पसंदीदा",
package/src/i18n/ja.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[音声切り替え]",
144
144
  voicesFavoriteBtn: "[★ お気に入り]",
145
145
  voicesDownloadBtn: "[音声ダウンロード]",
146
- voicesInstallAllBtn: "[BMADボイスをインストール]",
147
146
  voicesRowHintInstalled: "[Space] プレビュー [Enter] 選択 [*] お気に入り",
148
147
  voicesRowHintUninstalled: "[Enter] ダウンロードしてインストール",
149
148
  musicRowHint: "[Space] 再生 [Enter] 選択 [*] お気に入り",
package/src/i18n/ko.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[음성 전환]",
144
144
  voicesFavoriteBtn: "[★ 즐겨찾기]",
145
145
  voicesDownloadBtn: "[음성 다운로드]",
146
- voicesInstallAllBtn: "[BMAD 음성 설치]",
147
146
  voicesRowHintInstalled: "[Space] 미리보기 [Enter] 선택 [*] 즐겨찾기",
148
147
  voicesRowHintUninstalled: "[Enter] 다운로드 및 설치",
149
148
  musicRowHint: "[Space] 재생 [Enter] 선택 [*] 즐겨찾기",
package/src/i18n/pt.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[Trocar Voz]",
144
144
  voicesFavoriteBtn: "[★ Favorito]",
145
145
  voicesDownloadBtn: "[Baixar Voz]",
146
- voicesInstallAllBtn: "[Instalar Vozes BMAD]",
147
146
  voicesRowHintInstalled: "[Space] Visualizar [Enter] Selecionar [*] Favorito",
148
147
  voicesRowHintUninstalled: "[Enter] Baixar e instalar",
149
148
  musicRowHint: "[Space] Reproduzir [Enter] Selecionar [*] Favorito",
package/src/i18n/zh-CN.js CHANGED
@@ -143,7 +143,6 @@ export default {
143
143
  voicesSwitchBtn: "[切换语音]",
144
144
  voicesFavoriteBtn: "[★ 收藏]",
145
145
  voicesDownloadBtn: "[下载语音]",
146
- voicesInstallAllBtn: "[安装BMAD语音]",
147
146
  voicesRowHintInstalled: "[Space] 预览 [Enter] 选择 [*] 收藏",
148
147
  voicesRowHintUninstalled: "[Enter] 下载并安装",
149
148
  musicRowHint: "[Space] 播放 [Enter] 选择 [*] 收藏",
package/src/installer.js CHANGED
@@ -4176,6 +4176,52 @@ async function copyCodexFiles(targetDir, spinner) {
4176
4176
  * @param {string} targetDir - Target installation directory
4177
4177
  * @param {Object} spinner - Ora spinner instance
4178
4178
  */
4179
+ /**
4180
+ * Safely read a JSON settings file that is about to be modified in place.
4181
+ *
4182
+ * Non-Destructive Configuration Rule: a corrupt settings.json (e.g. one stray
4183
+ * trailing comma) must NEVER be silently overwritten — doing so wipes all of the
4184
+ * user's real settings. This helper distinguishes three cases so callers can
4185
+ * skip-and-warn on corruption instead of clobbering:
4186
+ * - missing / empty -> { exists:false, corrupt:false } (safe to create fresh)
4187
+ * - present + valid -> { exists:true, corrupt:false, data }
4188
+ * - present + invalid -> { exists:true, corrupt:true } (caller must NOT write)
4189
+ * @param {string} filePath
4190
+ * @returns {Promise<{exists:boolean, corrupt:boolean, data:object}>}
4191
+ */
4192
+ async function readJsonConfigSafe(filePath) {
4193
+ let raw;
4194
+ try {
4195
+ raw = await fs.readFile(filePath, 'utf8');
4196
+ } catch (err) {
4197
+ if (err && err.code === 'ENOENT') return { exists: false, corrupt: false, data: {} };
4198
+ // Unreadable for another reason (permissions, etc.) — treat as present+corrupt
4199
+ // so we never overwrite a file we could not fully read.
4200
+ return { exists: true, corrupt: true, data: {} };
4201
+ }
4202
+ if (raw.trim() === '') return { exists: false, corrupt: false, data: {} };
4203
+ try {
4204
+ const parsed = JSON.parse(raw);
4205
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
4206
+ return { exists: true, corrupt: true, data: {} };
4207
+ }
4208
+ return { exists: true, corrupt: false, data: parsed };
4209
+ } catch {
4210
+ return { exists: true, corrupt: true, data: {} };
4211
+ }
4212
+ }
4213
+
4214
+ /**
4215
+ * Best-effort backup of a config file before mutating it. Writes `<file>.bak`.
4216
+ * A backup failure is non-fatal (callers only mutate files that parsed cleanly).
4217
+ * @param {string} filePath
4218
+ */
4219
+ async function backupConfigFile(filePath) {
4220
+ try {
4221
+ await fs.copyFile(filePath, filePath + '.bak');
4222
+ } catch { /* best-effort backup — never block the install on this */ }
4223
+ }
4224
+
4179
4225
  async function configureSessionStartHook(targetDir, spinner) {
4180
4226
  spinner = createRobustSpinner(spinner);
4181
4227
  spinner.start('Configuring AgentVibes hook for automatic TTS...');
@@ -4184,13 +4230,15 @@ async function configureSessionStartHook(targetDir, spinner) {
4184
4230
  const templateSettingsPath = path.join(__dirname, '..', '.claude', 'settings.json');
4185
4231
 
4186
4232
  try {
4187
- let existingSettings = {};
4188
- try {
4189
- const existingContent = await fs.readFile(settingsPath, 'utf8');
4190
- existingSettings = JSON.parse(existingContent);
4191
- } catch {
4192
- // File doesn't exist or is invalid - use template
4233
+ const existing = await readJsonConfigSafe(settingsPath);
4234
+ if (existing.corrupt) {
4235
+ spinner.warn(chalk.yellow(
4236
+ `settings.json at ${settingsPath} is not valid JSON — leaving it untouched to avoid data loss.\n` +
4237
+ ` Fix the file manually and re-run to enable the AgentVibes hook.\n`
4238
+ ));
4239
+ return;
4193
4240
  }
4241
+ const existingSettings = existing.data;
4194
4242
 
4195
4243
  const templateContent = await fs.readFile(templateSettingsPath, 'utf8');
4196
4244
  const templateSettings = JSON.parse(templateContent);
@@ -4216,6 +4264,7 @@ async function configureSessionStartHook(targetDir, spinner) {
4216
4264
  existingSettings.$schema = templateSettings.$schema;
4217
4265
  }
4218
4266
 
4267
+ if (existing.exists) await backupConfigFile(settingsPath);
4219
4268
  await fs.writeFile(settingsPath, JSON.stringify(existingSettings, null, 2));
4220
4269
  spinner.succeed(chalk.green('SessionStart hook configured!\n'));
4221
4270
  } else {
@@ -4260,14 +4309,17 @@ async function configurePartyModeHook(targetDir, spinner, homeDirOverride) {
4260
4309
  ? `powershell -NoProfile -ExecutionPolicy Bypass -File "$HOME\\.claude\\hooks-windows\\bmad-party-speak.ps1"`
4261
4310
  : `bash "$HOME/.claude/hooks/bmad-party-speak.sh"`;
4262
4311
 
4263
- // Read/create global settings.json
4264
- let settings = {};
4265
- try {
4266
- const content = await fs.readFile(globalSettingsPath, 'utf8');
4267
- settings = JSON.parse(content);
4268
- } catch {
4269
- // File missing or invalid start fresh
4312
+ // Read/create global settings.json — never overwrite a corrupt file (would
4313
+ // wipe the user's real global settings).
4314
+ const existingGlobal = await readJsonConfigSafe(globalSettingsPath);
4315
+ if (existingGlobal.corrupt) {
4316
+ spinner.warn(chalk.yellow(
4317
+ `Global settings.json at ${globalSettingsPath} is not valid JSON — leaving it untouched.\n` +
4318
+ ` Fix it manually and re-run to enable party mode TTS.\n`
4319
+ ));
4320
+ return;
4270
4321
  }
4322
+ let settings = existingGlobal.data;
4271
4323
 
4272
4324
  if (!settings.hooks) settings.hooks = {};
4273
4325
 
@@ -4286,6 +4338,7 @@ async function configurePartyModeHook(targetDir, spinner, homeDirOverride) {
4286
4338
  settings.hooks.PostToolUse.push({
4287
4339
  hooks: [{ type: 'command', command: hookCommand }]
4288
4340
  });
4341
+ if (existingGlobal.exists) await backupConfigFile(globalSettingsPath);
4289
4342
  await fs.writeFile(globalSettingsPath, JSON.stringify(settings, null, 2));
4290
4343
  spinner.succeed(chalk.green('BMAD party mode TTS hook configured!\n'));
4291
4344
  } else {
@@ -4671,7 +4724,7 @@ async function checkAndInstallPiperWindows(targetDir, options) {
4671
4724
  }
4672
4725
  console.log(chalk.green('\n✅ LibriTTS voices ready! Browse with /agent-vibes:list\n'));
4673
4726
  } else {
4674
- console.log(chalk.gray(' Skipped. Download later via: npx agentvibes Voices tab → Install BMAD Voices\n'));
4727
+ console.log(chalk.gray(' Skipped. Download later via: npx agentvibes voices download\n'));
4675
4728
  }
4676
4729
  }
4677
4730
  }
@@ -5356,6 +5409,124 @@ async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
5356
5409
  return updated;
5357
5410
  }
5358
5411
 
5412
+ /**
5413
+ * Story AVI-S8.5 (Stage 2 packaging) — ship the utterance resolver bundle
5414
+ * (bin/resolve-utterance.js + src/services/{utterance-resolver,utterance-loader}.js)
5415
+ * into a target `.claude` tree so the play-tts hooks can find it.
5416
+ *
5417
+ * Layout (F-5): a SELF-CONTAINED bundle dir `<claudeDir>/agentvibes-resolver/`:
5418
+ * agentvibes-resolver/
5419
+ * package.json -> {"type":"module"}
5420
+ * bin/resolve-utterance.js
5421
+ * src/services/utterance-resolver.js
5422
+ * src/services/utterance-loader.js
5423
+ * The bundle's OWN package.json declares ESM, so `node resolve-utterance.js`
5424
+ * works on EVERY Node version (the CLI uses `import`; without this, Node < 22.7
5425
+ * treats the copied .js as CommonJS and the import throws → players silently fall
5426
+ * back to legacy forever). It lives in a dedicated dir, NOT in `<claudeDir>/hooks/`,
5427
+ * so it can't change the module type of any user-owned `.js` hook. The players
5428
+ * probe `$SCRIPT_DIR/../agentvibes-resolver/bin/resolve-utterance.js`; its
5429
+ * `../src/services/...` imports resolve inside the bundle dir.
5430
+ *
5431
+ * This is AgentVibes-owned code — same class as the hook scripts themselves —
5432
+ * so copy/overwrite on update is correct. manifestSafeCopy still refuses to
5433
+ * clobber a user-modified copy (saves a .user.bak) and running this twice is
5434
+ * a no-op on the second pass (Non-Destructive Configuration Rule).
5435
+ *
5436
+ * @param {string} claudeDir - Absolute path to the target `.claude` directory
5437
+ * @param {Object} manifest - Loaded manifest (hash lookups for safe-copy decisions)
5438
+ * @param {Object} manifestUpdates - Mutated in place with new hash entries
5439
+ * @returns {Promise<number>} Count of files actually copied/updated this run
5440
+ */
5441
+ async function copyResolverBundleTo(claudeDir, manifest, manifestUpdates) {
5442
+ const pkgRoot = path.join(__dirname, '..');
5443
+ const bundleDir = path.join(claudeDir, 'agentvibes-resolver');
5444
+ const srcBin = path.join(pkgRoot, 'bin', 'resolve-utterance.js');
5445
+ const srcResolver = path.join(pkgRoot, 'src', 'services', 'utterance-resolver.js');
5446
+ const srcLoader = path.join(pkgRoot, 'src', 'services', 'utterance-loader.js');
5447
+
5448
+ const targets = [
5449
+ { src: srcBin, dest: path.join(bundleDir, 'bin', 'resolve-utterance.js') },
5450
+ { src: srcResolver, dest: path.join(bundleDir, 'src', 'services', 'utterance-resolver.js') },
5451
+ { src: srcLoader, dest: path.join(bundleDir, 'src', 'services', 'utterance-loader.js') },
5452
+ ];
5453
+
5454
+ let count = 0;
5455
+ for (const { src, dest } of targets) {
5456
+ await fs.mkdir(path.dirname(dest), { recursive: true });
5457
+ try {
5458
+ const result = await manifestSafeCopy(src, dest, manifest);
5459
+ if (result.action !== 'skipped') {
5460
+ if (result.hash) manifestUpdates[dest] = { hash: result.hash, installedAt: new Date().toISOString() };
5461
+ if (result.action !== 'unchanged') count++;
5462
+ }
5463
+ } catch {
5464
+ // src missing (e.g. stripped package) — skip silently, players fall back to legacy
5465
+ }
5466
+ }
5467
+
5468
+ // The ESM marker — a fixed one-liner (not copied from a src file). Write it
5469
+ // only if absent or different, so a second run is a no-op.
5470
+ try {
5471
+ const pkgJsonPath = path.join(bundleDir, 'package.json');
5472
+ const desired = JSON.stringify({ type: 'module', private: true, name: 'agentvibes-resolver-bundle' }, null, 2) + '\n';
5473
+ let existing = null;
5474
+ try { existing = await fs.readFile(pkgJsonPath, 'utf8'); } catch { /* absent */ }
5475
+ if (existing !== desired) {
5476
+ await fs.mkdir(bundleDir, { recursive: true });
5477
+ await fs.writeFile(pkgJsonPath, desired);
5478
+ count++;
5479
+ }
5480
+ } catch {
5481
+ // best-effort — if this fails the players fall back to legacy, never break
5482
+ }
5483
+ return count;
5484
+ }
5485
+
5486
+ /**
5487
+ * Install/update the resolver bundle into a project-local (or explicitly
5488
+ * targeted) `.claude` directory. Mirrors copyHookFiles' manifest handling.
5489
+ * @param {string} targetDir - Target installation directory (parent of .claude/)
5490
+ * @param {Object} spinner - Ora spinner instance
5491
+ * @returns {Promise<{count: number}>} Count of files copied/updated
5492
+ */
5493
+ async function copyResolverBundle(targetDir, spinner) {
5494
+ spinner = createRobustSpinner(spinner);
5495
+ spinner.start('Installing utterance resolver bundle...');
5496
+ const claudeDir = path.join(targetDir, '.claude');
5497
+ const manifestPath = getProjectManifestPath(targetDir);
5498
+ const manifest = await loadManifest(manifestPath);
5499
+ const manifestUpdates = {};
5500
+
5501
+ const count = await copyResolverBundleTo(claudeDir, manifest, manifestUpdates);
5502
+
5503
+ if (Object.keys(manifestUpdates).length > 0) {
5504
+ await saveManifest(manifestPath, { ...manifest, ...manifestUpdates }).catch(() => { /* best effort */ });
5505
+ }
5506
+ spinner.succeed(chalk.green('Installed utterance resolver bundle!\n'));
5507
+ return { count };
5508
+ }
5509
+
5510
+ /**
5511
+ * Keep the resolver bundle up to date in the global ~/.claude/ tree on every
5512
+ * `npx agentvibes update`, regardless of targetDir — mirrors updateGlobalHooks()
5513
+ * since most real installs point their hooks at $HOME, not a project dir.
5514
+ * @param {string} [homeDirOverride] - Override home dir (for testing only)
5515
+ * @returns {Promise<number>} Count of files copied/updated
5516
+ */
5517
+ async function updateGlobalResolverBundle(homeDirOverride) {
5518
+ const homeDir = homeDirOverride || os.homedir();
5519
+ const claudeDir = path.join(homeDir, '.claude');
5520
+ const manifestPath = getGlobalManifestPath(homeDir);
5521
+ const manifest = await loadManifest(manifestPath);
5522
+ const manifestUpdates = { ...manifest };
5523
+
5524
+ const count = await copyResolverBundleTo(claudeDir, manifest, manifestUpdates);
5525
+
5526
+ await saveManifest(manifestPath, manifestUpdates).catch(() => { /* best effort */ });
5527
+ return count;
5528
+ }
5529
+
5359
5530
  /**
5360
5531
  * Restart the AgentVibes TTS queue watcher on Windows after an update.
5361
5532
  * Only runs if the watcher is already installed (~/.agentvibes/tts-watcher.ps1 exists),
@@ -5378,7 +5549,9 @@ async function restartWatcherIfInstalled(homeDirOverride) {
5378
5549
  return false;
5379
5550
  }
5380
5551
 
5381
- const { spawnSync, spawn } = require('child_process');
5552
+ // spawn/spawnSync come from the top-level ESM import (line 48). A require()
5553
+ // here throws "require is not defined" in this "type":"module" package and
5554
+ // aborted `agentvibes update` mid-run for every SSH-receiver user.
5382
5555
 
5383
5556
  // Kill old watcher — use array args to avoid quoting issues
5384
5557
  spawnSync('powershell.exe', [ // NOSONAR
@@ -5427,6 +5600,12 @@ async function performUpdateOperations(targetDir, spinner) {
5427
5600
  const hookResult = await copyHookFiles(targetDir, silentSpinner);
5428
5601
  console.log(chalk.green(`✓ Updated ${hookResult.count} TTS scripts`));
5429
5602
 
5603
+ // Update the utterance resolver bundle alongside the hooks it feeds (AVI-S8.5 Stage 2)
5604
+ const resolverResult = await copyResolverBundle(targetDir, silentSpinner);
5605
+ if (resolverResult.count > 0) {
5606
+ console.log(chalk.green(`✓ Updated ${resolverResult.count} resolver bundle files`));
5607
+ }
5608
+
5430
5609
  // Also update critical hooks in global ~/.claude/hooks/ if present (fixes stale installs)
5431
5610
  const hooksSubdir = isNativeWindows() ? 'hooks-windows' : 'hooks';
5432
5611
  const srcHooksDir = path.join(__dirname, '..', '.claude', hooksSubdir);
@@ -5435,6 +5614,12 @@ async function performUpdateOperations(targetDir, spinner) {
5435
5614
  console.log(chalk.green(`✓ Updated ${globalHooksUpdated} critical scripts in ~/.claude/hooks/`));
5436
5615
  }
5437
5616
 
5617
+ // Keep the global ~/.claude/ resolver bundle current too (mirrors updateGlobalHooks)
5618
+ const globalResolverUpdated = await updateGlobalResolverBundle();
5619
+ if (globalResolverUpdated > 0) {
5620
+ console.log(chalk.green(`✓ Updated ${globalResolverUpdated} resolver bundle files in ~/.claude/`));
5621
+ }
5622
+
5438
5623
  // On Windows: restart the TTS queue watcher if it was previously installed via
5439
5624
  // setup-ssh-receiver.ps1. This propagates hook updates without requiring the
5440
5625
  // user to manually run update-watcher.ps1 after every `npx agentvibes update`.
@@ -5648,6 +5833,7 @@ async function install(options = {}) {
5648
5833
  // Copy all files silently
5649
5834
  await copyCommandFiles(targetDir, silentSpinner);
5650
5835
  await copyHookFiles(targetDir, silentSpinner);
5836
+ await copyResolverBundle(targetDir, silentSpinner);
5651
5837
  await copyPersonalityFiles(targetDir, silentSpinner);
5652
5838
  await copyPluginFiles(targetDir, silentSpinner);
5653
5839
  await copyBmadConfigFiles(targetDir, silentSpinner);
@@ -5660,6 +5846,8 @@ async function install(options = {}) {
5660
5846
  const hooksSubdirInstall = isNativeWindows() ? 'hooks-windows' : 'hooks';
5661
5847
  const srcHooksDirInstall = path.join(__dirname, '..', '.claude', hooksSubdirInstall);
5662
5848
  await updateGlobalHooks(srcHooksDirInstall);
5849
+ // Populate global ~/.claude/ resolver bundle too, same reasoning as above (AVI-S8.5 Stage 2)
5850
+ await updateGlobalResolverBundle();
5663
5851
 
5664
5852
  await configureSessionStartHook(targetDir, silentSpinner);
5665
5853
  await configurePartyModeHook(targetDir, silentSpinner);
@@ -6805,11 +6993,13 @@ export {
6805
6993
  copyConfigFiles, copyCodexFiles, configureSessionStartHook, configurePartyModeHook, ensureGitRepo,
6806
6994
  installPluginManifest, checkAndInstallPiper,
6807
6995
  updateGlobalHooks, updateCommandFiles, updatePersonalityFiles,
6996
+ copyResolverBundle, updateGlobalResolverBundle,
6808
6997
  CRITICAL_HOOKS, CRITICAL_HOOKS_WINDOWS,
6809
6998
  // Manifest utilities (used by tests and external tooling)
6810
6999
  getProjectManifestPath, getGlobalManifestPath,
6811
7000
  loadManifest, saveManifest, computeFileHash, manifestSafeCopy, removeManifestFiles,
6812
7001
  // Pure helper functions exported for testing
7002
+ readJsonConfigSafe, backupConfigFile,
6813
7003
  isPiperProvider, supportsEmoji, getPersonalityIcon,
6814
7004
  detectEnvironment, createPageHeaderFooter, buildNavigationChoices, handleNavigationAction, getPageTitle,
6815
7005
  getUserShell, showWelcome, getReleaseInfoBoxen, generateActivationInstructions,