agentvibes 5.13.0 โ†’ 5.14.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.
Files changed (82) hide show
  1. package/.claude/audio/ui/CREDITS.txt +16 -16
  2. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  3. package/.claude/commands/agent-vibes-rdp.md +24 -24
  4. package/.claude/config/audio-effects.cfg.sample +52 -52
  5. package/.claude/docs/TERMUX_SETUP.md +408 -408
  6. package/.claude/github-star-reminder.txt +1 -1
  7. package/.claude/hooks/kokoro-server.py +219 -219
  8. package/.claude/hooks/kokoro-tts.py +141 -141
  9. package/.claude/hooks/party-stage-roster.py +328 -328
  10. package/.claude/hooks/play-tts-kokoro.sh +7 -2
  11. package/.claude/hooks/play-tts-ssh-remote.sh +38 -3
  12. package/.claude/hooks/requirements.txt +6 -6
  13. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  14. package/.claude/hooks/translator.py +237 -237
  15. package/.claude/hooks-windows/kokoro-server.py +219 -219
  16. package/.claude/hooks-windows/kokoro-tts.py +107 -107
  17. package/.claude/hooks-windows/play-tts.ps1 +17 -0
  18. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  19. package/.claude/verbosity.txt +1 -1
  20. package/.clawdbot/README.md +105 -105
  21. package/CLAUDE.md +84 -176
  22. package/README.md +27 -2
  23. package/RELEASE_NOTES.md +86 -0
  24. package/WINDOWS-SETUP.md +208 -208
  25. package/bin/agent-vibes +39 -39
  26. package/bin/mcp-server.js +121 -121
  27. package/bin/test-bmad-pr +78 -78
  28. package/mcp-server/QUICK_START.md +203 -203
  29. package/mcp-server/README.md +345 -345
  30. package/mcp-server/examples/claude_desktop_config.json +11 -11
  31. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  32. package/mcp-server/examples/custom_instructions.md +169 -169
  33. package/mcp-server/install-deps.js +12 -2
  34. package/mcp-server/server.py +2111 -2085
  35. package/mcp-server/test_mcp_correctness.py +504 -504
  36. package/mcp-server/test_windows_script_parity.py +339 -339
  37. package/package.json +7 -4
  38. package/setup-ssh-receiver.ps1 +259 -0
  39. package/setup-windows.ps1 +39 -29
  40. package/src/cli/list-personalities.js +110 -110
  41. package/src/commands/bmad-voices.js +394 -394
  42. package/src/console/brand-colors.js +13 -13
  43. package/src/console/constants/personalities.js +44 -44
  44. package/src/console/tabs/agents-tab.js +14 -4
  45. package/src/console/tabs/help-tab.js +314 -314
  46. package/src/console/tabs/readme-tab.js +272 -272
  47. package/src/console/tabs/settings-tab.js +31 -6
  48. package/src/console/tabs/setup-tab.js +154 -32
  49. package/src/console/tabs/voices-tab.js +48 -6
  50. package/src/console/widgets/destroy-list.js +25 -25
  51. package/src/console/widgets/notice.js +55 -55
  52. package/src/installer/language-screen.js +31 -31
  53. package/src/installer/music-file-input.js +304 -304
  54. package/src/installer.js +103 -43
  55. package/src/services/language-service.js +47 -47
  56. package/src/services/llm-provider-service.js +4 -1
  57. package/src/services/provider-voice-catalog.js +9 -5
  58. package/src/utils/audio-format-validator.js +277 -277
  59. package/src/utils/dependency-checker.js +469 -469
  60. package/src/utils/file-ownership-verifier.js +358 -358
  61. package/src/utils/music-file-validator.js +285 -285
  62. package/src/utils/preview-list-prompt.js +144 -144
  63. package/src/utils/secure-music-storage.js +412 -412
  64. package/templates/agentvibes-receiver.ps1 +311 -0
  65. package/templates/agentvibes-receiver.sh +297 -297
  66. package/voice-assignments.json +8244 -8244
  67. package/.agentvibes/config.json +0 -17
  68. package/.agentvibes/install-manifest.json +0 -342
  69. package/.claude/config/audio-effects.cfg +0 -7
  70. package/.claude/config/audio-effects.cfg.bak-kokoro +0 -7
  71. package/.claude/config/background-music-enabled.txt +0 -1
  72. package/.claude/config/background-music-position.txt +0 -27
  73. package/.claude/config/background-music-volume.txt +0 -1
  74. package/.claude/config/background-music.cfg +0 -1
  75. package/.claude/config/background-music.txt +0 -1
  76. package/.claude/config/language.txt +0 -1
  77. package/.claude/config/personality.txt +0 -1
  78. package/.claude/config/reverb-level.txt +0 -1
  79. package/.claude/config/tts-speech-rate.txt +0 -1
  80. package/.claude/config/tts-verbosity.txt +0 -1
  81. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
  82. 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