agentvibes 5.7.1 → 5.7.3

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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  [![Publish](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml/badge.svg)](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml)
12
12
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
13
13
 
14
- **Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.7.1
14
+ **Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.7.3
15
15
 
16
16
  ---
17
17
 
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.7.1",
4
+ "version": "5.7.3",
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": [
@@ -109,6 +109,29 @@ function _detect(players, env) {
109
109
  return null;
110
110
  }
111
111
 
112
+ /**
113
+ * Detect the first LLM configured with remote transport in
114
+ * ~/.agentvibes/transport-config.json.
115
+ *
116
+ * Returns the LLM key (e.g. "claude-code") whose `mode` is "remote", or
117
+ * null if no such entry exists or the file is absent/invalid.
118
+ * Used by TUI voice-preview functions to route audio through the correct
119
+ * SSH transport rather than playing locally.
120
+ *
121
+ * @returns {string|null}
122
+ */
123
+ export function detectRemoteLlm() {
124
+ const cfgPath = path.join(os.homedir(), '.agentvibes', 'transport-config.json');
125
+ if (!fs.existsSync(cfgPath)) return null;
126
+ try {
127
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
128
+ for (const [llm, opts] of Object.entries(cfg)) {
129
+ if (opts && opts.mode === 'remote') return llm;
130
+ }
131
+ } catch {}
132
+ return null;
133
+ }
134
+
112
135
  /**
113
136
  * Detect the best available MP3 player.
114
137
  * On Windows, falls back to ffplay/mpv if installed, otherwise null.
@@ -18,7 +18,7 @@ import {
18
18
  PIPER_VOICES_DIR, SAMPLE_PHRASES,
19
19
  parseMultiSpeaker, scanInstalledVoices, getVoiceMeta, genderIconTag,
20
20
  } from './voices-tab.js';
21
- import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
21
+ import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
22
22
  import { destroyList } from '../widgets/destroy-list.js';
23
23
  import { BRAND_PINK } from '../brand-colors.js';
24
24
  import { t } from '../../i18n/strings.js';
@@ -1031,64 +1031,35 @@ ${_tl('bmadDesc')}
1031
1031
  if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); screen.render(); return; }
1032
1032
  _killVP();
1033
1033
 
1034
- const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
1035
-
1036
- const _ms = parseMultiSpeaker(voiceId);
1037
- const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
1038
- const safeBase = path.resolve(PIPER_VOICES_DIR);
1039
- if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
1040
-
1041
- const tempWav = _secureTempWav('vp');
1042
1034
  const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
1035
+ const playTtsScript = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
1036
+ if (!fs.existsSync(playTtsScript)) return;
1043
1037
 
1044
- // Resolve piper binary (on Windows, find piper.exe)
1045
- let _piperBin = 'piper';
1046
- if (_isWin) {
1047
- const _lad = process.env.LOCALAPPDATA ||
1048
- (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
1049
- if (_lad) {
1050
- const _ep = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
1051
- if (fs.existsSync(_ep)) _piperBin = _ep;
1052
- }
1053
- }
1038
+ const remoteLlm = detectRemoteLlm();
1039
+ const args = [playTtsScript, phrase, voiceId];
1040
+ if (remoteLlm) args.push('--llm', remoteLlm);
1054
1041
 
1055
- const args = ['--model', voicePath, '--output_file', tempWav];
1056
- if (_ms.speakerId != null) args.push('--speaker', String(_ms.speakerId));
1057
- const piper = spawn(_piperBin, args, {
1058
- stdio: ['pipe', 'ignore', 'ignore'],
1059
- detached: !_isWin,
1060
- windowsHide: true,
1042
+ _previewProc = spawn('bash', args, {
1043
+ stdio: 'ignore',
1044
+ detached: true,
1061
1045
  env: _spawnEnv,
1046
+ cwd: _projectRoot,
1062
1047
  });
1063
- piper.stdin.write(phrase + '\n');
1064
- piper.stdin.end();
1065
- _previewProc = piper;
1066
1048
  _previewVoiceId = voiceId;
1067
1049
 
1068
1050
  if (!_vpClosed) {
1069
- vpPreviewLine.setContent(`{bright-cyan-fg}♪ Synthesizing: ${voiceId}...{/bright-cyan-fg}`);
1051
+ vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}...{/bright-cyan-fg}`);
1070
1052
  screen.render();
1071
1053
  }
1072
1054
 
1073
- piper.on('exit', (code) => {
1074
- if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
1075
- if (code !== 0) { _previewProc = null; _previewVoiceId = null; try { fs.unlinkSync(tempWav); } catch {} return; }
1076
- const wp = detectWavPlayer(_spawnEnv);
1077
- if (!wp) return;
1078
- const pp = spawn(wp.bin, wp.args(tempWav), {
1079
- stdio: 'ignore',
1080
- detached: !_isWin,
1081
- windowsHide: true,
1082
- env: _spawnEnv,
1083
- });
1084
- _previewProc = pp;
1085
- if (!_vpClosed) { vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}{/bright-cyan-fg}`); screen.render(); }
1086
- pp.on('exit', () => {
1087
- if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); } }
1088
- try { fs.unlinkSync(tempWav); } catch {}
1089
- });
1055
+ _previewProc.on('exit', () => {
1056
+ if (_previewVoiceId === voiceId) {
1057
+ _previewVoiceId = null;
1058
+ _previewProc = null;
1059
+ if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); }
1060
+ }
1090
1061
  });
1091
- piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
1062
+ _previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
1092
1063
  }
1093
1064
 
1094
1065
  vpList.key(['enter'], () => {
@@ -1220,8 +1191,10 @@ ${_tl('bmadDesc')}
1220
1191
  const _spawnEnv = buildAudioEnv();
1221
1192
  const scriptDir = path.join(_projectRoot, '.claude', 'hooks');
1222
1193
  const plainScript = path.join(scriptDir, 'play-tts.sh');
1194
+ const remoteLlm = detectRemoteLlm();
1223
1195
  const args = [plainScript, phrase];
1224
1196
  if (voiceId) args.push(voiceId);
1197
+ if (remoteLlm) args.push('--llm', remoteLlm);
1225
1198
 
1226
1199
  const proc = spawn('bash', args, {
1227
1200
  stdio: ['ignore', 'ignore', 'ignore'],
@@ -24,7 +24,7 @@ import {
24
24
  } from './voices-tab.js';
25
25
  import { LanguageService } from '../../services/language-service.js';
26
26
  import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
27
- import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
27
+ import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
28
28
  import { destroyList } from '../widgets/destroy-list.js';
29
29
  import { openReverbPicker } from '../widgets/reverb-picker.js';
30
30
  import { openPersonalityPicker } from '../widgets/personality-picker.js';
@@ -633,61 +633,35 @@ export function createSettingsTab(screen, services) {
633
633
  if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
634
634
  _killVP();
635
635
 
636
- const _ms = parseMultiSpeaker(voiceId);
637
- const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
638
- const safeBase = path.resolve(PIPER_VOICES_DIR);
639
- if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
640
-
641
- const tempWav = _secureTempWav('vp');
642
636
  const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
637
+ const playTtsScript = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
638
+ if (!fs.existsSync(playTtsScript)) return;
643
639
 
644
- let _piperBin = 'piper';
645
- if (_isWin) {
646
- const _lad = process.env.LOCALAPPDATA ||
647
- (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
648
- if (_lad) {
649
- const _ep = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
650
- if (fs.existsSync(_ep)) _piperBin = _ep;
651
- }
652
- }
640
+ const remoteLlm = detectRemoteLlm();
641
+ const args = [playTtsScript, phrase, voiceId];
642
+ if (remoteLlm) args.push('--llm', remoteLlm);
653
643
 
654
- const args = ['--model', voicePath, '--output_file', tempWav];
655
- if (_ms.speakerId != null) args.push('--speaker', String(_ms.speakerId));
656
- const piper = spawn(_piperBin, args, {
657
- stdio: ['pipe', 'ignore', 'ignore'],
658
- detached: !_isWin,
659
- windowsHide: true,
644
+ _previewProc = spawn('bash', args, {
645
+ stdio: 'ignore',
646
+ detached: true,
660
647
  env: _spawnEnv,
648
+ cwd: _projectRoot,
661
649
  });
662
- piper.stdin.write(phrase + '\n');
663
- piper.stdin.end();
664
- _previewProc = piper;
665
650
  _previewVoiceId = voiceId;
666
651
 
667
652
  if (!_vpClosed) {
668
- vpPreviewLine.setContent(`{cyan-fg}♪ Synthesizing: ${voiceId}...{/cyan-fg}`);
653
+ vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}...{/cyan-fg}`);
669
654
  _refreshVP();
670
655
  }
671
656
 
672
- piper.on('exit', (code) => {
673
- if (_previewVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
674
- if (code !== 0) { _previewProc = null; _previewVoiceId = null; try { fs.unlinkSync(tempWav); } catch {} return; }
675
- const wp = detectWavPlayer(_spawnEnv);
676
- if (!wp) return;
677
- const pp = spawn(wp.bin, wp.args(tempWav), {
678
- stdio: 'ignore',
679
- detached: !_isWin,
680
- windowsHide: true,
681
- env: _spawnEnv,
682
- });
683
- _previewProc = pp;
684
- if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}{/cyan-fg}`); screen.render(); }
685
- pp.on('exit', () => {
686
- if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
687
- try { fs.unlinkSync(tempWav); } catch {}
688
- });
657
+ _previewProc.on('exit', () => {
658
+ if (_previewVoiceId === voiceId) {
659
+ _previewVoiceId = null;
660
+ _previewProc = null;
661
+ if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); }
662
+ }
689
663
  });
690
- piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
664
+ _previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
691
665
  }
692
666
 
693
667
  vpList.key(['enter'], () => {
@@ -10,7 +10,7 @@ import fs from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import { spawn } from 'node:child_process';
12
12
  import { destroyList } from './destroy-list.js';
13
- import { buildAudioEnv } from '../audio-env.js';
13
+ import { buildAudioEnv, detectRemoteLlm } from '../audio-env.js';
14
14
  import { BRAND_PINK } from '../brand-colors.js';
15
15
  import { PERSONALITY_EMOJIS, PERSONALITIES } from '../constants/personalities.js';
16
16
 
@@ -138,7 +138,10 @@ export function openPersonalityPicker(screen, currentPersonality, onSelect, onCl
138
138
  });
139
139
  } else {
140
140
  const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
141
- _pickerTtsProc = spawn('bash', [ttsScript, phrase], {
141
+ const remoteLlm = detectRemoteLlm();
142
+ const ttsArgs = [ttsScript, phrase];
143
+ if (remoteLlm) ttsArgs.push('--llm', remoteLlm);
144
+ _pickerTtsProc = spawn('bash', ttsArgs, {
142
145
  stdio: 'ignore',
143
146
  detached: true,
144
147
  env: _env,