agentvibes 5.7.5 → 5.7.7
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/audio/tracks/CelestialVelvet.mp3 +0 -0
- 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/bmad-party-speak.sh +27 -6
- package/.claude/hooks/play-tts-ssh-remote.sh +46 -17
- package/README.md +19 -9
- package/RELEASE_NOTES.md +61 -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/audio-env.js +19 -0
- package/src/console/tabs/agents-tab.js +1 -1
- package/src/console/tabs/music-tab.js +2 -0
- package/src/console/tabs/settings-tab.js +74 -0
- package/src/console/tabs/voices-tab.js +47 -29
- package/templates/agentvibes-receiver.sh +139 -66
|
@@ -19,10 +19,17 @@ set -euo pipefail
|
|
|
19
19
|
|
|
20
20
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
21
|
LOCK_FILE="/tmp/agentvibes-party-queue.lock"
|
|
22
|
+
DEBUG_LOG="/tmp/agentvibes-party-debug.log"
|
|
23
|
+
|
|
24
|
+
_dbg() { printf '[%s] %s\n' "$(date -Iseconds)" "$*" >> "$DEBUG_LOG" 2>/dev/null || true; }
|
|
22
25
|
|
|
23
26
|
# --- Read stdin ---
|
|
24
27
|
raw="$(cat)"
|
|
25
|
-
[[ -z "$raw" ]]
|
|
28
|
+
if [[ -z "$raw" ]]; then
|
|
29
|
+
_dbg "exit: empty stdin"
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
_dbg "fired (stdin ${#raw} bytes)"
|
|
26
33
|
|
|
27
34
|
# --- Parse all needed fields in one python3 call (fixes M5: 3x subprocess, echo safety) ---
|
|
28
35
|
# Outputs: TOOL_NAME|DISPLAY_NAME|RESPONSE_TEXT (newlines in response encoded as \n literals)
|
|
@@ -73,13 +80,26 @@ response_text="${rest#*|}"
|
|
|
73
80
|
response_text="${response_text//\\n/ }"
|
|
74
81
|
|
|
75
82
|
# --- Only handle Agent tool ---
|
|
76
|
-
[[ "$tool_name" != "Agent" ]]
|
|
83
|
+
if [[ "$tool_name" != "Agent" ]]; then
|
|
84
|
+
_dbg "skip: tool_name='$tool_name' (not Agent)"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
77
87
|
|
|
78
88
|
# --- Fingerprint: only fire for BMAD party mode agents (safe string match, no pipe) ---
|
|
79
|
-
[[ "$raw"
|
|
89
|
+
if [[ "$raw" != *"BMAD agent in a collaborative roundtable"* ]]; then
|
|
90
|
+
_dbg "skip: fingerprint MISS (Agent call but prompt lacks 'BMAD agent in a collaborative roundtable')"
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
_dbg "fingerprint HIT: display='$display_name' text_len=${#response_text}"
|
|
80
94
|
|
|
81
|
-
[[ -z "$display_name" ]]
|
|
82
|
-
|
|
95
|
+
if [[ -z "$display_name" ]]; then
|
|
96
|
+
_dbg "skip: empty display_name"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
if [[ -z "$response_text" ]]; then
|
|
100
|
+
_dbg "skip: empty response_text"
|
|
101
|
+
exit 0
|
|
102
|
+
fi
|
|
83
103
|
|
|
84
104
|
# --- Resolve project root ---
|
|
85
105
|
project_root="${CLAUDE_PROJECT_DIR:-}"
|
|
@@ -154,7 +174,8 @@ esac
|
|
|
154
174
|
exec 9>"$LOCK_FILE"
|
|
155
175
|
if command -v flock &>/dev/null; then
|
|
156
176
|
flock -w 60 9
|
|
157
|
-
"$bmad_speak
|
|
177
|
+
_dbg "invoking: $bmad_speak '$agent_id' (text_len=${#response_text})"
|
|
178
|
+
"$bmad_speak" "$agent_id" "$response_text" || _dbg "bmad-speak returned non-zero"
|
|
158
179
|
flock -u 9
|
|
159
180
|
else
|
|
160
181
|
# macOS fallback: atomic mkdir polling lock
|
|
@@ -151,12 +151,23 @@ SOX_EFFECTS=""
|
|
|
151
151
|
BG_FILE=""
|
|
152
152
|
BG_VOLUME="0.10"
|
|
153
153
|
|
|
154
|
-
|
|
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
|
|
155
161
|
if [[ -f "$EFFECTS_CFG" ]]; then
|
|
156
|
-
|
|
157
|
-
|
|
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)
|
|
158
166
|
if [[ -n "$CONFIG_LINE" ]]; then
|
|
159
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%% }"
|
|
160
171
|
fi
|
|
161
172
|
fi
|
|
162
173
|
|
|
@@ -172,14 +183,18 @@ LLM_REVERB=""
|
|
|
172
183
|
LLM_BG_FILE=""
|
|
173
184
|
LLM_BG_VOLUME=""
|
|
174
185
|
|
|
175
|
-
# Build config search path
|
|
176
|
-
#
|
|
177
|
-
#
|
|
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.
|
|
178
191
|
_llm_cfg_paths=()
|
|
179
192
|
if [[ -n "${CLAUDE_PROJECT_DIR:-}" && "$CLAUDE_PROJECT_DIR" != "$PROJECT_ROOT" ]]; then
|
|
180
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")
|
|
181
196
|
fi
|
|
182
|
-
_llm_cfg_paths+=("$
|
|
197
|
+
_llm_cfg_paths+=("$HOME/.claude/config/audio-effects.cfg")
|
|
183
198
|
|
|
184
199
|
_llm_key="llm:${LLM_NAME}"
|
|
185
200
|
_llm_row_found=0
|
|
@@ -224,7 +239,11 @@ if [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
|
|
|
224
239
|
fi
|
|
225
240
|
fi
|
|
226
241
|
|
|
227
|
-
#
|
|
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.
|
|
228
247
|
PRETEXT=""
|
|
229
248
|
PRETEXT_FILE="$PROJECT_ROOT/.agentvibes/config/pretext.txt"
|
|
230
249
|
if [[ -f "$PRETEXT_FILE" ]]; then
|
|
@@ -279,9 +298,9 @@ build_json_payload() {
|
|
|
279
298
|
--arg llm "$LLM_NAME" \
|
|
280
299
|
'{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, project: $project, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm}'
|
|
281
300
|
else
|
|
282
|
-
# Manual JSON — escape
|
|
301
|
+
# Manual JSON — escape backslashes, quotes, control chars
|
|
283
302
|
local escaped_text
|
|
284
|
-
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')
|
|
285
304
|
local escaped_pretext
|
|
286
305
|
escaped_pretext=$(printf '%s' "$PRETEXT" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
287
306
|
printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","project":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s"}' \
|
|
@@ -303,6 +322,13 @@ fi
|
|
|
303
322
|
# Send to receiver via SSH (fire and forget — backgrounded)
|
|
304
323
|
# ---------------------------------------------------------------------------
|
|
305
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
|
+
|
|
306
332
|
echo "Sending to $SSH_HOST..." >&2
|
|
307
333
|
|
|
308
334
|
# Build SSH args — use explicit key/port from config if available, else rely on ~/.ssh/config
|
|
@@ -310,15 +336,18 @@ SSH_ARGS=()
|
|
|
310
336
|
[[ -n "$SSH_KEY" && -f "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
311
337
|
[[ -n "$SSH_PORT" ]] && SSH_ARGS+=(-p "$SSH_PORT")
|
|
312
338
|
|
|
313
|
-
# ForceCommand receiver: SSH_ORIGINAL_COMMAND passes the payload directly
|
|
314
|
-
ssh
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
339
|
+
# ForceCommand receiver: SSH_ORIGINAL_COMMAND passes the payload directly.
|
|
340
|
+
# Run ssh inside the backgrounded subshell so its exit code is reachable via $?
|
|
341
|
+
# (a `wait` from outside the spawning shell would error: "pid X is not a child").
|
|
342
|
+
(
|
|
343
|
+
ssh "${SSH_ARGS[@]}" "$SSH_HOST" "$ENCODED_PAYLOAD"
|
|
344
|
+
_exit=$?
|
|
345
|
+
if [[ $_exit -ne 0 ]]; then
|
|
319
346
|
echo "$(date -Iseconds) [ERROR] SSH to $SSH_HOST failed (exit $_exit)" \
|
|
320
347
|
>> "$HOME/.agentvibes/ssh-remote.log" 2>/dev/null || true
|
|
321
|
-
fi
|
|
348
|
+
fi
|
|
349
|
+
) &
|
|
350
|
+
SSH_PID=$!
|
|
322
351
|
|
|
323
352
|
echo "Sent to $SSH_HOST (PID: $SSH_PID)" >&2
|
|
324
353
|
exit 0
|
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.7
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -40,19 +40,29 @@ 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.7 — Party Mode Voice Restore + Polish
|
|
44
44
|
|
|
45
|
-
**
|
|
45
|
+
**Party mode agents now speak again:** BMAD `/party-mode` now reliably invokes the correct AgentVibes skill, and each agent's response is spoken aloud in their unique voice with per-agent music, pretext, and reverb — loaded automatically from `~/.agentvibes/bmad-voice-map.json`.
|
|
46
46
|
|
|
47
|
-
**
|
|
47
|
+
**New bundled track:** 🌌 CelestialVelvet added to the built-in music catalog.
|
|
48
48
|
|
|
49
|
-
**
|
|
49
|
+
**TUI contrast fix:** Selected rows in Voices and Agents tabs no longer render unreadable gray text.
|
|
50
50
|
|
|
51
|
-
**
|
|
51
|
+
**SSH remote:** Fixed "wait: pid is not a child of this shell" error in `play-tts-ssh-remote.sh`.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
## v5.7.6 — SSH Remote Payload Integrity + Receiver Rewrite
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
**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.
|
|
56
|
+
|
|
57
|
+
**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.
|
|
58
|
+
|
|
59
|
+
**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.
|
|
60
|
+
|
|
61
|
+
**SSH host visible in TUI:** The Settings and Voices tabs now display your configured SSH remote host alias.
|
|
62
|
+
|
|
63
|
+
**Security fixes** and 24 new BATS tests covering the full sender → receiver round-trip.
|
|
64
|
+
|
|
65
|
+
## v5.7.5 — TUI Button Contrast + BMAD Routing Fixes
|
|
56
66
|
|
|
57
67
|
**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.
|
|
58
68
|
|
|
@@ -353,7 +363,7 @@ All 50+ Piper voices AgentVibes provides are sourced from Hugging Face's open-so
|
|
|
353
363
|
|
|
354
364
|
## 📰 Latest Release
|
|
355
365
|
|
|
356
|
-
**[v3.6.0 - "Voice Explorer" Release](https://github.com/paulpreibisch/AgentVibes/releases/tag/
|
|
366
|
+
**[v3.6.0 - "Voice Explorer" Release](https://github.com/paulpreibisch/AgentVibes/releases/tag/v5.7.5)** 🎉
|
|
357
367
|
|
|
358
368
|
### 🎤 Voices Tab — Browse & Sample 914 Voices
|
|
359
369
|
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🎭 v5.7.7 — Party Mode Voice Restore + Polish
|
|
4
|
+
|
|
5
|
+
**Released:** 2026-05-17
|
|
6
|
+
|
|
7
|
+
### 🐛 BMAD Party Mode Agents Silent (No Per-Agent TTS)
|
|
8
|
+
|
|
9
|
+
Party mode agents were displaying responses in text but not speaking with their unique voices. Two root causes:
|
|
10
|
+
|
|
11
|
+
**Skill disambiguation:** `/party-mode` was matching the upstream BMAD `_bmad/core/workflows/party-mode` command (which tries to load a path that doesn't exist in this project) instead of the AgentVibes skill. A project-local `/party-mode` command override now routes to the correct skill.
|
|
12
|
+
|
|
13
|
+
**Mandatory TTS step:** The orchestrator's `bmad-speak.js` call step was underspecified, so it was sometimes skipped. Step 4 in the BMAD party mode skill is now clearly marked MANDATORY, with explicit documentation of what `bmad-speak.js` applies per agent: voice, pretext, reverb, personality, and background music — all loaded automatically from `~/.agentvibes/bmad-voice-map.json`.
|
|
14
|
+
|
|
15
|
+
### 🔍 Party Mode Debug Logging
|
|
16
|
+
|
|
17
|
+
`bmad-party-speak.sh` (PostToolUse hook) now writes structured diagnostic entries to `/tmp/agentvibes-party-debug.log` — `fired`, `fingerprint HIT/MISS`, `invoking`, and errors — so voice issues are diagnosable without guessing.
|
|
18
|
+
|
|
19
|
+
### 🎵 New Bundled Track: CelestialVelvet
|
|
20
|
+
|
|
21
|
+
A new ambient music track **CelestialVelvet** (🌌) has been added to the built-in catalog. Available immediately in the TUI music picker and BMAD voice map — no download required.
|
|
22
|
+
|
|
23
|
+
### 🐛 TUI: Gray Text on Selected Rows Fixed
|
|
24
|
+
|
|
25
|
+
White text now renders correctly on selected rows in the Voices and Agents tabs. Previously `bright-black` foreground combined with green background produced unreadable gray text in many terminals.
|
|
26
|
+
|
|
27
|
+
### 🐛 SSH Remote: "wait: pid is not a child of this shell" Error
|
|
28
|
+
|
|
29
|
+
`play-tts-ssh-remote.sh` would emit `wait: pid X is not a child of this shell` on certain shells. Fixed by spawning `ssh` directly inside the background subshell so `$?` captures the exit code without a cross-shell `wait` call.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🔧 v5.7.6 — SSH Remote Payload Integrity + Receiver Rewrite
|
|
34
|
+
|
|
35
|
+
**Released:** 2026-05-16
|
|
36
|
+
|
|
37
|
+
### 🐛 SSH Remote Playing Wrong Music and Voice
|
|
38
|
+
|
|
39
|
+
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`.
|
|
40
|
+
|
|
41
|
+
### 🐛 Bash Receiver Incompatible with JSON Payload Format
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
### 🐛 Personality Intro Heard Twice on Remote
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
### 🆕 SSH Host Alias Visible in Settings Tab
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
### 🔒 Security Fixes
|
|
54
|
+
|
|
55
|
+
Input validation improvements in the SSH remote sender and receiver.
|
|
56
|
+
|
|
57
|
+
### 🧪 24 New BATS Tests
|
|
58
|
+
|
|
59
|
+
- 15 SSH remote payload tests: verify voice, music track, volume, reverb/effects, pretext handling, LLM identifier, project config precedence, and JSON validity
|
|
60
|
+
- 9 end-to-end round-trip tests: sender builds payload → receiver decodes and applies all fields simultaneously, catching regressions in either end
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
3
64
|
## 🖥️ v5.7.5 — TUI Button Contrast + BMAD Routing Fixes
|
|
4
65
|
|
|
5
66
|
**Released:** 2026-05-13
|
|
@@ -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.7",
|
|
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/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
|
+
}
|
|
@@ -153,7 +153,7 @@ const COL_MUSIC = 11;
|
|
|
153
153
|
const COL_VOL = 5; // e.g. "70%" or "100%"
|
|
154
154
|
|
|
155
155
|
// Inline hint appended to the selected row when list is focused
|
|
156
|
-
const _ROW_HINT_BMAD = ` {
|
|
156
|
+
const _ROW_HINT_BMAD = ` {white-fg}[Space] Preview [Enter] Configure{/white-fg}`;
|
|
157
157
|
|
|
158
158
|
// ---------------------------------------------------------------------------
|
|
159
159
|
|
|
@@ -64,6 +64,7 @@ const TRACK_DISPLAY = Object.freeze({
|
|
|
64
64
|
'agent_vibes_arabic_v2_loop.mp3': '🎵 Arabic Oud',
|
|
65
65
|
'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
|
|
66
66
|
'agent_vibes_bossa_nova_v2_loop.mp3': '🌸 Bossa Nova',
|
|
67
|
+
'CelestialVelvet.mp3': '🌌 Celestial Velvet',
|
|
67
68
|
'agent_vibes_celtic_harp_v1_loop.mp3': '🎶 Celtic Harp',
|
|
68
69
|
'agent_vibes_chillwave_v2_loop.mp3': '🌊 Chillwave',
|
|
69
70
|
'agent_vibes_cumbia_v1_loop.mp3': '🎸 Cumbia',
|
|
@@ -85,6 +86,7 @@ const BUILT_IN_TRACK_CATALOG = Object.freeze([
|
|
|
85
86
|
{ id: 'agent_vibes_arabic_v2_loop.mp3', label: '🎵 Arabic Oud' },
|
|
86
87
|
{ id: 'agent_vibes_bachata_v1_loop.mp3', label: '🎺 Bachata' },
|
|
87
88
|
{ id: 'agent_vibes_bossa_nova_v2_loop.mp3', label: '🌸 Bossa Nova' },
|
|
89
|
+
{ id: 'CelestialVelvet.mp3', label: '🌌 Celestial Velvet' },
|
|
88
90
|
{ id: 'agent_vibes_celtic_harp_v1_loop.mp3', label: '🎶 Celtic Harp' },
|
|
89
91
|
{ id: 'agent_vibes_chillwave_v2_loop.mp3', label: '🌊 Chillwave' },
|
|
90
92
|
{ id: 'agent_vibes_cumbia_v1_loop.mp3', label: '🎸 Cumbia' },
|
|
@@ -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;
|
|
@@ -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() {
|
|
@@ -13,7 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import { spawn } from 'node:child_process';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
16
|
+
import { buildAudioEnv, detectWavPlayer, getAllWavPlayers } from '../audio-env.js';
|
|
17
17
|
import { SURNAME_POOL, uniquifyVoiceName } from '../../utils/voice-names.js';
|
|
18
18
|
|
|
19
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -770,7 +770,7 @@ export function createVoicesTab(screen, services) {
|
|
|
770
770
|
// Inline selection hint appended to the currently highlighted voice row.
|
|
771
771
|
// _hintBase stores the item's clean content (no hint, no █) — no sentinel needed.
|
|
772
772
|
// Use getter functions so hints re-translate when language changes.
|
|
773
|
-
const _rowHintInstalled = () => ` {
|
|
773
|
+
const _rowHintInstalled = () => ` {white-fg}${_tl('voicesRowHintInstalled')}{/white-fg}`;
|
|
774
774
|
const _rowHintUninstalled = () => ` {bright-yellow-fg}${_tl('voicesRowHintUninstalled')}{/bright-yellow-fg}`;
|
|
775
775
|
let _hintIdx = -1;
|
|
776
776
|
let _hintBase = ''; // content of items[_hintIdx] before hint was appended
|
|
@@ -1022,9 +1022,9 @@ export function createVoicesTab(screen, services) {
|
|
|
1022
1022
|
return;
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
-
// Play the synthesized wav
|
|
1026
|
-
const
|
|
1027
|
-
if (
|
|
1025
|
+
// Play the synthesized wav — try each installed player until one succeeds
|
|
1026
|
+
const _wavPlayers = getAllWavPlayers(_spawnEnv);
|
|
1027
|
+
if (_wavPlayers.length === 0) {
|
|
1028
1028
|
_playingVoiceId = null;
|
|
1029
1029
|
_playingProcess = null;
|
|
1030
1030
|
previewLine.setContent(`{red-fg}No audio player found. Install ffmpeg.{/red-fg}`);
|
|
@@ -1033,44 +1033,62 @@ export function createVoicesTab(screen, services) {
|
|
|
1033
1033
|
try { fs.unlinkSync(tempWav); } catch {}
|
|
1034
1034
|
return;
|
|
1035
1035
|
}
|
|
1036
|
-
|
|
1037
|
-
stdio: 'ignore',
|
|
1038
|
-
detached: !isWindows,
|
|
1039
|
-
windowsHide: true,
|
|
1040
|
-
env: _spawnEnv,
|
|
1041
|
-
});
|
|
1036
|
+
|
|
1042
1037
|
// Race note: _playingVoiceId could change between piper exit and here
|
|
1043
1038
|
// if the user stops playback. Re-check before assigning to avoid orphan.
|
|
1044
1039
|
if (_playingVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
1045
|
-
_playingProcess = playProc;
|
|
1046
1040
|
|
|
1047
1041
|
previewLine.setContent(`{${COLORS.activeFg}-fg}♪ Playing: ${voiceId} (Enter/Space to stop){/${COLORS.activeFg}-fg}`);
|
|
1048
1042
|
screen.render();
|
|
1049
1043
|
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1044
|
+
function _tryNextPlayer(remainingPlayers) {
|
|
1045
|
+
if (!remainingPlayers.length) {
|
|
1052
1046
|
_playingVoiceId = null;
|
|
1053
1047
|
_playingProcess = null;
|
|
1048
|
+
previewLine.setContent(`{red-fg}♪ Audio playback failed (no audio device?) — check your provider in Setup{/red-fg}`);
|
|
1049
|
+
screen.render();
|
|
1050
|
+
setTimeout(() => { if (!_closed) { previewLine.setContent(_listFocused ? HINT_TEXT : ''); screen.render(); } }, 5000);
|
|
1051
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const [wavP, ...rest] = remainingPlayers;
|
|
1055
|
+
const playProc = spawn(wavP.bin, wavP.args(tempWav), {
|
|
1056
|
+
stdio: 'ignore',
|
|
1057
|
+
detached: !isWindows,
|
|
1058
|
+
windowsHide: true,
|
|
1059
|
+
env: _spawnEnv,
|
|
1060
|
+
});
|
|
1061
|
+
if (_playingVoiceId !== voiceId) {
|
|
1062
|
+
try { playProc.kill(); } catch {}
|
|
1063
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
_playingProcess = playProc;
|
|
1067
|
+
|
|
1068
|
+
playProc.on('exit', (code) => {
|
|
1069
|
+
if (_playingVoiceId !== voiceId) {
|
|
1070
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1054
1073
|
if (code !== 0) {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
setTimeout(() => { if (!_closed) { previewLine.setContent(_listFocused ? HINT_TEXT : ''); screen.render(); } }, 5000);
|
|
1074
|
+
// This player failed — try the next one
|
|
1075
|
+
_tryNextPlayer(rest);
|
|
1058
1076
|
} else {
|
|
1077
|
+
_playingVoiceId = null;
|
|
1078
|
+
_playingProcess = null;
|
|
1059
1079
|
previewLine.setContent(_listFocused ? HINT_TEXT : '');
|
|
1060
|
-
refreshDisplay();
|
|
1080
|
+
refreshDisplay();
|
|
1081
|
+
try { fs.unlinkSync(tempWav); } catch {}
|
|
1061
1082
|
}
|
|
1062
|
-
}
|
|
1063
|
-
try { fs.unlinkSync(tempWav); } catch {}
|
|
1064
|
-
});
|
|
1083
|
+
});
|
|
1065
1084
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
});
|
|
1085
|
+
playProc.on('error', () => {
|
|
1086
|
+
if (_playingVoiceId !== voiceId) { try { fs.unlinkSync(tempWav); } catch {} return; }
|
|
1087
|
+
_tryNextPlayer(rest);
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
_tryNextPlayer(_wavPlayers);
|
|
1074
1092
|
});
|
|
1075
1093
|
|
|
1076
1094
|
piper.on('error', () => {
|