agentvibes 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/agentvibes.js CHANGED
@@ -75,6 +75,16 @@ export function resolveStartTab(args, configService) {
75
75
  return { startTab: 'install' };
76
76
  }
77
77
 
78
+ if (cmd === 'update') {
79
+ // Always route update to CLI installer (src/installer.js)
80
+ return { cliUpdate: true, args: args.slice(1) };
81
+ }
82
+
83
+ if (cmd === 'uninstall') {
84
+ // Always route uninstall to CLI installer (src/installer.js)
85
+ return { cliUninstall: true, args: args.slice(1) };
86
+ }
87
+
78
88
  if (cmd === 'config' || cmd === 'configure') {
79
89
  return { startTab: 'settings' };
80
90
  }
@@ -154,6 +164,24 @@ if (_argv1 === _thisFile) {
154
164
  process.exit(0);
155
165
  }
156
166
 
167
+ if (result.cliUpdate) {
168
+ const installerPath = path.resolve(__dirname, '..', 'src', 'installer.js');
169
+ execFileSync(process.execPath, [installerPath, 'update', ...result.args], {
170
+ stdio: 'inherit',
171
+ shell: false,
172
+ });
173
+ process.exit(0);
174
+ }
175
+
176
+ if (result.cliUninstall) {
177
+ const installerPath = path.resolve(__dirname, '..', 'src', 'installer.js');
178
+ execFileSync(process.execPath, [installerPath, 'uninstall', ...result.args], {
179
+ stdio: 'inherit',
180
+ shell: false,
181
+ });
182
+ process.exit(0);
183
+ }
184
+
157
185
  launchConsole({ startTab: result.startTab }).catch(err => {
158
186
  process.stderr.write(`Failed to launch AgentVibes console: ${err.message}\n`);
159
187
  process.exit(1);
@@ -192,6 +192,17 @@ class AgentVibesServer:
192
192
  original_language = await self._get_language()
193
193
  await self._run_script(self.LANGUAGE_MANAGER_SCRIPT, ["set", language])
194
194
 
195
+ # Resolve LLM key: AGENTVIBES_LLM > CLAUDECODE=1 > AGENTVIBES_MCP_FALLBACK > "default"
196
+ llm_key = os.environ.get("AGENTVIBES_LLM", "").strip()
197
+ if llm_key and not _re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", llm_key):
198
+ llm_key = ""
199
+ if not llm_key and os.environ.get("CLAUDECODE", "").strip() == "1":
200
+ llm_key = "claude-code"
201
+ if not llm_key:
202
+ fallback = os.environ.get("AGENTVIBES_MCP_FALLBACK", "").strip()
203
+ if fallback and _re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", fallback):
204
+ llm_key = fallback
205
+
195
206
  # Call the TTS script via appropriate shell
196
207
  tts_script = "play-tts.ps1" if self.is_windows else "play-tts.sh"
197
208
  play_tts = self.hooks_dir / tts_script
@@ -199,8 +210,13 @@ class AgentVibesServer:
199
210
  args = ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(play_tts), text]
200
211
  if voice:
201
212
  args.extend(["-VoiceOverride", voice])
213
+ if llm_key:
214
+ args.extend(["-llm", llm_key])
202
215
  else:
203
- args = ["bash", str(play_tts), text]
216
+ args = ["bash", str(play_tts)]
217
+ if llm_key:
218
+ args.extend(["--llm", llm_key])
219
+ args.append(text)
204
220
  if voice:
205
221
  args.append(voice)
206
222
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "agentvibes",
4
- "version": "5.4.0",
4
+ "version": "5.6.0",
5
5
  "description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
6
6
  "homepage": "https://agentvibes.org",
7
7
  "keywords": [
@@ -185,12 +185,15 @@ export function scanTracks() {
185
185
  const tracksDir = _getTracksDir();
186
186
  try {
187
187
  const files = fs.readdirSync(tracksDir);
188
+ const mp3s = files.filter(f => /\.mp3$/i.test(f));
189
+ // If the directory exists but has no mp3s (e.g. empty npm package dir),
190
+ // fall back to the static catalog so bundled tracks always show.
191
+ if (mp3s.length === 0) return BUILT_IN_TRACK_CATALOG.map(t => ({ ...t, isBuiltIn: true }));
188
192
  const builtInIds = new Set(BUILT_IN_TRACK_CATALOG.map(t => t.id));
189
193
  // Sort by the alphabetic part of the label (skip leading emoji/symbols)
190
194
  // so the order reflects the track NAME, not the emoji codepoint.
191
195
  const _sortKey = (s) => s.replace(/^[^a-zA-Z]+/, '');
192
- return files
193
- .filter(f => /\.mp3$/i.test(f))
196
+ return mp3s
194
197
  .map(f => ({ id: f, label: formatTrackLabel(f), isBuiltIn: builtInIds.has(f) }))
195
198
  .sort((a, b) => _sortKey(a.label).localeCompare(_sortKey(b.label), undefined, { sensitivity: 'base' }));
196
199
  } catch {
@@ -133,6 +133,10 @@ function loadCatalog() {
133
133
  // Build lookup map for O(1) access by voiceId
134
134
  _catalogMap = new Map();
135
135
  for (const c of _catalogEntries) _catalogMap.set(c.voiceId, c);
136
+
137
+ // Patch libritts_r onnx.json files so their speaker IDs become friendly names.
138
+ // Must run after catalog loads so the name mapping is available.
139
+ patchLibriTTSSpeakerNames();
136
140
  }
137
141
 
138
142
  /**
@@ -142,45 +146,48 @@ function loadCatalog() {
142
146
  * Safe to call multiple times — skips if already patched.
143
147
  */
144
148
  function patchLibriTTSSpeakerNames() {
149
+ // Load catalog once for all models
150
+ const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
151
+ if (!fs.existsSync(catalogPath)) return;
152
+ let speakers;
145
153
  try {
146
- const jsonPath = path.join(PIPER_VOICES_DIR, 'en_US-libritts-high.onnx.json');
147
- if (!fs.existsSync(jsonPath)) return;
148
- const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
149
- if (!data.speaker_id_map || data.num_speakers <= 1) return;
150
-
151
- const names = Object.keys(data.speaker_id_map);
152
- // Already patched if first name doesn't start with 'p' followed by digits
153
- if (names.length > 0 && !/^p\d+$/.test(names[0])) return;
154
-
155
- // Build index p-name reverse map
156
- const indexToP = {};
157
- for (const [pname, idx] of Object.entries(data.speaker_id_map)) {
158
- indexToP[idx] = pname;
159
- }
154
+ speakers = JSON.parse(fs.readFileSync(catalogPath, 'utf8')).libritts_speakers ?? {};
155
+ } catch { return; }
156
+
157
+ // Models to patch and how to detect unpatched keys:
158
+ // libritts-high → raw keys are p-prefixed corpus IDs (p3922, p8699, …)
159
+ // libritts_r-* → raw keys are plain numeric corpus IDs (3922, 8699, …)
160
+ const MODELS = [
161
+ { file: 'en_US-libritts-high.onnx.json', notPatched: (k) => /^p\d+$/.test(k) },
162
+ { file: 'en_US-libritts_r-medium.onnx.json', notPatched: (k) => /^\d+$/.test(k) },
163
+ { file: 'en_US-libritts_r-high.onnx.json', notPatched: (k) => /^\d+$/.test(k) },
164
+ ];
165
+
166
+ for (const { file, notPatched } of MODELS) {
167
+ try {
168
+ const jsonPath = path.join(PIPER_VOICES_DIR, file);
169
+ if (!fs.existsSync(jsonPath)) continue;
170
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
171
+ if (!data.speaker_id_map || data.num_speakers <= 1) continue;
160
172
 
161
- // Load friendly names from catalog
162
- const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
163
- if (!fs.existsSync(catalogPath)) return;
164
- const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
165
- const speakers = catalog.libritts_speakers ?? {};
166
-
167
- // Rebuild speaker_id_map with friendly names
168
- const newMap = {};
169
- for (const [idx, pname] of Object.entries(indexToP)) {
170
- const friendly = speakers[idx]?.voice_name;
171
- if (friendly) {
172
- newMap[friendly] = parseInt(idx, 10);
173
- } else {
174
- newMap[pname] = parseInt(idx, 10);
173
+ const names = Object.keys(data.speaker_id_map);
174
+ // Skip if already patched (first key is a friendly name, not a raw corpus ID)
175
+ if (names.length === 0 || !notPatched(names[0])) continue;
176
+
177
+ // Values are 0-based sequential indices into the model — use as catalog key
178
+ const newMap = {};
179
+ for (const [rawKey, idx] of Object.entries(data.speaker_id_map)) {
180
+ const friendly = speakers[String(idx)]?.voice_name;
181
+ newMap[friendly ?? rawKey] = idx;
175
182
  }
176
- }
177
183
 
178
- data.speaker_id_map = newMap;
179
- // Verify file ownership before writing (security: CLAUDE.md)
180
- const stat = fs.statSync(jsonPath);
181
- if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) return;
182
- fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
183
- } catch { /* non-fatal */ }
184
+ data.speaker_id_map = newMap;
185
+ // Verify file ownership before writing (security: CLAUDE.md)
186
+ const stat = fs.statSync(jsonPath);
187
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) continue;
188
+ fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
189
+ } catch { /* non-fatal — skip this model */ }
190
+ }
184
191
  }
185
192
 
186
193
  // Column widths for the multi-column voice list
@@ -417,9 +424,9 @@ export function parseMultiSpeaker(voiceId) {
417
424
  try {
418
425
  const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
419
426
  let speakerId = data.speaker_id_map?.[speakerName] ?? null;
420
- // Fallback: if the .onnx.json still has raw p-names (not yet patched),
427
+ // Fallback: if the .onnx.json still has raw corpus IDs (not yet patched),
421
428
  // look up the numeric speaker ID from voice-assignments.json catalog.
422
- if (speakerId == null && model === 'en_US-libritts-high') {
429
+ if (speakerId == null && (model === 'en_US-libritts-high' || /^en_US-libritts_r-/.test(model))) {
423
430
  try {
424
431
  const catalogPath = path.resolve(__dirname, '..', '..', '..', 'voice-assignments.json');
425
432
  const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
@@ -443,6 +450,9 @@ export function parseMultiSpeaker(voiceId) {
443
450
  * @returns {string[]}
444
451
  */
445
452
  export function scanInstalledVoices() {
453
+ // Ensure catalog is loaded and libritts_r onnx.json files are patched
454
+ // before we read their speaker_id_map keys (otherwise we get raw corpus IDs).
455
+ loadCatalog();
446
456
  try {
447
457
  const files = fs.readdirSync(PIPER_VOICES_DIR);
448
458
  const onnxFiles = files
@@ -588,6 +598,30 @@ export function getVoiceMeta(voiceId) {
588
598
  _metaCache.set(voiceId, result);
589
599
  return result;
590
600
  }
601
+ // libritts_r variants share speaker names with libritts-high after patching
602
+ if (/^en_US-libritts_r-/.test(ms.model)) {
603
+ // After patching: speakerName is a friendly name — look up in libritts-high catalog
604
+ const highCat = _catalogMap.get(`en_US-libritts-high${MS_SEP}${ms.speakerName}`);
605
+ if (highCat) {
606
+ const result = { displayName: highCat.displayName, gender: highCat.gender, provider: `Piper (${ms.model})` };
607
+ _metaCache.set(voiceId, result);
608
+ return result;
609
+ }
610
+ // Before patching: speakerName is a raw numeric corpus ID — resolve via onnx.json value
611
+ if (/^\d+$/.test(ms.speakerName)) {
612
+ try {
613
+ const jsonPath = path.join(PIPER_VOICES_DIR, ms.model + '.onnx.json');
614
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
615
+ const seqIdx = data.speaker_id_map?.[ms.speakerName];
616
+ if (seqIdx != null && _catalogEntries[seqIdx]) {
617
+ const cat = _catalogEntries[seqIdx];
618
+ const result = { displayName: cat.displayName, gender: cat.gender, provider: `Piper (${ms.model})` };
619
+ _metaCache.set(voiceId, result);
620
+ return result;
621
+ }
622
+ } catch { /* fall through */ }
623
+ }
624
+ }
591
625
  // Fallback for speakers not in the catalog (e.g. 16Speakers model)
592
626
  const displayName = uniquifyVoiceName(ms.speakerName.replace(/_/g, ' '));
593
627
  const result = {
package/src/installer.js CHANGED
@@ -3249,6 +3249,7 @@ async function handleTermuxSshConfiguration() {
3249
3249
  * @returns {Promise<{count: number, boxen: string}>} Number of files copied and boxen content
3250
3250
  */
3251
3251
  async function copyCommandFiles(targetDir, spinner) {
3252
+ spinner = createRobustSpinner(spinner);
3252
3253
  spinner.start('Installing /agent-vibes slash commands...');
3253
3254
  const srcCommandsDir = path.join(__dirname, '..', '.claude', 'commands', 'agent-vibes');
3254
3255
  const commandsDir = path.join(targetDir, '.claude', 'commands');
@@ -3462,6 +3463,7 @@ function buildHookInstallationBoxen(installedFiles, failedFiles) {
3462
3463
  * @returns {Promise<{count: number, boxen: string|null}>} Number of files copied and boxen content
3463
3464
  */
3464
3465
  async function copyHookFiles(targetDir, spinner) {
3466
+ spinner = createRobustSpinner(spinner);
3465
3467
  spinner.start('Installing TTS helper scripts...');
3466
3468
  const hooksSubdir = isNativeWindows() ? 'hooks-windows' : 'hooks';
3467
3469
  const srcHooksDir = path.join(__dirname, '..', '.claude', hooksSubdir);
@@ -3516,6 +3518,7 @@ async function copyHookFiles(targetDir, spinner) {
3516
3518
  * @returns {Promise<{count: number, boxen: string|null}>} Number of files copied and boxen content
3517
3519
  */
3518
3520
  async function copyPersonalityFiles(targetDir, spinner) {
3521
+ spinner = createRobustSpinner(spinner);
3519
3522
  spinner.start('Installing personality templates...');
3520
3523
  const srcPersonalitiesDir = path.join(__dirname, '..', '.claude', 'personalities');
3521
3524
  const destPersonalitiesDir = path.join(targetDir, '.claude', 'personalities');
@@ -3598,6 +3601,7 @@ async function copyPersonalityFiles(targetDir, spinner) {
3598
3601
  * @returns {Promise<number>} Number of files copied
3599
3602
  */
3600
3603
  async function copyPluginFiles(targetDir, spinner) {
3604
+ spinner = createRobustSpinner(spinner);
3601
3605
  spinner.start('Installing BMAD plugin files...');
3602
3606
  const srcPluginsDir = path.join(__dirname, '..', '.claude', 'plugins');
3603
3607
  const destPluginsDir = path.join(targetDir, '.claude', 'plugins');
@@ -3632,6 +3636,7 @@ async function copyPluginFiles(targetDir, spinner) {
3632
3636
  * @returns {Promise<number>} Number of files copied
3633
3637
  */
3634
3638
  async function copyBmadConfigFiles(targetDir, spinner) {
3639
+ spinner = createRobustSpinner(spinner);
3635
3640
  spinner.start('Installing BMAD config files...');
3636
3641
  const srcBmadDir = path.join(__dirname, '..', '.agentvibes', 'bmad');
3637
3642
  const destBmadDir = path.join(targetDir, '.agentvibes', 'bmad');
@@ -3664,6 +3669,7 @@ async function copyBmadConfigFiles(targetDir, spinner) {
3664
3669
  * @returns {Promise<{count: number, boxen: string}>} Number of files copied and boxen content
3665
3670
  */
3666
3671
  async function copyBackgroundMusicFiles(targetDir, spinner) {
3672
+ spinner = createRobustSpinner(spinner);
3667
3673
  spinner.start('Installing background music tracks...');
3668
3674
  const srcBackgroundsDir = path.join(__dirname, '..', '.claude', 'audio', 'tracks');
3669
3675
  const destBackgroundsDir = path.join(targetDir, '.claude', 'audio', 'tracks');
@@ -3786,6 +3792,7 @@ async function copyBackgroundMusicFiles(targetDir, spinner) {
3786
3792
  * @returns {Promise<number>} Number of files copied
3787
3793
  */
3788
3794
  async function copyConfigFiles(targetDir, spinner) {
3795
+ spinner = createRobustSpinner(spinner);
3789
3796
  spinner.start('Installing configuration files...');
3790
3797
  const srcConfigDir = path.join(__dirname, '..', '.claude', 'config');
3791
3798
  const destConfigDir = path.join(targetDir, '.claude', 'config');
@@ -3848,6 +3855,7 @@ async function copyConfigFiles(targetDir, spinner) {
3848
3855
  * @param {Object} spinner - Ora spinner instance
3849
3856
  */
3850
3857
  async function copyCodexFiles(targetDir, spinner) {
3858
+ spinner = createRobustSpinner(spinner);
3851
3859
  spinner.start('Installing Codex integration files...');
3852
3860
  const srcCodexDir = path.join(__dirname, '..', '.codex');
3853
3861
  const destCodexDir = path.join(targetDir, '.codex');
@@ -3901,6 +3909,7 @@ async function copyCodexFiles(targetDir, spinner) {
3901
3909
  * @param {Object} spinner - Ora spinner instance
3902
3910
  */
3903
3911
  async function configureSessionStartHook(targetDir, spinner) {
3912
+ spinner = createRobustSpinner(spinner);
3904
3913
  spinner.start('Configuring AgentVibes hook for automatic TTS...');
3905
3914
  const claudeDir = path.join(targetDir, '.claude');
3906
3915
  const settingsPath = path.join(claudeDir, 'settings.json');
@@ -3956,6 +3965,7 @@ async function configureSessionStartHook(targetDir, spinner) {
3956
3965
  * @param {Object} spinner - Ora spinner instance
3957
3966
  */
3958
3967
  async function configurePartyModeHook(targetDir, spinner, homeDirOverride) {
3968
+ spinner = createRobustSpinner(spinner);
3959
3969
  spinner.start('Configuring BMAD party mode TTS hook...');
3960
3970
  const homeDir = homeDirOverride || os.homedir();
3961
3971
  const globalClaudeDir = path.join(homeDir, '.claude');
@@ -198,7 +198,7 @@ export async function installClaudeMcp(targetDir) {
198
198
 
199
199
  try {
200
200
  // Copy hooks, commands, config, personality, plugin, bmad config files
201
- const silentSpinner = { start: () => {}, succeed: () => {}, fail: () => {} };
201
+ const silentSpinner = { start: () => {}, stop: () => {}, succeed: () => {}, fail: () => {}, warn: () => {}, info: () => {}, stopAndPersist: () => {}, get text() { return ''; }, set text(_) {}, get isSpinning() { return false; } };
202
202
  const installer = await import('../installer.js');
203
203
  await installer.copyHookFiles(targetDir, silentSpinner);
204
204
  await installer.copyCommandFiles(targetDir, silentSpinner);