agentvibes 5.13.1 โ 5.15.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/.claude/audio/ui/CREDITS.txt +16 -16
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/kokoro-server.py +219 -219
- package/.claude/hooks/kokoro-tts.py +141 -141
- package/.claude/hooks/party-stage-roster.py +328 -328
- package/.claude/hooks/play-tts-kokoro.sh +7 -2
- package/.claude/hooks/play-tts-ssh-remote.sh +38 -3
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks-windows/agentvibes-session-id.ps1 +83 -0
- package/.claude/hooks-windows/kokoro-server.py +219 -219
- package/.claude/hooks-windows/kokoro-tts.py +107 -107
- package/.claude/hooks-windows/play-tts.ps1 +112 -1
- package/.claude/hooks-windows/session-start-tts.ps1 +36 -0
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/CLAUDE.md +84 -176
- package/README.md +35 -2
- package/RELEASE_NOTES.md +139 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/mcp-server.js +121 -121
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +12 -2
- package/mcp-server/server.py +2111 -2085
- package/mcp-server/test_mcp_correctness.py +504 -504
- package/mcp-server/test_windows_script_parity.py +339 -339
- package/package.json +6 -3
- package/setup-ssh-receiver.ps1 +259 -0
- package/setup-windows.ps1 +39 -29
- package/src/cli/list-personalities.js +110 -110
- package/src/commands/bmad-voices.js +394 -394
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/tabs/agents-tab.js +14 -4
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/settings-tab.js +31 -6
- package/src/console/tabs/setup-tab.js +154 -32
- package/src/console/tabs/voices-tab.js +48 -6
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +98 -37
- package/src/services/language-service.js +47 -47
- package/src/services/llm-provider-service.js +4 -1
- package/src/services/provider-voice-catalog.js +9 -5
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +144 -144
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.ps1 +311 -0
- package/templates/agentvibes-receiver.sh +297 -297
- package/voice-assignments.json +8244 -8244
- package/.agentvibes/config.json +0 -17
- package/.agentvibes/install-manifest.json +0 -362
- package/.claude/config/audio-effects.cfg +0 -7
- package/.claude/config/audio-effects.cfg.bak-kokoro +0 -7
- package/.claude/config/background-music-enabled.txt +0 -1
- package/.claude/config/background-music-position.txt +0 -27
- package/.claude/config/background-music-volume.txt +0 -1
- package/.claude/config/background-music.cfg +0 -1
- package/.claude/config/background-music.txt +0 -1
- package/.claude/config/language.txt +0 -1
- package/.claude/config/personality.txt +0 -1
- package/.claude/config/reverb-level.txt +0 -1
- package/.claude/config/tts-speech-rate.txt +0 -1
- package/.claude/config/tts-verbosity.txt +0 -1
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
|
@@ -1,297 +1,297 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# File: agentvibes-receiver.sh
|
|
4
|
-
# Location: User installs to ~/.termux/agentvibes-play.sh or ~/.agentvibes/play-remote.sh
|
|
5
|
-
#
|
|
6
|
-
# AgentVibes SSH-TTS Receiver
|
|
7
|
-
# Receives base64-encoded JSON payload from remote server via SSH,
|
|
8
|
-
# decodes it, applies voice/music/effects, and plays with local AgentVibes.
|
|
9
|
-
#
|
|
10
|
-
# Payload format: base64({"text","voice","effects","music","volume","pretext","speed","provider","llm"})
|
|
11
|
-
#
|
|
12
|
-
# SSH ForceCommand: SSH_ORIGINAL_COMMAND carries the base64 payload.
|
|
13
|
-
# Direct invocation: pass the payload as first argument.
|
|
14
|
-
#
|
|
15
|
-
# Installation:
|
|
16
|
-
# curl -sSL https://raw.githubusercontent.com/paulpreibisch/AgentVibes/main/scripts/install-ssh-receiver.sh | bash
|
|
17
|
-
# OR
|
|
18
|
-
# agentvibes install --ssh-receiver
|
|
19
|
-
#
|
|
20
|
-
# Copyright (c) 2025 Paul Preibisch
|
|
21
|
-
# Licensed under Apache-2.0
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
set -euo pipefail
|
|
25
|
-
|
|
26
|
-
# Handle -- argument separator (skip it if present)
|
|
27
|
-
if [[ "${1:-}" == "--" ]]; then
|
|
28
|
-
shift
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# ---------------------------------------------------------------------------
|
|
32
|
-
# Resolve encoded payload: arg โ SSH_ORIGINAL_COMMAND โ stdin (legacy)
|
|
33
|
-
# ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
ENCODED_PAYLOAD="${1:-}"
|
|
36
|
-
if [[ -z "$ENCODED_PAYLOAD" ]]; then
|
|
37
|
-
ENCODED_PAYLOAD="${SSH_ORIGINAL_COMMAND:-}"
|
|
38
|
-
fi
|
|
39
|
-
if [[ -z "$ENCODED_PAYLOAD" ]]; then
|
|
40
|
-
ENCODED_PAYLOAD=$(cat 2>/dev/null || true)
|
|
41
|
-
fi
|
|
42
|
-
|
|
43
|
-
if [[ -z "$ENCODED_PAYLOAD" ]]; then
|
|
44
|
-
echo "โ No payload provided" >&2
|
|
45
|
-
echo "Usage: $0 <base64-json-payload>" >&2
|
|
46
|
-
exit 1
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
# Reject oversized payloads (DoS guard: 64 KB is generous for any TTS request)
|
|
50
|
-
_MAX_PAYLOAD_BYTES=65536
|
|
51
|
-
if [[ ${#ENCODED_PAYLOAD} -gt $_MAX_PAYLOAD_BYTES ]]; then
|
|
52
|
-
echo "โ Payload too large (max ${_MAX_PAYLOAD_BYTES} bytes)" >&2
|
|
53
|
-
exit 1
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
# Validate it looks like base64
|
|
57
|
-
if [[ ! "$ENCODED_PAYLOAD" =~ ^[A-Za-z0-9+/=]+$ ]]; then
|
|
58
|
-
echo "โ Payload must be base64-encoded" >&2
|
|
59
|
-
exit 1
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
# Decode
|
|
63
|
-
DECODED=$(printf '%s' "$ENCODED_PAYLOAD" | base64 -d 2>/dev/null) || {
|
|
64
|
-
echo "โ Failed to decode base64 payload" >&2; exit 1
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# ---------------------------------------------------------------------------
|
|
68
|
-
# Parse JSON payload โ python3 is required for JSON parsing
|
|
69
|
-
# ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
if ! command -v python3 &>/dev/null; then
|
|
72
|
-
echo "โ python3 required for JSON parsing" >&2; exit 1
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
# Parse all fields in one python call to avoid repeated process spawning
|
|
76
|
-
# and to log a clear error if the payload is malformed JSON.
|
|
77
|
-
_PARSE_OUT=$(python3 - "$DECODED" 2>&1 <<'PYEOF'
|
|
78
|
-
import json, sys
|
|
79
|
-
try:
|
|
80
|
-
d = json.loads(sys.argv[1])
|
|
81
|
-
for field in ("text","voice","music","volume","effects","pretext","speed","provider","llm","mute","language"):
|
|
82
|
-
print(d.get(field, ""))
|
|
83
|
-
except json.JSONDecodeError as e:
|
|
84
|
-
print(f"JSON_PARSE_ERROR: {e}", file=sys.stderr)
|
|
85
|
-
sys.exit(1)
|
|
86
|
-
PYEOF
|
|
87
|
-
) || { echo "โ Invalid JSON in payload: $_PARSE_OUT" >&2; exit 1; }
|
|
88
|
-
|
|
89
|
-
# Assign fields from ordered output lines
|
|
90
|
-
TEXT=$( sed -n '1p' <<< "$_PARSE_OUT")
|
|
91
|
-
VOICE=$( sed -n '2p' <<< "$_PARSE_OUT")
|
|
92
|
-
MUSIC=$( sed -n '3p' <<< "$_PARSE_OUT")
|
|
93
|
-
VOLUME=$( sed -n '4p' <<< "$_PARSE_OUT")
|
|
94
|
-
EFFECTS=$( sed -n '5p' <<< "$_PARSE_OUT")
|
|
95
|
-
PRETEXT=$( sed -n '6p' <<< "$_PARSE_OUT")
|
|
96
|
-
SPEED=$( sed -n '7p' <<< "$_PARSE_OUT")
|
|
97
|
-
PROVIDER=$(sed -n '8p' <<< "$_PARSE_OUT")
|
|
98
|
-
LLM=$( sed -n '9p' <<< "$_PARSE_OUT")
|
|
99
|
-
MUTE=$( sed -n '10p' <<< "$_PARSE_OUT")
|
|
100
|
-
LANGUAGE=$(sed -n '11p' <<< "$_PARSE_OUT")
|
|
101
|
-
|
|
102
|
-
# Fall back to defaults
|
|
103
|
-
# TODO(AVI-S8.6): generate the volume constant from the shared JSON source of truth.
|
|
104
|
-
[[ -z "$VOICE" ]] && VOICE="en_US-lessac-medium"
|
|
105
|
-
[[ -z "$VOLUME" ]] && VOLUME="0.20"
|
|
106
|
-
[[ -z "$PROVIDER" ]] && PROVIDER="piper"
|
|
107
|
-
|
|
108
|
-
# Validate text
|
|
109
|
-
if [[ -z "$TEXT" ]]; then
|
|
110
|
-
echo "โ No text in payload" >&2; exit 1
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
|
-
# SECURITY: Validate voice โ letters, digits, underscore, hyphen, period, colon
|
|
114
|
-
if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_.:/-]+$ ]]; then
|
|
115
|
-
echo "โ Invalid voice format: $VOICE" >&2; exit 1
|
|
116
|
-
fi
|
|
117
|
-
|
|
118
|
-
# Validate volume is numeric
|
|
119
|
-
if [[ ! "$VOLUME" =~ ^[0-9]+\.?[0-9]*$ ]]; then
|
|
120
|
-
VOLUME="0.20"
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
# Validate provider
|
|
124
|
-
case "$PROVIDER" in
|
|
125
|
-
piper|soprano|macos|windows-sapi|kokoro|elevenlabs) ;;
|
|
126
|
-
*) PROVIDER="piper" ;;
|
|
127
|
-
esac
|
|
128
|
-
|
|
129
|
-
# Prepend pretext if present (same logic as PS receiver)
|
|
130
|
-
if [[ -n "$PRETEXT" ]]; then
|
|
131
|
-
TEXT="${PRETEXT}. ${TEXT}"
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
# ---------------------------------------------------------------------------
|
|
135
|
-
# RECEIVER-AUTHORITATIVE: mute safety-OR (map ยงC, R17)
|
|
136
|
-
# ---------------------------------------------------------------------------
|
|
137
|
-
# effective_mute = receiver_local_mute OR forwarded_mute.
|
|
138
|
-
# If EITHER the sender (forwarded MUTE=="true") or THIS receiver host is muted,
|
|
139
|
-
# we stay silent. A receiver-local mute can always veto; the forwarded mute is a
|
|
140
|
-
# subordinate DEFAULT that can also veto โ we NEVER force-unmute a muted receiver.
|
|
141
|
-
# The receiver's own 3-level mute (project-unmute > project-mute > global-mute)
|
|
142
|
-
# is additionally enforced downstream by play-tts.sh; here we OR in its global
|
|
143
|
-
# mute file so the OR decision is visible/testable at this layer too.
|
|
144
|
-
receiver_local_muted() {
|
|
145
|
-
[[ -f "$HOME/.agentvibes-muted" ]] && return 0
|
|
146
|
-
return 1
|
|
147
|
-
}
|
|
148
|
-
EFFECTIVE_MUTE="false"
|
|
149
|
-
if [[ "$MUTE" == "true" ]] || receiver_local_muted; then
|
|
150
|
-
EFFECTIVE_MUTE="true"
|
|
151
|
-
fi
|
|
152
|
-
|
|
153
|
-
# ---------------------------------------------------------------------------
|
|
154
|
-
# RECEIVER-AUTHORITATIVE: language fallback (map ยงD, R18)
|
|
155
|
-
# ---------------------------------------------------------------------------
|
|
156
|
-
# The receiver's OWN language config wins; otherwise the forwarded language is
|
|
157
|
-
# used as the default. Only forward-as-default is applied here (we never
|
|
158
|
-
# overwrite the receiver's own config). tts-language.txt lives at the .claude/
|
|
159
|
-
# root (language-manager.sh) or the config/ subdir; check both, project-scope is
|
|
160
|
-
# irrelevant on the receiver so we use the receiver host's ~/.claude.
|
|
161
|
-
receiver_local_language() {
|
|
162
|
-
local f
|
|
163
|
-
for f in "$HOME/.claude/tts-language.txt" "$HOME/.claude/config/tts-language.txt"; do
|
|
164
|
-
if [[ -f "$f" ]]; then
|
|
165
|
-
local v
|
|
166
|
-
v=$(cat "$f" 2>/dev/null || true)
|
|
167
|
-
if [[ -n "$v" ]]; then echo "$v"; return 0; fi
|
|
168
|
-
fi
|
|
169
|
-
done
|
|
170
|
-
return 1
|
|
171
|
-
}
|
|
172
|
-
EFFECTIVE_LANGUAGE="$LANGUAGE"
|
|
173
|
-
_RECV_LANG=$(receiver_local_language || true)
|
|
174
|
-
if [[ -n "$_RECV_LANG" ]]; then
|
|
175
|
-
EFFECTIVE_LANGUAGE="$_RECV_LANG" # receiver's own language config wins
|
|
176
|
-
fi
|
|
177
|
-
|
|
178
|
-
# ---------------------------------------------------------------------------
|
|
179
|
-
# Test mode: dump what would be played without calling play-tts
|
|
180
|
-
# ---------------------------------------------------------------------------
|
|
181
|
-
|
|
182
|
-
if [[ "${AGENTVIBES_TEST_MODE:-false}" == "true" ]]; then
|
|
183
|
-
python3 -c "
|
|
184
|
-
import json, sys
|
|
185
|
-
print(json.dumps({
|
|
186
|
-
'text': sys.argv[1],
|
|
187
|
-
'voice': sys.argv[2],
|
|
188
|
-
'music': sys.argv[3],
|
|
189
|
-
'volume': sys.argv[4],
|
|
190
|
-
'effects': sys.argv[5],
|
|
191
|
-
'pretext': sys.argv[6],
|
|
192
|
-
'provider': sys.argv[7],
|
|
193
|
-
'llm': sys.argv[8],
|
|
194
|
-
'mute': sys.argv[9],
|
|
195
|
-
'effective_mute': sys.argv[10],
|
|
196
|
-
'language': sys.argv[11],
|
|
197
|
-
}, ensure_ascii=False))
|
|
198
|
-
" "$TEXT" "$VOICE" "$MUSIC" "$VOLUME" "$EFFECTS" "$PRETEXT" "$PROVIDER" "$LLM" "$MUTE" "$EFFECTIVE_MUTE" "$EFFECTIVE_LANGUAGE"
|
|
199
|
-
exit 0
|
|
200
|
-
fi
|
|
201
|
-
|
|
202
|
-
# RECEIVER-AUTHORITATIVE mute veto: if either side muted, do not play.
|
|
203
|
-
if [[ "$EFFECTIVE_MUTE" == "true" ]]; then
|
|
204
|
-
echo "๐ TTS muted (forwarded=$MUTE, receiver-local honored) โ not playing" >&2
|
|
205
|
-
exit 0
|
|
206
|
-
fi
|
|
207
|
-
|
|
208
|
-
# ---------------------------------------------------------------------------
|
|
209
|
-
# Find AgentVibes installation
|
|
210
|
-
# ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
# Suppress GitHub star reminders (receiver mode)
|
|
213
|
-
export AGENTVIBES_NO_REMINDERS=1
|
|
214
|
-
|
|
215
|
-
find_agentvibes() {
|
|
216
|
-
if command -v agentvibes >/dev/null 2>&1; then
|
|
217
|
-
local bin_path
|
|
218
|
-
bin_path=$(which agentvibes)
|
|
219
|
-
if [[ -L "$bin_path" ]]; then
|
|
220
|
-
bin_path=$(readlink -f "$bin_path" 2>/dev/null || realpath "$bin_path" 2>/dev/null || echo "$bin_path")
|
|
221
|
-
fi
|
|
222
|
-
local lib_path
|
|
223
|
-
lib_path="$(dirname "$(dirname "$bin_path")")/lib/node_modules/agentvibes"
|
|
224
|
-
if [[ -d "$lib_path" ]]; then
|
|
225
|
-
echo "$lib_path"
|
|
226
|
-
return 0
|
|
227
|
-
fi
|
|
228
|
-
fi
|
|
229
|
-
|
|
230
|
-
local search_paths=(
|
|
231
|
-
"$HOME/.npm-global/lib/node_modules/agentvibes"
|
|
232
|
-
"/usr/local/lib/node_modules/agentvibes"
|
|
233
|
-
"/data/data/com.termux/files/usr/lib/node_modules/agentvibes"
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
if [[ -d "$HOME/.nvm/versions/node" ]]; then
|
|
237
|
-
local nvm_path
|
|
238
|
-
nvm_path=$(find "$HOME/.nvm/versions/node" -maxdepth 3 -type d -name "agentvibes" -path "*/lib/node_modules/*" 2>/dev/null | head -1)
|
|
239
|
-
if [[ -n "$nvm_path" ]] && [[ -d "$nvm_path" ]]; then
|
|
240
|
-
echo "$nvm_path"
|
|
241
|
-
return 0
|
|
242
|
-
fi
|
|
243
|
-
fi
|
|
244
|
-
|
|
245
|
-
for p in "${search_paths[@]}"; do
|
|
246
|
-
[[ -d "$p" ]] && { echo "$p"; return 0; }
|
|
247
|
-
done
|
|
248
|
-
|
|
249
|
-
return 1
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
AGENTVIBES_ROOT=$(find_agentvibes)
|
|
253
|
-
|
|
254
|
-
if [[ -z "$AGENTVIBES_ROOT" ]]; then
|
|
255
|
-
echo "โ AgentVibes not found" >&2
|
|
256
|
-
echo "๐ก Install: npm install -g agentvibes" >&2
|
|
257
|
-
exit 1
|
|
258
|
-
fi
|
|
259
|
-
|
|
260
|
-
PLAY_TTS="$AGENTVIBES_ROOT/.claude/hooks/play-tts.sh"
|
|
261
|
-
|
|
262
|
-
if [[ ! -f "$PLAY_TTS" ]]; then
|
|
263
|
-
echo "โ play-tts.sh not found at $PLAY_TTS" >&2; exit 1
|
|
264
|
-
fi
|
|
265
|
-
|
|
266
|
-
# ---------------------------------------------------------------------------
|
|
267
|
-
# Apply music / effects overrides via env vars for play-tts.sh
|
|
268
|
-
# ---------------------------------------------------------------------------
|
|
269
|
-
|
|
270
|
-
[[ -n "$MUSIC" ]] && export AGENTVIBES_OVERRIDE_MUSIC="$MUSIC"
|
|
271
|
-
[[ -n "$VOLUME" ]] && export AGENTVIBES_OVERRIDE_VOLUME="$VOLUME"
|
|
272
|
-
[[ -n "$EFFECTS" ]] && export AGENTVIBES_OVERRIDE_EFFECTS="$EFFECTS"
|
|
273
|
-
[[ -n "$SPEED" ]] && export AGENTVIBES_OVERRIDE_SPEED="$SPEED"
|
|
274
|
-
# RECEIVER-AUTHORITATIVE language default: the receiver's own language config (if
|
|
275
|
-
# any) already won above; forward the effective language downstream so play-tts.sh
|
|
276
|
-
# uses it only when the receiver has no local opinion. Subordinate to receiver config.
|
|
277
|
-
[[ -n "$EFFECTIVE_LANGUAGE" ]] && export AGENTVIBES_OVERRIDE_LANGUAGE="$EFFECTIVE_LANGUAGE"
|
|
278
|
-
|
|
279
|
-
if [[ "${AGENTVIBES_DEBUG:-0}" == "1" ]]; then
|
|
280
|
-
echo "[DEBUG] Voice: $VOICE" >&2
|
|
281
|
-
echo "[DEBUG] Music: ${MUSIC:-none}" >&2
|
|
282
|
-
echo "[DEBUG] Volume: ${VOLUME:-none}" >&2
|
|
283
|
-
echo "[DEBUG] Effects: ${EFFECTS:-none}" >&2
|
|
284
|
-
echo "[DEBUG] Pretext: ${PRETEXT:-none}" >&2
|
|
285
|
-
echo "[DEBUG] Provider: $PROVIDER" >&2
|
|
286
|
-
echo "[DEBUG] Mute: forwarded=$MUTE effective=$EFFECTIVE_MUTE" >&2
|
|
287
|
-
echo "[DEBUG] Language: forwarded=${LANGUAGE:-none} effective=${EFFECTIVE_LANGUAGE:-none}" >&2
|
|
288
|
-
fi
|
|
289
|
-
|
|
290
|
-
echo "๐ต Playing via AgentVibes: ${TEXT:0:50}..." >&2
|
|
291
|
-
# AGENTVIBES_FORCE_PROVIDER lets the sender specify which engine the receiver
|
|
292
|
-
# should use (e.g. kokoro, elevenlabs). The receiver's tts-provider.txt takes
|
|
293
|
-
# precedence if already set to a non-default value; FORCE_PROVIDER is the
|
|
294
|
-
# fallback when the receiver has no local preference configured.
|
|
295
|
-
AGENTVIBES_FORCE_PROVIDER="$PROVIDER" bash "$PLAY_TTS" "$TEXT" "$VOICE"
|
|
296
|
-
|
|
297
|
-
exit 0
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# File: agentvibes-receiver.sh
|
|
4
|
+
# Location: User installs to ~/.termux/agentvibes-play.sh or ~/.agentvibes/play-remote.sh
|
|
5
|
+
#
|
|
6
|
+
# AgentVibes SSH-TTS Receiver
|
|
7
|
+
# Receives base64-encoded JSON payload from remote server via SSH,
|
|
8
|
+
# decodes it, applies voice/music/effects, and plays with local AgentVibes.
|
|
9
|
+
#
|
|
10
|
+
# Payload format: base64({"text","voice","effects","music","volume","pretext","speed","provider","llm"})
|
|
11
|
+
#
|
|
12
|
+
# SSH ForceCommand: SSH_ORIGINAL_COMMAND carries the base64 payload.
|
|
13
|
+
# Direct invocation: pass the payload as first argument.
|
|
14
|
+
#
|
|
15
|
+
# Installation:
|
|
16
|
+
# curl -sSL https://raw.githubusercontent.com/paulpreibisch/AgentVibes/main/scripts/install-ssh-receiver.sh | bash
|
|
17
|
+
# OR
|
|
18
|
+
# agentvibes install --ssh-receiver
|
|
19
|
+
#
|
|
20
|
+
# Copyright (c) 2025 Paul Preibisch
|
|
21
|
+
# Licensed under Apache-2.0
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# Handle -- argument separator (skip it if present)
|
|
27
|
+
if [[ "${1:-}" == "--" ]]; then
|
|
28
|
+
shift
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Resolve encoded payload: arg โ SSH_ORIGINAL_COMMAND โ stdin (legacy)
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
ENCODED_PAYLOAD="${1:-}"
|
|
36
|
+
if [[ -z "$ENCODED_PAYLOAD" ]]; then
|
|
37
|
+
ENCODED_PAYLOAD="${SSH_ORIGINAL_COMMAND:-}"
|
|
38
|
+
fi
|
|
39
|
+
if [[ -z "$ENCODED_PAYLOAD" ]]; then
|
|
40
|
+
ENCODED_PAYLOAD=$(cat 2>/dev/null || true)
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [[ -z "$ENCODED_PAYLOAD" ]]; then
|
|
44
|
+
echo "โ No payload provided" >&2
|
|
45
|
+
echo "Usage: $0 <base64-json-payload>" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Reject oversized payloads (DoS guard: 64 KB is generous for any TTS request)
|
|
50
|
+
_MAX_PAYLOAD_BYTES=65536
|
|
51
|
+
if [[ ${#ENCODED_PAYLOAD} -gt $_MAX_PAYLOAD_BYTES ]]; then
|
|
52
|
+
echo "โ Payload too large (max ${_MAX_PAYLOAD_BYTES} bytes)" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Validate it looks like base64
|
|
57
|
+
if [[ ! "$ENCODED_PAYLOAD" =~ ^[A-Za-z0-9+/=]+$ ]]; then
|
|
58
|
+
echo "โ Payload must be base64-encoded" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Decode
|
|
63
|
+
DECODED=$(printf '%s' "$ENCODED_PAYLOAD" | base64 -d 2>/dev/null) || {
|
|
64
|
+
echo "โ Failed to decode base64 payload" >&2; exit 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Parse JSON payload โ python3 is required for JSON parsing
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
if ! command -v python3 &>/dev/null; then
|
|
72
|
+
echo "โ python3 required for JSON parsing" >&2; exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Parse all fields in one python call to avoid repeated process spawning
|
|
76
|
+
# and to log a clear error if the payload is malformed JSON.
|
|
77
|
+
_PARSE_OUT=$(python3 - "$DECODED" 2>&1 <<'PYEOF'
|
|
78
|
+
import json, sys
|
|
79
|
+
try:
|
|
80
|
+
d = json.loads(sys.argv[1])
|
|
81
|
+
for field in ("text","voice","music","volume","effects","pretext","speed","provider","llm","mute","language"):
|
|
82
|
+
print(d.get(field, ""))
|
|
83
|
+
except json.JSONDecodeError as e:
|
|
84
|
+
print(f"JSON_PARSE_ERROR: {e}", file=sys.stderr)
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
PYEOF
|
|
87
|
+
) || { echo "โ Invalid JSON in payload: $_PARSE_OUT" >&2; exit 1; }
|
|
88
|
+
|
|
89
|
+
# Assign fields from ordered output lines
|
|
90
|
+
TEXT=$( sed -n '1p' <<< "$_PARSE_OUT")
|
|
91
|
+
VOICE=$( sed -n '2p' <<< "$_PARSE_OUT")
|
|
92
|
+
MUSIC=$( sed -n '3p' <<< "$_PARSE_OUT")
|
|
93
|
+
VOLUME=$( sed -n '4p' <<< "$_PARSE_OUT")
|
|
94
|
+
EFFECTS=$( sed -n '5p' <<< "$_PARSE_OUT")
|
|
95
|
+
PRETEXT=$( sed -n '6p' <<< "$_PARSE_OUT")
|
|
96
|
+
SPEED=$( sed -n '7p' <<< "$_PARSE_OUT")
|
|
97
|
+
PROVIDER=$(sed -n '8p' <<< "$_PARSE_OUT")
|
|
98
|
+
LLM=$( sed -n '9p' <<< "$_PARSE_OUT")
|
|
99
|
+
MUTE=$( sed -n '10p' <<< "$_PARSE_OUT")
|
|
100
|
+
LANGUAGE=$(sed -n '11p' <<< "$_PARSE_OUT")
|
|
101
|
+
|
|
102
|
+
# Fall back to defaults
|
|
103
|
+
# TODO(AVI-S8.6): generate the volume constant from the shared JSON source of truth.
|
|
104
|
+
[[ -z "$VOICE" ]] && VOICE="en_US-lessac-medium"
|
|
105
|
+
[[ -z "$VOLUME" ]] && VOLUME="0.20"
|
|
106
|
+
[[ -z "$PROVIDER" ]] && PROVIDER="piper"
|
|
107
|
+
|
|
108
|
+
# Validate text
|
|
109
|
+
if [[ -z "$TEXT" ]]; then
|
|
110
|
+
echo "โ No text in payload" >&2; exit 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# SECURITY: Validate voice โ letters, digits, underscore, hyphen, period, colon
|
|
114
|
+
if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_.:/-]+$ ]]; then
|
|
115
|
+
echo "โ Invalid voice format: $VOICE" >&2; exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Validate volume is numeric
|
|
119
|
+
if [[ ! "$VOLUME" =~ ^[0-9]+\.?[0-9]*$ ]]; then
|
|
120
|
+
VOLUME="0.20"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Validate provider
|
|
124
|
+
case "$PROVIDER" in
|
|
125
|
+
piper|soprano|macos|windows-sapi|kokoro|elevenlabs) ;;
|
|
126
|
+
*) PROVIDER="piper" ;;
|
|
127
|
+
esac
|
|
128
|
+
|
|
129
|
+
# Prepend pretext if present (same logic as PS receiver)
|
|
130
|
+
if [[ -n "$PRETEXT" ]]; then
|
|
131
|
+
TEXT="${PRETEXT}. ${TEXT}"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# RECEIVER-AUTHORITATIVE: mute safety-OR (map ยงC, R17)
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# effective_mute = receiver_local_mute OR forwarded_mute.
|
|
138
|
+
# If EITHER the sender (forwarded MUTE=="true") or THIS receiver host is muted,
|
|
139
|
+
# we stay silent. A receiver-local mute can always veto; the forwarded mute is a
|
|
140
|
+
# subordinate DEFAULT that can also veto โ we NEVER force-unmute a muted receiver.
|
|
141
|
+
# The receiver's own 3-level mute (project-unmute > project-mute > global-mute)
|
|
142
|
+
# is additionally enforced downstream by play-tts.sh; here we OR in its global
|
|
143
|
+
# mute file so the OR decision is visible/testable at this layer too.
|
|
144
|
+
receiver_local_muted() {
|
|
145
|
+
[[ -f "$HOME/.agentvibes-muted" ]] && return 0
|
|
146
|
+
return 1
|
|
147
|
+
}
|
|
148
|
+
EFFECTIVE_MUTE="false"
|
|
149
|
+
if [[ "$MUTE" == "true" ]] || receiver_local_muted; then
|
|
150
|
+
EFFECTIVE_MUTE="true"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# RECEIVER-AUTHORITATIVE: language fallback (map ยงD, R18)
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# The receiver's OWN language config wins; otherwise the forwarded language is
|
|
157
|
+
# used as the default. Only forward-as-default is applied here (we never
|
|
158
|
+
# overwrite the receiver's own config). tts-language.txt lives at the .claude/
|
|
159
|
+
# root (language-manager.sh) or the config/ subdir; check both, project-scope is
|
|
160
|
+
# irrelevant on the receiver so we use the receiver host's ~/.claude.
|
|
161
|
+
receiver_local_language() {
|
|
162
|
+
local f
|
|
163
|
+
for f in "$HOME/.claude/tts-language.txt" "$HOME/.claude/config/tts-language.txt"; do
|
|
164
|
+
if [[ -f "$f" ]]; then
|
|
165
|
+
local v
|
|
166
|
+
v=$(cat "$f" 2>/dev/null || true)
|
|
167
|
+
if [[ -n "$v" ]]; then echo "$v"; return 0; fi
|
|
168
|
+
fi
|
|
169
|
+
done
|
|
170
|
+
return 1
|
|
171
|
+
}
|
|
172
|
+
EFFECTIVE_LANGUAGE="$LANGUAGE"
|
|
173
|
+
_RECV_LANG=$(receiver_local_language || true)
|
|
174
|
+
if [[ -n "$_RECV_LANG" ]]; then
|
|
175
|
+
EFFECTIVE_LANGUAGE="$_RECV_LANG" # receiver's own language config wins
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Test mode: dump what would be played without calling play-tts
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
if [[ "${AGENTVIBES_TEST_MODE:-false}" == "true" ]]; then
|
|
183
|
+
python3 -c "
|
|
184
|
+
import json, sys
|
|
185
|
+
print(json.dumps({
|
|
186
|
+
'text': sys.argv[1],
|
|
187
|
+
'voice': sys.argv[2],
|
|
188
|
+
'music': sys.argv[3],
|
|
189
|
+
'volume': sys.argv[4],
|
|
190
|
+
'effects': sys.argv[5],
|
|
191
|
+
'pretext': sys.argv[6],
|
|
192
|
+
'provider': sys.argv[7],
|
|
193
|
+
'llm': sys.argv[8],
|
|
194
|
+
'mute': sys.argv[9],
|
|
195
|
+
'effective_mute': sys.argv[10],
|
|
196
|
+
'language': sys.argv[11],
|
|
197
|
+
}, ensure_ascii=False))
|
|
198
|
+
" "$TEXT" "$VOICE" "$MUSIC" "$VOLUME" "$EFFECTS" "$PRETEXT" "$PROVIDER" "$LLM" "$MUTE" "$EFFECTIVE_MUTE" "$EFFECTIVE_LANGUAGE"
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# RECEIVER-AUTHORITATIVE mute veto: if either side muted, do not play.
|
|
203
|
+
if [[ "$EFFECTIVE_MUTE" == "true" ]]; then
|
|
204
|
+
echo "๐ TTS muted (forwarded=$MUTE, receiver-local honored) โ not playing" >&2
|
|
205
|
+
exit 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Find AgentVibes installation
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
# Suppress GitHub star reminders (receiver mode)
|
|
213
|
+
export AGENTVIBES_NO_REMINDERS=1
|
|
214
|
+
|
|
215
|
+
find_agentvibes() {
|
|
216
|
+
if command -v agentvibes >/dev/null 2>&1; then
|
|
217
|
+
local bin_path
|
|
218
|
+
bin_path=$(which agentvibes)
|
|
219
|
+
if [[ -L "$bin_path" ]]; then
|
|
220
|
+
bin_path=$(readlink -f "$bin_path" 2>/dev/null || realpath "$bin_path" 2>/dev/null || echo "$bin_path")
|
|
221
|
+
fi
|
|
222
|
+
local lib_path
|
|
223
|
+
lib_path="$(dirname "$(dirname "$bin_path")")/lib/node_modules/agentvibes"
|
|
224
|
+
if [[ -d "$lib_path" ]]; then
|
|
225
|
+
echo "$lib_path"
|
|
226
|
+
return 0
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
local search_paths=(
|
|
231
|
+
"$HOME/.npm-global/lib/node_modules/agentvibes"
|
|
232
|
+
"/usr/local/lib/node_modules/agentvibes"
|
|
233
|
+
"/data/data/com.termux/files/usr/lib/node_modules/agentvibes"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if [[ -d "$HOME/.nvm/versions/node" ]]; then
|
|
237
|
+
local nvm_path
|
|
238
|
+
nvm_path=$(find "$HOME/.nvm/versions/node" -maxdepth 3 -type d -name "agentvibes" -path "*/lib/node_modules/*" 2>/dev/null | head -1)
|
|
239
|
+
if [[ -n "$nvm_path" ]] && [[ -d "$nvm_path" ]]; then
|
|
240
|
+
echo "$nvm_path"
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
for p in "${search_paths[@]}"; do
|
|
246
|
+
[[ -d "$p" ]] && { echo "$p"; return 0; }
|
|
247
|
+
done
|
|
248
|
+
|
|
249
|
+
return 1
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
AGENTVIBES_ROOT=$(find_agentvibes)
|
|
253
|
+
|
|
254
|
+
if [[ -z "$AGENTVIBES_ROOT" ]]; then
|
|
255
|
+
echo "โ AgentVibes not found" >&2
|
|
256
|
+
echo "๐ก Install: npm install -g agentvibes" >&2
|
|
257
|
+
exit 1
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
PLAY_TTS="$AGENTVIBES_ROOT/.claude/hooks/play-tts.sh"
|
|
261
|
+
|
|
262
|
+
if [[ ! -f "$PLAY_TTS" ]]; then
|
|
263
|
+
echo "โ play-tts.sh not found at $PLAY_TTS" >&2; exit 1
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
# Apply music / effects overrides via env vars for play-tts.sh
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
[[ -n "$MUSIC" ]] && export AGENTVIBES_OVERRIDE_MUSIC="$MUSIC"
|
|
271
|
+
[[ -n "$VOLUME" ]] && export AGENTVIBES_OVERRIDE_VOLUME="$VOLUME"
|
|
272
|
+
[[ -n "$EFFECTS" ]] && export AGENTVIBES_OVERRIDE_EFFECTS="$EFFECTS"
|
|
273
|
+
[[ -n "$SPEED" ]] && export AGENTVIBES_OVERRIDE_SPEED="$SPEED"
|
|
274
|
+
# RECEIVER-AUTHORITATIVE language default: the receiver's own language config (if
|
|
275
|
+
# any) already won above; forward the effective language downstream so play-tts.sh
|
|
276
|
+
# uses it only when the receiver has no local opinion. Subordinate to receiver config.
|
|
277
|
+
[[ -n "$EFFECTIVE_LANGUAGE" ]] && export AGENTVIBES_OVERRIDE_LANGUAGE="$EFFECTIVE_LANGUAGE"
|
|
278
|
+
|
|
279
|
+
if [[ "${AGENTVIBES_DEBUG:-0}" == "1" ]]; then
|
|
280
|
+
echo "[DEBUG] Voice: $VOICE" >&2
|
|
281
|
+
echo "[DEBUG] Music: ${MUSIC:-none}" >&2
|
|
282
|
+
echo "[DEBUG] Volume: ${VOLUME:-none}" >&2
|
|
283
|
+
echo "[DEBUG] Effects: ${EFFECTS:-none}" >&2
|
|
284
|
+
echo "[DEBUG] Pretext: ${PRETEXT:-none}" >&2
|
|
285
|
+
echo "[DEBUG] Provider: $PROVIDER" >&2
|
|
286
|
+
echo "[DEBUG] Mute: forwarded=$MUTE effective=$EFFECTIVE_MUTE" >&2
|
|
287
|
+
echo "[DEBUG] Language: forwarded=${LANGUAGE:-none} effective=${EFFECTIVE_LANGUAGE:-none}" >&2
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
echo "๐ต Playing via AgentVibes: ${TEXT:0:50}..." >&2
|
|
291
|
+
# AGENTVIBES_FORCE_PROVIDER lets the sender specify which engine the receiver
|
|
292
|
+
# should use (e.g. kokoro, elevenlabs). The receiver's tts-provider.txt takes
|
|
293
|
+
# precedence if already set to a non-default value; FORCE_PROVIDER is the
|
|
294
|
+
# fallback when the receiver has no local preference configured.
|
|
295
|
+
AGENTVIBES_FORCE_PROVIDER="$PROVIDER" bash "$PLAY_TTS" "$TEXT" "$VOICE"
|
|
296
|
+
|
|
297
|
+
exit 0
|