agentvibes 5.7.4 → 5.7.6
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/.agentvibes/install-manifest.json +115 -111
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/play-tts-ssh-remote.sh +50 -10
- package/.claude/hooks/play-tts.sh +2 -2
- package/README.md +15 -3
- package/RELEASE_NOTES.md +67 -0
- package/mcp-server/WINDOWS_SETUP.md +1 -1
- package/mcp-server/docs/troubleshooting-audio.md +1 -1
- package/package.json +1 -1
- package/src/console/app.js +7 -0
- package/src/console/audio-env.js +19 -0
- package/src/console/tabs/agents-tab.js +64 -10
- package/src/console/tabs/music-tab.js +4 -4
- package/src/console/tabs/settings-tab.js +75 -1
- package/src/console/tabs/setup-tab.js +94 -28
- package/src/console/tabs/voices-tab.js +51 -33
- package/src/installer.js +15 -1
- package/src/services/llm-provider-service.js +4 -4
- package/templates/agentvibes-receiver.sh +139 -66
|
@@ -17,6 +17,7 @@ set -euo pipefail
|
|
|
17
17
|
TEXT="${1:-}"
|
|
18
18
|
VOICE="${2:-en_US-lessac-medium}"
|
|
19
19
|
AGENT_NAME="${3:-default}"
|
|
20
|
+
AGENT_PROFILE_FILE="${4:-}"
|
|
20
21
|
|
|
21
22
|
# LLM identity — forwarded to the remote so it can look up its own
|
|
22
23
|
# audio-effects.cfg llm:<name> row for voice, reverb, music, pretext, engine.
|
|
@@ -150,12 +151,23 @@ SOX_EFFECTS=""
|
|
|
150
151
|
BG_FILE=""
|
|
151
152
|
BG_VOLUME="0.10"
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
# Use CLAUDE_PROJECT_DIR (injected via --project-dir by play-tts.sh) so the
|
|
155
|
+
# agent-name / default row is read from the user's project, not the package.
|
|
156
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" && -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then
|
|
157
|
+
EFFECTS_CFG="$CLAUDE_PROJECT_DIR/.claude/config/audio-effects.cfg"
|
|
158
|
+
else
|
|
159
|
+
EFFECTS_CFG="$PROJECT_ROOT/.claude/config/audio-effects.cfg"
|
|
160
|
+
fi
|
|
154
161
|
if [[ -f "$EFFECTS_CFG" ]]; then
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
# awk exact field-1 match — no regex injection risk from AGENT_NAME
|
|
163
|
+
CONFIG_LINE=$(awk -F'|' -v k="${AGENT_NAME}" '$1==k{print;exit}' "$EFFECTS_CFG" 2>/dev/null || true)
|
|
164
|
+
[[ -z "$CONFIG_LINE" ]] && \
|
|
165
|
+
CONFIG_LINE=$(awk -F'|' '$1=="default"{print;exit}' "$EFFECTS_CFG" 2>/dev/null || true)
|
|
157
166
|
if [[ -n "$CONFIG_LINE" ]]; then
|
|
158
167
|
IFS='|' read -r _ SOX_EFFECTS BG_FILE BG_VOLUME <<< "$CONFIG_LINE"
|
|
168
|
+
SOX_EFFECTS="${SOX_EFFECTS## }"; SOX_EFFECTS="${SOX_EFFECTS%% }"
|
|
169
|
+
BG_FILE="${BG_FILE## }"; BG_FILE="${BG_FILE%% }"
|
|
170
|
+
BG_VOLUME="${BG_VOLUME## }"; BG_VOLUME="${BG_VOLUME%% }"
|
|
159
171
|
fi
|
|
160
172
|
fi
|
|
161
173
|
|
|
@@ -171,14 +183,18 @@ LLM_REVERB=""
|
|
|
171
183
|
LLM_BG_FILE=""
|
|
172
184
|
LLM_BG_VOLUME=""
|
|
173
185
|
|
|
174
|
-
# Build config search path
|
|
175
|
-
#
|
|
176
|
-
#
|
|
186
|
+
# Build config search path for LLM-specific row lookup.
|
|
187
|
+
# Priority: CLAUDE_PROJECT_DIR (real user project) → global HOME fallback.
|
|
188
|
+
# PROJECT_ROOT is intentionally excluded when CLAUDE_PROJECT_DIR is set and
|
|
189
|
+
# different — prevents the AgentVibes package's own audio-effects.cfg from
|
|
190
|
+
# bleeding into a user project that doesn't define its own llm: row.
|
|
177
191
|
_llm_cfg_paths=()
|
|
178
192
|
if [[ -n "${CLAUDE_PROJECT_DIR:-}" && "$CLAUDE_PROJECT_DIR" != "$PROJECT_ROOT" ]]; then
|
|
179
193
|
_llm_cfg_paths+=("$CLAUDE_PROJECT_DIR/.claude/config/audio-effects.cfg")
|
|
194
|
+
else
|
|
195
|
+
_llm_cfg_paths+=("$PROJECT_ROOT/.claude/config/audio-effects.cfg")
|
|
180
196
|
fi
|
|
181
|
-
_llm_cfg_paths+=("$
|
|
197
|
+
_llm_cfg_paths+=("$HOME/.claude/config/audio-effects.cfg")
|
|
182
198
|
|
|
183
199
|
_llm_key="llm:${LLM_NAME}"
|
|
184
200
|
_llm_row_found=0
|
|
@@ -210,7 +226,24 @@ done
|
|
|
210
226
|
[[ -n "$LLM_BG_FILE" ]] && BG_FILE="$LLM_BG_FILE"
|
|
211
227
|
[[ -n "$LLM_BG_VOLUME" ]] && BG_VOLUME="$LLM_BG_VOLUME"
|
|
212
228
|
|
|
213
|
-
#
|
|
229
|
+
# Per-agent profile (written by bmad-speak.sh) takes highest priority for music
|
|
230
|
+
if [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
|
|
231
|
+
_prof_track=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(p.backgroundMusic?.track??'')}catch{process.stdout.write('')}" 2>/dev/null || true)
|
|
232
|
+
_prof_vol=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.volume??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
|
|
233
|
+
_prof_enabled=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.enabled??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
|
|
234
|
+
if [[ "$_prof_enabled" == "true" ]] && [[ -n "$_prof_track" ]]; then
|
|
235
|
+
BG_FILE="$_prof_track"
|
|
236
|
+
if [[ "$_prof_vol" =~ ^[0-9]+$ ]]; then
|
|
237
|
+
BG_VOLUME=$(awk "BEGIN{printf \"%.2f\", ${_prof_vol}/100}")
|
|
238
|
+
fi
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# PRETEXT is NOT extracted from the llm row here.
|
|
243
|
+
# play-tts.sh already prepends the llm row's pretext to TEXT before calling this script.
|
|
244
|
+
# Extracting it again and sending it as a separate JSON field would cause the receiver
|
|
245
|
+
# to prepend it a second time — the user hears the intro text twice.
|
|
246
|
+
# PRETEXT here is only for the (rare) pretext.txt override file, not the llm row.
|
|
214
247
|
PRETEXT=""
|
|
215
248
|
PRETEXT_FILE="$PROJECT_ROOT/.agentvibes/config/pretext.txt"
|
|
216
249
|
if [[ -f "$PRETEXT_FILE" ]]; then
|
|
@@ -265,9 +298,9 @@ build_json_payload() {
|
|
|
265
298
|
--arg llm "$LLM_NAME" \
|
|
266
299
|
'{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, project: $project, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm}'
|
|
267
300
|
else
|
|
268
|
-
# Manual JSON — escape
|
|
301
|
+
# Manual JSON — escape backslashes, quotes, control chars
|
|
269
302
|
local escaped_text
|
|
270
|
-
escaped_text=$(printf '%s' "$TEXT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g')
|
|
303
|
+
escaped_text=$(printf '%s' "$TEXT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ' | sed 's/\r//g')
|
|
271
304
|
local escaped_pretext
|
|
272
305
|
escaped_pretext=$(printf '%s' "$PRETEXT" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
273
306
|
printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","project":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s"}' \
|
|
@@ -289,6 +322,13 @@ fi
|
|
|
289
322
|
# Send to receiver via SSH (fire and forget — backgrounded)
|
|
290
323
|
# ---------------------------------------------------------------------------
|
|
291
324
|
|
|
325
|
+
# In test mode, dump the decoded payload to stdout so tests can inspect it
|
|
326
|
+
# without needing a real SSH connection or a mock binary.
|
|
327
|
+
if [[ "${AGENTVIBES_TEST_MODE:-false}" == "true" ]]; then
|
|
328
|
+
echo "$JSON_PAYLOAD"
|
|
329
|
+
exit 0
|
|
330
|
+
fi
|
|
331
|
+
|
|
292
332
|
echo "Sending to $SSH_HOST..." >&2
|
|
293
333
|
|
|
294
334
|
# Build SSH args — use explicit key/port from config if available, else rely on ~/.ssh/config
|
|
@@ -367,7 +367,7 @@ speak_text() {
|
|
|
367
367
|
bash "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$text" "$voice"
|
|
368
368
|
;;
|
|
369
369
|
ssh-remote)
|
|
370
|
-
bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$text" "$voice"
|
|
370
|
+
bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$text" "$voice" "" "${profile_file:-}"
|
|
371
371
|
;;
|
|
372
372
|
agentvibes-receiver)
|
|
373
373
|
bash "$SCRIPT_DIR/play-tts-agentvibes-receiver-for-voiceless-connections.sh" "$text" "$voice"
|
|
@@ -497,7 +497,7 @@ case "$ACTIVE_PROVIDER" in
|
|
|
497
497
|
exec bash "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$TEXT" "$VOICE_OVERRIDE"
|
|
498
498
|
;;
|
|
499
499
|
ssh-remote)
|
|
500
|
-
exec bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$TEXT" "$VOICE_OVERRIDE"
|
|
500
|
+
exec bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$TEXT" "$VOICE_OVERRIDE" "" "${AGENT_PROFILE_FILE:-}"
|
|
501
501
|
;;
|
|
502
502
|
agentvibes-receiver)
|
|
503
503
|
exec bash "$SCRIPT_DIR/play-tts-agentvibes-receiver-for-voiceless-connections.sh" "$TEXT" "$VOICE_OVERRIDE"
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml)
|
|
12
12
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
13
13
|
|
|
14
|
-
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.7.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.7.6
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -40,7 +40,19 @@ Whether you're coding in Claude Code, chatting in Claude Desktop, using Warp Ter
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
## 🌟 NEW IN v5.7.
|
|
43
|
+
## 🌟 NEW IN v5.7.6 — SSH Remote Payload Integrity + Receiver Rewrite
|
|
44
|
+
|
|
45
|
+
**SSH remote music/voice fix:** The correct project music track and voice now reach the remote receiver — previously the global config was used instead of the active project's settings.
|
|
46
|
+
|
|
47
|
+
**Bash receiver rewrite:** The Linux/Termux `agentvibes-receiver.sh` has been fully rewritten to decode the current base64 JSON payload format. The old positional-arg format from pre-v5.5 is gone.
|
|
48
|
+
|
|
49
|
+
**No more double intro:** The personality pretext (e.g., "Bcs latin dance here") is no longer spoken twice over SSH remote. `play-tts.sh` prepends it to the text; the receiver no longer gets a separate pretext field to prepend again.
|
|
50
|
+
|
|
51
|
+
**SSH host visible in TUI:** The Settings and Voices tabs now display your configured SSH remote host alias.
|
|
52
|
+
|
|
53
|
+
**Security fixes** and 24 new BATS tests covering the full sender → receiver round-trip.
|
|
54
|
+
|
|
55
|
+
## v5.7.5 — TUI Button Contrast + BMAD Routing Fixes
|
|
44
56
|
|
|
45
57
|
**BMAD v6.6.0:** AgentVibes now detects the new `.claude/skills/*/agents/` agent structure, correctly handles globally-installed BMAD at `~/_bmad`, and gracefully skips v6.6+ plain-Markdown agents during TTS injection instead of erroring. The BMAD tab now shows detection correctly for global installs.
|
|
46
58
|
|
|
@@ -341,7 +353,7 @@ All 50+ Piper voices AgentVibes provides are sourced from Hugging Face's open-so
|
|
|
341
353
|
|
|
342
354
|
## 📰 Latest Release
|
|
343
355
|
|
|
344
|
-
**[v3.6.0 - "Voice Explorer" Release](https://github.com/paulpreibisch/AgentVibes/releases/tag/
|
|
356
|
+
**[v3.6.0 - "Voice Explorer" Release](https://github.com/paulpreibisch/AgentVibes/releases/tag/v5.7.5)** 🎉
|
|
345
357
|
|
|
346
358
|
### 🎤 Voices Tab — Browse & Sample 914 Voices
|
|
347
359
|
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🔧 v5.7.6 — SSH Remote Payload Integrity + Receiver Rewrite
|
|
4
|
+
|
|
5
|
+
**Released:** 2026-05-16
|
|
6
|
+
|
|
7
|
+
### 🐛 SSH Remote Playing Wrong Music and Voice
|
|
8
|
+
|
|
9
|
+
When using the SSH remote TTS feature, the wrong project's music track and voice were being applied. Root cause: `CLAUDE_PROJECT_DIR` was not forwarded to the sender, causing it to fall back to the global config instead of the active project's `audio-effects.cfg`.
|
|
10
|
+
|
|
11
|
+
### 🐛 Bash Receiver Incompatible with JSON Payload Format
|
|
12
|
+
|
|
13
|
+
The Linux/Termux bash receiver (`agentvibes-receiver.sh`) was using a positional-argument format from pre-v5.5 and could not decode the current base64 JSON payload at all. The receiver has been fully rewritten to match the PowerShell receiver's logic: decodes base64, parses JSON, applies voice/music/effects/volume, and validates all fields.
|
|
14
|
+
|
|
15
|
+
### 🐛 Personality Intro Heard Twice on Remote
|
|
16
|
+
|
|
17
|
+
The personality pretext (e.g., "Bcs latin dance here") was being spoken twice when using SSH remote TTS. Root cause: `play-tts.sh` already prepends the pretext to the speech text before calling the sender; the sender was also packing it into the JSON `pretext` field, causing the receiver to prepend it again. The `pretext` JSON field is now intentionally left empty — the personality is delivered via the `text` field only.
|
|
18
|
+
|
|
19
|
+
### 🆕 SSH Host Alias Visible in Settings Tab
|
|
20
|
+
|
|
21
|
+
The configured SSH remote host alias is now displayed in the Settings and Voices tabs so users can confirm which remote machine TTS is targeting without opening config files.
|
|
22
|
+
|
|
23
|
+
### 🔒 Security Fixes
|
|
24
|
+
|
|
25
|
+
Input validation improvements in the SSH remote sender and receiver.
|
|
26
|
+
|
|
27
|
+
### 🧪 24 New BATS Tests
|
|
28
|
+
|
|
29
|
+
- 15 SSH remote payload tests: verify voice, music track, volume, reverb/effects, pretext handling, LLM identifier, project config precedence, and JSON validity
|
|
30
|
+
- 9 end-to-end round-trip tests: sender builds payload → receiver decodes and applies all fields simultaneously, catching regressions in either end
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🖥️ v5.7.5 — TUI Button Contrast + BMAD Routing Fixes
|
|
35
|
+
|
|
36
|
+
**Released:** 2026-05-13
|
|
37
|
+
|
|
38
|
+
### 🐛 TUI Button Focus: Grey Text Eliminated Across All Terminals
|
|
39
|
+
|
|
40
|
+
Focused and selected buttons in the TUI (voices, music, settings, setup tabs) displayed light grey text on light-blue backgrounds in many terminals. Root cause: `bold: true` combined with a dark foreground triggers terminal "bright mode," rendering the color as grey regardless of shade.
|
|
41
|
+
|
|
42
|
+
**Fix:** All button focus states now use **white text on dark green (`#2e7d32`) background** — the same high-contrast pattern already used by the Agents tab. Explicit `focus`/`blur` handlers were added to setup-tab modal buttons to prevent `attachBtnBlink` from interfering with blessed's passive `style.focus` color application.
|
|
43
|
+
|
|
44
|
+
### 🐛 BMAD Tab Voice Picker ♪ Indicator Not Showing
|
|
45
|
+
|
|
46
|
+
The ♪ preview indicator in the BMAD tab voice list didn't appear during preview. The Agents tab was missing `_refreshVP()` calls that the Settings tab already had. A 2-second minimum display timer now keeps the indicator visible when SSH-remote exits immediately (fire-and-forget mode).
|
|
47
|
+
|
|
48
|
+
### 🐛 Non-Interactive Install: Generic Pretext Instead of Project Name
|
|
49
|
+
|
|
50
|
+
Running `agentvibes install` non-interactively always set the pretext to `"Claude Code here"` regardless of project. The installer now derives a project-aware pretext from `path.basename(process.cwd())` with capitalization (e.g., `"MyProject here"`), with a safe fallback for Docker root paths.
|
|
51
|
+
|
|
52
|
+
### 🐛 Global Pretext Overriding Per-Project Config
|
|
53
|
+
|
|
54
|
+
`seedAllLlmDefaultsSync` was seeding project-level LLM rows with the global pretext string, causing the global `"Claude Code here"` to override per-project `tts-pretext.txt` values. Project-level rows are now seeded with empty pretexts so the per-project file takes precedence.
|
|
55
|
+
|
|
56
|
+
### 🐛 `screen`/`tmux` TERM Variant Caused `plab_norm` Capability Error
|
|
57
|
+
|
|
58
|
+
When `TERM` was set to a `screen-*` or `tmux-*` variant, blessed threw a `plab_norm` terminal capability error on startup. The app now overrides `TERM` to `xterm-256color` before creating the blessed screen when such a variant is detected.
|
|
59
|
+
|
|
60
|
+
### 🐛 BMAD Per-Agent Music/Reverb Not Reaching SSH Receiver
|
|
61
|
+
|
|
62
|
+
`play-tts.sh` was not forwarding `AGENT_PROFILE_FILE` to the SSH remote transport, so per-agent background music and reverb overrides in the BMAD tab were silently ignored for remote audio. The profile file path is now passed as argument 4 to `play-tts-ssh-remote.sh`.
|
|
63
|
+
|
|
64
|
+
### 🐛 Node 18 Compatibility: `import.meta.dirname` Replaced
|
|
65
|
+
|
|
66
|
+
A test file used `import.meta.dirname`, available only in Node 21+. Replaced with the `fileURLToPath(import.meta.url)` pattern so tests run correctly on Node 18 and 20.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
3
70
|
## 🎭 v5.7.0 — BMAD v6.6 Support + Windows Auto-Restart Watcher
|
|
4
71
|
|
|
5
72
|
**Released:** 2026-05-11
|
|
@@ -121,7 +121,7 @@ Now we'll tell Claude Desktop to use AgentVibes.
|
|
|
121
121
|
"mcpServers": {
|
|
122
122
|
"agentvibes": {
|
|
123
123
|
"command": "npx",
|
|
124
|
-
"args": ["-y", "agentvibes@beta", "agentvibes-mcp-server"]
|
|
124
|
+
"args": ["-y", "-p", "agentvibes@beta", "agentvibes-mcp-server"]
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
}
|
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.
|
|
4
|
+
"version": "5.7.6",
|
|
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": [
|
package/src/console/app.js
CHANGED
|
@@ -138,7 +138,14 @@ export class AgentVibesConsole {
|
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// blessed's terminfo parser chokes on screen-256color's plab_norm capability.
|
|
142
|
+
// xterm-256color is functionally identical for our TUI and parses cleanly.
|
|
143
|
+
const _origTerm = process.env.TERM;
|
|
144
|
+
if (/^(screen|tmux)(-256color)?$/.test(process.env.TERM || '')) {
|
|
145
|
+
process.env.TERM = 'xterm-256color';
|
|
146
|
+
}
|
|
141
147
|
this.screen = blessed.screen(this._screenOptions);
|
|
148
|
+
if (_origTerm !== undefined) process.env.TERM = _origTerm;
|
|
142
149
|
|
|
143
150
|
// Reflow on terminal resize
|
|
144
151
|
this.screen.on('resize', () => this.screen.render());
|
package/src/console/audio-env.js
CHANGED
|
@@ -163,3 +163,22 @@ export function detectWavPlayer(env) {
|
|
|
163
163
|
}
|
|
164
164
|
return _detect(WAV_PLAYERS, env);
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns all installed WAV players in preference order.
|
|
169
|
+
* Used for fallback playback — try each until one succeeds.
|
|
170
|
+
* On Windows, always appends the PowerShell fallback.
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} [env] - Environment (defaults to buildAudioEnv())
|
|
173
|
+
* @returns {Player[]}
|
|
174
|
+
*/
|
|
175
|
+
export function getAllWavPlayers(env) {
|
|
176
|
+
env = env || buildAudioEnv();
|
|
177
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
178
|
+
const installed = WAV_PLAYERS.filter(p => {
|
|
179
|
+
const r = spawnSync(whichCmd, [p.bin], { stdio: 'pipe', env });
|
|
180
|
+
return r.status === 0;
|
|
181
|
+
});
|
|
182
|
+
if (process.platform === 'win32') installed.push(WIN_WAV_PLAYER);
|
|
183
|
+
return installed;
|
|
184
|
+
}
|
|
@@ -367,7 +367,7 @@ ${_tl('bmadDesc')}
|
|
|
367
367
|
padding: { left: 1, right: 1 },
|
|
368
368
|
style: {
|
|
369
369
|
bg: COLORS.btnDefault,
|
|
370
|
-
fg: 'white',
|
|
370
|
+
fg: 'bright-white',
|
|
371
371
|
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
372
372
|
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
373
373
|
},
|
|
@@ -711,7 +711,7 @@ ${_tl('bmadDesc')}
|
|
|
711
711
|
padding: { left: 1, right: 1 },
|
|
712
712
|
style: {
|
|
713
713
|
bg: COLORS.btnDefault,
|
|
714
|
-
fg: 'white',
|
|
714
|
+
fg: 'bright-white',
|
|
715
715
|
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
716
716
|
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
717
717
|
},
|
|
@@ -1139,8 +1139,6 @@ ${_tl('bmadDesc')}
|
|
|
1139
1139
|
const inputBox = blessed.textbox({
|
|
1140
1140
|
parent: editModal, top: 3, left: 2, right: 2, height: 3,
|
|
1141
1141
|
border: { type: 'line' },
|
|
1142
|
-
inputOnFocus: true,
|
|
1143
|
-
value: draft.pretext,
|
|
1144
1142
|
style: {
|
|
1145
1143
|
fg: COLORS.valueFg, bg: '#0d1b35',
|
|
1146
1144
|
border: { fg: COLORS.borderFg },
|
|
@@ -1149,22 +1147,78 @@ ${_tl('bmadDesc')}
|
|
|
1149
1147
|
});
|
|
1150
1148
|
|
|
1151
1149
|
let _editClosed = false;
|
|
1150
|
+
let _cursor = draft.pretext.length;
|
|
1151
|
+
inputBox.value = draft.pretext;
|
|
1152
|
+
|
|
1153
|
+
function _renderPretext() {
|
|
1154
|
+
const val = inputBox.value;
|
|
1155
|
+
const lpos = inputBox._getCoords();
|
|
1156
|
+
if (!lpos) { screen.render(); return; }
|
|
1157
|
+
const contentWidth = Math.max(1, (lpos.xl - lpos.xi) - inputBox.iwidth);
|
|
1158
|
+
const start = _cursor > contentWidth - 1 ? _cursor - contentWidth + 1 : 0;
|
|
1159
|
+
inputBox.setContent(val.slice(start));
|
|
1160
|
+
screen.render();
|
|
1161
|
+
screen.program.cup(lpos.yi + inputBox.itop, lpos.xi + inputBox.ileft + (_cursor - start));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const _prevGrabKeys = screen.grabKeys;
|
|
1152
1165
|
function _closeEdit(save) {
|
|
1153
1166
|
if (_editClosed) return;
|
|
1154
1167
|
_editClosed = true;
|
|
1168
|
+
inputBox.removeAllListeners('keypress');
|
|
1169
|
+
screen.grabKeys = _prevGrabKeys;
|
|
1170
|
+
screen.program.hideCursor();
|
|
1155
1171
|
if (save) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
draft.pretext = (raw || draft.pretext).slice(0, MAX_PRETEXT_LENGTH);
|
|
1172
|
+
// M7: enforce max pretext length; allow clearing to empty string
|
|
1173
|
+
draft.pretext = inputBox.value.trim().slice(0, MAX_PRETEXT_LENGTH);
|
|
1159
1174
|
}
|
|
1160
1175
|
destroyList(editModal, screen, onDone);
|
|
1161
1176
|
}
|
|
1162
1177
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1178
|
+
// Guard: if editModal is destroyed externally (parent closed, signal, etc.)
|
|
1179
|
+
// without _closeEdit being called, restore grab state so TUI stays responsive.
|
|
1180
|
+
editModal.once('destroy', () => {
|
|
1181
|
+
if (!_editClosed) {
|
|
1182
|
+
_editClosed = true;
|
|
1183
|
+
inputBox.removeAllListeners('keypress');
|
|
1184
|
+
screen.grabKeys = _prevGrabKeys;
|
|
1185
|
+
screen.program.hideCursor();
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1165
1188
|
|
|
1189
|
+
screen.grabKeys = true;
|
|
1166
1190
|
inputBox.focus();
|
|
1167
|
-
screen.render();
|
|
1191
|
+
screen.render(); // Layout pass so _getCoords() returns valid coords on first _renderPretext
|
|
1192
|
+
screen.program.showCursor();
|
|
1193
|
+
_renderPretext();
|
|
1194
|
+
|
|
1195
|
+
inputBox.on('keypress', function(ch, key) {
|
|
1196
|
+
if (_editClosed) return;
|
|
1197
|
+
const val = inputBox.value;
|
|
1198
|
+
if (key.name === 'enter') { _closeEdit(true); return; }
|
|
1199
|
+
if (key.name === 'escape') { _closeEdit(false); return; }
|
|
1200
|
+
if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
|
|
1201
|
+
_cursor = 0;
|
|
1202
|
+
} else if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
|
|
1203
|
+
_cursor = val.length;
|
|
1204
|
+
} else if (key.name === 'left') {
|
|
1205
|
+
if (_cursor > 0) _cursor--; else return;
|
|
1206
|
+
} else if (key.name === 'right') {
|
|
1207
|
+
if (_cursor < val.length) _cursor++; else return;
|
|
1208
|
+
} else if (key.name === 'backspace') {
|
|
1209
|
+
if (_cursor > 0) { inputBox.value = val.slice(0, _cursor - 1) + val.slice(_cursor); _cursor--; }
|
|
1210
|
+
else return;
|
|
1211
|
+
} else if (key.name === 'delete') {
|
|
1212
|
+
if (_cursor < val.length) { inputBox.value = val.slice(0, _cursor) + val.slice(_cursor + 1); }
|
|
1213
|
+
else return;
|
|
1214
|
+
} else if (ch && !key.ctrl && !key.meta && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
|
|
1215
|
+
inputBox.value = val.slice(0, _cursor) + ch + val.slice(_cursor);
|
|
1216
|
+
_cursor++;
|
|
1217
|
+
} else {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
_renderPretext();
|
|
1221
|
+
});
|
|
1168
1222
|
}
|
|
1169
1223
|
|
|
1170
1224
|
// -------------------------------------------------------------------------
|
|
@@ -409,7 +409,7 @@ export function createMusicTab(screen, services) {
|
|
|
409
409
|
padding: { left: 1, right: 1 },
|
|
410
410
|
style: {
|
|
411
411
|
bg: COLORS.btnDefault,
|
|
412
|
-
fg: 'white',
|
|
412
|
+
fg: 'bright-white',
|
|
413
413
|
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
414
414
|
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
415
415
|
},
|
|
@@ -423,7 +423,7 @@ export function createMusicTab(screen, services) {
|
|
|
423
423
|
});
|
|
424
424
|
btn.on('blur', () => {
|
|
425
425
|
btn.style.bg = COLORS.btnDefault;
|
|
426
|
-
btn.style.fg = 'white';
|
|
426
|
+
btn.style.fg = 'bright-white';
|
|
427
427
|
const raw = btn.content.replace(/[►◄]/g, '').trim();
|
|
428
428
|
btn.setContent(raw);
|
|
429
429
|
screen.render();
|
|
@@ -776,7 +776,7 @@ export function createMusicTab(screen, services) {
|
|
|
776
776
|
padding: { left: 1, right: 1 },
|
|
777
777
|
style: {
|
|
778
778
|
bg,
|
|
779
|
-
fg: 'white',
|
|
779
|
+
fg: 'bright-white',
|
|
780
780
|
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
781
781
|
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
782
782
|
},
|
|
@@ -819,7 +819,7 @@ export function createMusicTab(screen, services) {
|
|
|
819
819
|
padding: { left: 1, right: 1 },
|
|
820
820
|
style: {
|
|
821
821
|
bg: '#e65100',
|
|
822
|
-
fg: 'white',
|
|
822
|
+
fg: 'bright-white',
|
|
823
823
|
focus: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
824
824
|
hover: { bg: COLORS.btnFocus, fg: COLORS.btnFocusFg, bold: true },
|
|
825
825
|
},
|
|
@@ -201,6 +201,15 @@ export function createSettingsTab(screen, services) {
|
|
|
201
201
|
},
|
|
202
202
|
desc: 'Play audio locally or stream to a remote host via SSH',
|
|
203
203
|
},
|
|
204
|
+
{
|
|
205
|
+
key: 'sshAlias',
|
|
206
|
+
label: 'SSH Host Alias',
|
|
207
|
+
getValue: () => {
|
|
208
|
+
const alias = configService?.getConfig?.()?.audio_ssh_alias ?? '';
|
|
209
|
+
return alias || '(not set)';
|
|
210
|
+
},
|
|
211
|
+
desc: 'SSH host alias from ~/.ssh/config used when Audio Destination is Remote',
|
|
212
|
+
},
|
|
204
213
|
{
|
|
205
214
|
key: 'configStorage',
|
|
206
215
|
label: 'Config Storage',
|
|
@@ -395,6 +404,9 @@ export function createSettingsTab(screen, services) {
|
|
|
395
404
|
case 'audioDst':
|
|
396
405
|
_editAudioDst();
|
|
397
406
|
break;
|
|
407
|
+
case 'sshAlias':
|
|
408
|
+
_editSshAlias();
|
|
409
|
+
break;
|
|
398
410
|
case 'rerunWizard':
|
|
399
411
|
_rerunWizard();
|
|
400
412
|
break;
|
|
@@ -583,7 +595,7 @@ export function createSettingsTab(screen, services) {
|
|
|
583
595
|
style: {
|
|
584
596
|
fg: COLORS.labelFg, bg: COLORS.contentBg,
|
|
585
597
|
border: { fg: 'blue' },
|
|
586
|
-
selected: { bg: 'green', fg: '
|
|
598
|
+
selected: { bg: 'green', fg: 'black', bold: true },
|
|
587
599
|
item: { fg: COLORS.labelFg },
|
|
588
600
|
},
|
|
589
601
|
});
|
|
@@ -864,6 +876,68 @@ export function createSettingsTab(screen, services) {
|
|
|
864
876
|
screen.render();
|
|
865
877
|
}
|
|
866
878
|
|
|
879
|
+
// ── SSH alias editor ─────────────────────────────────────────────────────
|
|
880
|
+
|
|
881
|
+
function _editSshAlias() {
|
|
882
|
+
navigationService?.openModal();
|
|
883
|
+
|
|
884
|
+
const aliases = _detectSshAliases().filter(a => !a.includes('github.com'));
|
|
885
|
+
const MANUAL_OPTION = ' Type manually...';
|
|
886
|
+
const items = [...aliases.map(a => ` ${a}`), MANUAL_OPTION];
|
|
887
|
+
const currentAlias = configService?.getConfig?.()?.audio_ssh_alias ?? '';
|
|
888
|
+
|
|
889
|
+
const listHeight = Math.min(items.length + 4, 18);
|
|
890
|
+
const modal = blessed.list({
|
|
891
|
+
parent: screen,
|
|
892
|
+
top: 'center',
|
|
893
|
+
left: 'center',
|
|
894
|
+
width: 44,
|
|
895
|
+
height: listHeight,
|
|
896
|
+
border: { type: 'line' },
|
|
897
|
+
tags: true,
|
|
898
|
+
label: ' {bold}{cyan-fg} Select SSH Host Alias {/cyan-fg}{/bold} ',
|
|
899
|
+
keys: true,
|
|
900
|
+
vi: true,
|
|
901
|
+
mouse: true,
|
|
902
|
+
style: {
|
|
903
|
+
fg: COLORS.labelFg,
|
|
904
|
+
bg: COLORS.contentBg,
|
|
905
|
+
border: { fg: 'cyan' },
|
|
906
|
+
selected: { bg: 'blue', fg: 'yellow' },
|
|
907
|
+
item: { fg: COLORS.labelFg },
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
modal.setFront();
|
|
911
|
+
modal.setItems(items);
|
|
912
|
+
|
|
913
|
+
const currentIdx = aliases.indexOf(currentAlias);
|
|
914
|
+
if (currentIdx >= 0) modal.select(currentIdx);
|
|
915
|
+
|
|
916
|
+
modal.key(['enter'], () => {
|
|
917
|
+
const selItem = items[modal.selected];
|
|
918
|
+
if (selItem === MANUAL_OPTION) {
|
|
919
|
+
_closeModal();
|
|
920
|
+
_promptSshAlias();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const alias = selItem?.trim();
|
|
924
|
+
if (alias) configService.set('audio_ssh_alias', alias);
|
|
925
|
+
_closeModal();
|
|
926
|
+
});
|
|
927
|
+
modal.key(['escape', 'q'], _closeModal);
|
|
928
|
+
|
|
929
|
+
function _closeModal() {
|
|
930
|
+
navigationService?.closeModal();
|
|
931
|
+
destroyList(modal, screen);
|
|
932
|
+
_refreshValues();
|
|
933
|
+
box.focus();
|
|
934
|
+
screen.render();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
modal.focus();
|
|
938
|
+
screen.render();
|
|
939
|
+
}
|
|
940
|
+
|
|
867
941
|
// ── Re-run wizard ────────────────────────────────────────────────────────
|
|
868
942
|
|
|
869
943
|
function _rerunWizard() {
|