agentvibes 3.5.10-alpha.0 → 4.0.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 (70) hide show
  1. package/.agentvibes/bmad/bmad-voices-enabled.flag +0 -0
  2. package/.agentvibes/bmad/bmad-voices.md +69 -0
  3. package/.claude/config/audio-effects.cfg +1 -1
  4. package/.claude/config/background-music-position.txt +1 -27
  5. package/.claude/github-star-reminder.txt +1 -1
  6. package/.claude/hooks/audio-processor.sh +32 -17
  7. package/.claude/hooks/bmad-speak-enhanced.sh +5 -5
  8. package/.claude/hooks/bmad-speak.sh +4 -4
  9. package/.claude/hooks/bmad-voice-manager.sh +8 -8
  10. package/.claude/hooks/clawdbot-receiver-SECURE.sh +23 -25
  11. package/.claude/hooks/clawdbot-receiver.sh +28 -4
  12. package/.claude/hooks/language-manager.sh +1 -1
  13. package/.claude/hooks/path-resolver.sh +60 -0
  14. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -0
  15. package/.claude/hooks/play-tts-piper.sh +82 -24
  16. package/.claude/hooks/play-tts-ssh-remote.sh +13 -15
  17. package/.claude/hooks/play-tts.sh +16 -5
  18. package/.claude/hooks/session-start-tts.sh +26 -56
  19. package/.claude/hooks/soprano-gradio-synth.py +1 -1
  20. package/.claude/hooks/verbosity-manager.sh +10 -4
  21. package/.claude/settings.json +1 -1
  22. package/CLAUDE.md +129 -104
  23. package/README.md +418 -17
  24. package/RELEASE_NOTES.md +60 -1036
  25. package/bin/agentvibes-voice-browser.js +1827 -0
  26. package/bin/agentvibes.js +100 -0
  27. package/mcp-server/server.py +67 -3
  28. package/package.json +9 -2
  29. package/src/console/app.js +806 -0
  30. package/src/console/audio-env.js +123 -0
  31. package/src/console/brand-colors.js +13 -0
  32. package/src/console/footer-config.js +42 -0
  33. package/src/console/modals/.gitkeep +0 -0
  34. package/src/console/modals/modal-overlay.js +247 -0
  35. package/src/console/navigation.js +60 -0
  36. package/src/console/tabs/.gitkeep +0 -0
  37. package/src/console/tabs/agents-tab.js +369 -0
  38. package/src/console/tabs/help-tab.js +261 -0
  39. package/src/console/tabs/install-tab.js +990 -0
  40. package/src/console/tabs/music-tab.js +997 -0
  41. package/src/console/tabs/placeholder-tab.js +45 -0
  42. package/src/console/tabs/readme-tab.js +267 -0
  43. package/src/console/tabs/settings-tab.js +3949 -0
  44. package/src/console/tabs/voices-tab.js +1574 -0
  45. package/src/installer/music-file-input.js +304 -0
  46. package/src/installer.js +1104 -704
  47. package/src/services/.gitkeep +0 -0
  48. package/src/services/agent-voice-store.js +163 -0
  49. package/src/services/config-service.js +240 -0
  50. package/src/services/navigation-service.js +123 -0
  51. package/src/services/provider-service.js +132 -0
  52. package/src/services/verbosity-service.js +157 -0
  53. package/src/utils/audio-duration-validator.js +298 -0
  54. package/src/utils/audio-format-validator.js +277 -0
  55. package/src/utils/dependency-checker.js +3 -3
  56. package/src/utils/file-ownership-verifier.js +358 -0
  57. package/src/utils/music-file-validator.js +275 -0
  58. package/src/utils/preview-list-prompt.js +136 -0
  59. package/src/utils/secure-music-storage.js +412 -0
  60. package/templates/agentvibes-receiver.sh +11 -7
  61. package/voice-assignments.json +8245 -0
  62. package/.claude/config/background-music-volume.txt +0 -1
  63. package/.claude/config/background-music.cfg +0 -1
  64. package/.claude/config/background-music.txt +0 -1
  65. package/.claude/config/tts-speech-rate.txt +0 -1
  66. package/.claude/config/tts-verbosity.txt +0 -1
  67. package/.claude/hooks/bmad-party-manager.sh +0 -225
  68. package/.claude/hooks/stop.sh +0 -38
  69. package/.claude/piper-voices-dir.txt +0 -1
  70. package/.mcp.json +0 -34
File without changes
@@ -0,0 +1,69 @@
1
+ ---
2
+ plugin: bmad-voices
3
+ version: 2.0.0
4
+ enabled: true
5
+ description: Provider-aware voice mappings for BMAD agents
6
+ ---
7
+
8
+ # BMAD Voice Plugin
9
+
10
+ This plugin automatically assigns voices to BMAD agents based on their role and active TTS provider.
11
+
12
+ ## Agent Voice Mappings (Provider-Aware)
13
+
14
+ | Agent ID | Agent Name | Intro | Piper TTS Voice | Piper Voice | Personality |
15
+ |----------|------------|-------|------------------|-------------|-------------|
16
+ | pm | John (Product Manager) | John, Product Manager here | Matthew Schmitz | en_US-john-medium | professional |
17
+ | dev | Amelia (Developer) | Amelia, Developer here | Aria | en_US-amy-medium | normal |
18
+ | analyst | Mary (Business Analyst) | Mary, Business Analyst here | Jessica Anne Bogart | en_US-kristin-medium | normal |
19
+ | architect | Winston (Architect) | Winston, Architect here | Michael | en_GB-alan-medium | normal |
20
+ | sm | Bob (Scrum Master) | Bob, Scrum Master here | Matthew Schmitz | en_US-joe-medium | professional |
21
+ | tea | Murat (Test Architect) | Murat, Test Architect here | Michael | en_US-kusal-medium | normal |
22
+ | tech-writer | Paige (Technical Writer) | Paige, Technical Writer here | Aria | en_US-lessac-medium | normal |
23
+ | ux-designer | Sally (UX Designer) | Sally, UX Designer here | Jessica Anne Bogart | en_US-lessac-high | normal |
24
+ | frame-expert | Saif (Visual Designer) | Saif, Visual Designer here | Matthew Schmitz | en_GB-alan-medium | normal |
25
+ | bmad-master | BMad Master | BMad Master here | Michael | en_US-libritts-high | zen |
26
+ | quick-flow-solo-dev | Barry (Quick Flow Solo Dev) | Barry, Quick Flow Solo Dev here | Matthew Schmitz | en_US-john-medium | professional |
27
+
28
+ ## How It Works
29
+
30
+ The voice manager automatically selects the appropriate voice based on your active TTS provider:
31
+ - **Piper TTS active**: Uses voices from the "Piper TTS Voice" column
32
+ - **Piper active**: Uses voices from the "Piper Voice" column
33
+
34
+ This ensures BMAD agents work seamlessly regardless of which provider you're using.
35
+
36
+ ### Supports Both Display Names and Agent IDs
37
+
38
+ The `bmad-speak.sh` script accepts both formats:
39
+
40
+ **Party Mode** (multiple agents, uses display names):
41
+ ```bash
42
+ .claude/hooks/bmad-speak.sh "Winston" "I recommend microservices for scalability"
43
+ ```
44
+
45
+ **Individual Agents** (single agent sessions, uses agent IDs):
46
+ ```bash
47
+ .claude/hooks/bmad-speak.sh "architect" "I recommend microservices for scalability"
48
+ ```
49
+
50
+ Both formats map to the same voice configuration based on the agent ID in the table above. This allows BMAD to use customizable display names while maintaining stable voice mappings.
51
+
52
+ ## How to Edit
53
+
54
+ Simply edit the table above to change voice mappings. The format is:
55
+ - **Agent ID**: Must match BMAD's `agent.id` field (pm, dev, qa, etc.)
56
+ - **Agent Name**: Display name (for reference only)
57
+ - **Intro**: Text spoken before agent's message (e.g., "John, Product Manager here"). Leave empty to disable.
58
+ - **Piper TTS Voice**: Voice name for Piper TTS provider
59
+ - **Piper Voice**: Voice model name for Piper provider
60
+ - **Personality**: Optional personality to apply (or "normal" for none)
61
+
62
+ ## Commands
63
+
64
+ - `/agent-vibes:bmad enable` - Enable BMAD voice plugin
65
+ - `/agent-vibes:bmad disable` - Disable BMAD voice plugin
66
+ - `/agent-vibes:bmad status` - Show plugin status
67
+ - `/agent-vibes:bmad edit` - Open this file for editing
68
+ - `/agent-vibes:bmad list` - List all agent voice mappings
69
+ - `/agent-vibes:bmad set <agent-id> <piper-voice> <piper-voice> [personality]` - Set voices for specific agent
@@ -49,4 +49,4 @@ BMad Master|reverb 50 60 100 pitch -100|agentvibes_soft_flamenco_loop.mp3|0.30
49
49
  _party_mode|compand 0.3,1 6:-70,-60,-20|agent_vibes_dark_chill_step_loop.mp3|0.40
50
50
  |||
51
51
  # Default (no agent specified) - clean with Bachata background|||
52
- default|reverb 20 50 50|agentvibes_soft_flamenco_loop.mp3|0.30
52
+ default|reverb 40 50 70|agent_vibes_celtic_harp_v1_loop.mp3|0.30
@@ -1,27 +1 @@
1
- agent-vibes-dark-chill-step.mp3:49.006169
2
- Agent Vibes Japanese City Pop v1.mp3:29.392744
3
- Agent Vibes ChillWave v2.mp3:22.154467
4
- Agent Vibes Bossa Nova v2.mp3:23.733424
5
- Agent Vibes Tabla Dream Pop v1.mp3:19.101043
6
- Agent Vibes Hawaiian slack key guitar v2.mp3:36.381950
7
- AgentVibes Soft Flamenco.mp3:23.160000
8
- Agent Vibes Arabic v2.mp3:21.922268
9
- Agent Vibes Goa Trance v2.mp3:55.953741
10
- Agent Vibes Ganawa Ambient v2.mp3:39.680205
11
- Agent Vibes Celtic Harp v1.mp3:42.190476
12
- Agent Vibes Harpsichord v2.mp3:21.739410
13
- Agent Vibes Japanese City Pop v1-loop.mp3:13.917551
14
- Agent Vibes Hawaiian slack key guitar v2-loop.mp3:12.977143
15
- Agent Vibes Ganawa Ambient v2-loop.mp3:.00000000000000000002815996
16
- Agent Vibes Tabla Dream Pop v1-loop.mp3:.00000000000000000009067943
17
- Agent Vibes ChillWave v2-loop.mp3:.00000000000000000007080511
18
- Agent Vibes Harpsichord v2-loop.mp3:.00000000000000000013140818
19
- agent_vibes_japanese_city_pop_v1_loop.mp3:6.054512
20
- agent_vibes_bossa_nova_v2_loop.mp3:5.369524
21
- agent_vibes_salsa_v2_loop.mp3:9.972790
22
- agent_vibes_cumbia_v1_loop.mp3:5.717823
23
- agent_vibes_arabic_v2_loop.mp3:.00000000000000000006132724
24
- agent_vibes_chillwave_v2_loop.mp3:14.628390
25
- agent_vibes_bachata_v1_loop.mp3:.00000000000000000005344000
26
- agentvibes_soft_flamenco_loop.mp3:.00000000000000000006934441
27
- agent_vibes_goa_trance_v2_loop.mp3:.00000000000000000002499918
1
+ agent_vibes_celtic_harp_v1_loop.mp3:12.341678
@@ -1 +1 @@
1
- 20260214
1
+ 20260309
@@ -39,18 +39,19 @@ OUTPUT_FILE="${3:-}"
39
39
  CONFIG_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/config/audio-effects.cfg"
40
40
  BACKGROUNDS_DIR="$(cd "$SCRIPT_DIR/../audio" && pwd)/tracks"
41
41
  ENABLED_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/config/background-music-enabled.txt"
42
+ GLOBAL_ENABLED_FILE="$HOME/.claude/config/background-music-enabled.txt"
42
43
 
43
- # Check if background music is globally enabled
44
+ # Check if background music is enabled (project-local, then global fallback)
44
45
  is_background_music_enabled() {
45
- # Default to false if file doesn't exist
46
- if [[ ! -f "$ENABLED_FILE" ]]; then
46
+ local enabled=""
47
+ if [[ -f "$ENABLED_FILE" ]]; then
48
+ enabled=$(cat "$ENABLED_FILE" 2>/dev/null | tr -d '[:space:]')
49
+ elif [[ -f "$GLOBAL_ENABLED_FILE" ]]; then
50
+ enabled=$(cat "$GLOBAL_ENABLED_FILE" 2>/dev/null | tr -d '[:space:]')
51
+ else
47
52
  return 1 # Disabled by default
48
53
  fi
49
54
 
50
- # Read the enabled flag
51
- local enabled
52
- enabled=$(cat "$ENABLED_FILE" 2>/dev/null | tr -d '[:space:]')
53
-
54
55
  # Return 0 (true) if enabled, 1 (false) otherwise
55
56
  [[ "$enabled" == "true" ]]
56
57
  }
@@ -87,9 +88,9 @@ get_agent_config() {
87
88
  return
88
89
  fi
89
90
 
90
- # Try exact match first
91
+ # Try exact match first (use awk for safe literal matching)
91
92
  local config
92
- config=$(grep -i "^${agent}|" "$CONFIG_FILE" 2>/dev/null | head -1)
93
+ config=$(awk -F'|' -v agent="$agent" 'tolower($1) == tolower(agent)' "$CONFIG_FILE" 2>/dev/null | head -1)
93
94
 
94
95
  # Fall back to default
95
96
  if [[ -z "$config" ]]; then
@@ -119,6 +120,16 @@ apply_sox_effects() {
119
120
  return 0
120
121
  fi
121
122
 
123
+ # Validate effects contain only allowed sox effect names and numeric params
124
+ local allowed_effects="gain|reverb|echo|chorus|flanger|phaser|tremolo|overdrive|bass|treble|equalizer|highpass|lowpass|bandpass|vol|speed|tempo|pitch|rate|pad|silence|trim|fade|norm|loudness|compand|contrast|delay|repeat|stat|remix"
125
+ for word in $effects; do
126
+ if ! [[ "$word" =~ ^-?[0-9]*\.?[0-9]+$ ]] && ! echo "$word" | grep -qiE "^($allowed_effects)$"; then
127
+ echo "Warning: Invalid sox effect '$word', skipping effects" >&2
128
+ cp "$input" "$output"
129
+ return 0
130
+ fi
131
+ done
132
+
122
133
  # Apply effects - note: effects string is intentionally unquoted to allow word splitting
123
134
  # shellcheck disable=SC2086
124
135
  sox "$input" "$output" $effects 2>/dev/null || {
@@ -140,7 +151,7 @@ get_background_position() {
140
151
  bg_name=$(basename "$bg_file")
141
152
 
142
153
  if [[ -f "$POSITION_FILE" ]]; then
143
- grep "^${bg_name}:" "$POSITION_FILE" 2>/dev/null | cut -d: -f2 | tr -d '[:space:]' || echo "0"
154
+ awk -F: -v name="$bg_name" '$1 == name {print $2}' "$POSITION_FILE" 2>/dev/null | tr -d '[:space:]' | tail -1
144
155
  else
145
156
  echo "0"
146
157
  fi
@@ -158,12 +169,14 @@ save_background_position() {
158
169
 
159
170
  mkdir -p "$(dirname "$POSITION_FILE")"
160
171
 
161
- # Remove old entry and add new one
172
+ # Remove old entry and add new one (atomic update via temp file + mv)
173
+ local tmp_pos
174
+ tmp_pos=$(mktemp "${POSITION_FILE}.XXXXXX")
162
175
  if [[ -f "$POSITION_FILE" ]]; then
163
- grep -v "^${bg_name}:" "$POSITION_FILE" > "${POSITION_FILE}.tmp" 2>/dev/null || true
164
- mv "${POSITION_FILE}.tmp" "$POSITION_FILE"
176
+ grep -v "^${bg_name}:" "$POSITION_FILE" > "$tmp_pos" 2>/dev/null || true
165
177
  fi
166
- echo "${bg_name}:${position}" >> "$POSITION_FILE"
178
+ echo "${bg_name}:${position}" >> "$tmp_pos"
179
+ mv "$tmp_pos" "$POSITION_FILE"
167
180
  }
168
181
 
169
182
  # @function mix_background
@@ -336,9 +349,11 @@ main() {
336
349
  temp_effects=$(mktemp "$TEMP_DIR/effects-XXXXXX.wav")
337
350
  temp_final=$(mktemp "$TEMP_DIR/final-XXXXXX.wav")
338
351
 
339
- # Clean up on exit - use double quotes to capture paths at definition time
340
- # (local variables won't exist at trap execution time outside function scope)
341
- trap "rm -f '$temp_effects' '$temp_final'" EXIT
352
+ # Clean up on exit - use a cleanup function to avoid trap injection
353
+ _cleanup_effects="$temp_effects"
354
+ _cleanup_final="$temp_final"
355
+ cleanup() { rm -f "$_cleanup_effects" "$_cleanup_final"; }
356
+ trap cleanup EXIT
342
357
 
343
358
  # Step 1: Apply sox effects
344
359
  if [[ -n "$sox_effects" ]]; then
@@ -39,7 +39,7 @@ if [[ -f "$PROJECT_ROOT/.agentvibes/bmad/bmad-party-mode-disabled.flag" ]]; then
39
39
  fi
40
40
 
41
41
  # Check if BMAD is installed
42
- if [[ ! -f "$PROJECT_ROOT/.bmad/_cfg/agent-manifest.csv" ]]; then
42
+ if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
43
43
  exit 0
44
44
  fi
45
45
 
@@ -48,13 +48,13 @@ map_to_agent_id() {
48
48
  local name_or_id="$1"
49
49
 
50
50
  # If it looks like a file path, extract the agent ID
51
- if [[ "$name_or_id" =~ \.bmad/.*/agents/([^/]+)\.md$ ]]; then
51
+ if [[ "$name_or_id" =~ _?\.?bmad/.*/agents/([^/]+)\.md$ ]]; then
52
52
  echo "${BASH_REMATCH[1]}"
53
53
  return
54
54
  fi
55
55
 
56
56
  # Check if it's already an agent ID
57
- local direct_match=$(grep -i "^\"*${name_or_id}\"*," "$PROJECT_ROOT/.bmad/_cfg/agent-manifest.csv" | head -1)
57
+ local direct_match=$(grep -i "^\"*${name_or_id}\"*," "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" | head -1)
58
58
  if [[ -n "$direct_match" ]]; then
59
59
  echo "$name_or_id"
60
60
  return
@@ -73,7 +73,7 @@ map_to_agent_id() {
73
73
  exit
74
74
  }
75
75
  }
76
- ' "$PROJECT_ROOT/.bmad/_cfg/agent-manifest.csv")
76
+ ' "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv")
77
77
 
78
78
  echo "$agent_id"
79
79
  }
@@ -94,7 +94,7 @@ get_display_name() {
94
94
  exit
95
95
  }
96
96
  }
97
- ' "$PROJECT_ROOT/.bmad/_cfg/agent-manifest.csv")
97
+ ' "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv")
98
98
 
99
99
  echo "$display_name"
100
100
  }
@@ -34,7 +34,7 @@ if [[ -f "$PROJECT_ROOT/.agentvibes/bmad/bmad-party-mode-disabled.flag" ]]; then
34
34
  fi
35
35
 
36
36
  # Check if BMAD is installed
37
- if [[ ! -f "$PROJECT_ROOT/.bmad/_cfg/agent-manifest.csv" ]]; then
37
+ if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
38
38
  exit 0
39
39
  fi
40
40
 
@@ -44,14 +44,14 @@ map_to_agent_id() {
44
44
 
45
45
  # If it looks like a file path (.bmad/*/agents/*.md), extract the agent ID
46
46
  # Example: .bmad/bmm/agents/pm.md -> pm
47
- if [[ "$name_or_id" =~ \.bmad/.*/agents/([^/]+)\.md$ ]]; then
47
+ if [[ "$name_or_id" =~ _?\.?bmad/.*/agents/([^/]+)\.md$ ]]; then
48
48
  echo "${BASH_REMATCH[1]}"
49
49
  return
50
50
  fi
51
51
 
52
52
  # First check if it's already an agent ID (column 1 of manifest)
53
53
  # CSV format: name,displayName,title,icon,role,...
54
- local direct_match=$(grep -i "^\"*${name_or_id}\"*," "$PROJECT_ROOT/.bmad/_cfg/agent-manifest.csv" | head -1)
54
+ local direct_match=$(grep -i "^\"*${name_or_id}\"*," "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" | head -1)
55
55
  if [[ -n "$direct_match" ]]; then
56
56
  # Already an agent ID, pass through
57
57
  echo "$name_or_id"
@@ -78,7 +78,7 @@ map_to_agent_id() {
78
78
  exit
79
79
  }
80
80
  }
81
- ' "$PROJECT_ROOT/.bmad/_cfg/agent-manifest.csv")
81
+ ' "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv")
82
82
 
83
83
  echo "$agent_id"
84
84
  }
@@ -281,10 +281,10 @@ sync_intros_from_manifest() {
281
281
  bmad_voice_map="bmad/_cfg/agent-voice-map.csv"
282
282
  fi
283
283
 
284
- if [[ -f ".bmad/_cfg/agent-manifest.csv" ]]; then
285
- manifest_file=".bmad/_cfg/agent-manifest.csv"
286
- elif [[ -f "bmad/_cfg/agent-manifest.csv" ]]; then
287
- manifest_file="bmad/_cfg/agent-manifest.csv"
284
+ if [[ -f "_bmad/_config/agent-manifest.csv" ]]; then
285
+ manifest_file="_bmad/_config/agent-manifest.csv"
286
+ elif [[ -f "_bmad/_config/agent-manifest.csv" ]]; then
287
+ manifest_file="_bmad/_config/agent-manifest.csv"
288
288
  fi
289
289
 
290
290
  # Both files must exist for sync to work
@@ -404,10 +404,10 @@ get_agent_intro() {
404
404
  if [[ -z "$intro" ]] || [[ "$intro" == "Hello! Ready to help with the discussion." ]]; then
405
405
  # Try to get display name from agent-manifest.csv
406
406
  local manifest_file=""
407
- if [[ -f ".bmad/_cfg/agent-manifest.csv" ]]; then
408
- manifest_file=".bmad/_cfg/agent-manifest.csv"
409
- elif [[ -f "bmad/_cfg/agent-manifest.csv" ]]; then
410
- manifest_file="bmad/_cfg/agent-manifest.csv"
407
+ if [[ -f "_bmad/_config/agent-manifest.csv" ]]; then
408
+ manifest_file="_bmad/_config/agent-manifest.csv"
409
+ elif [[ -f "_bmad/_config/agent-manifest.csv" ]]; then
410
+ manifest_file="_bmad/_config/agent-manifest.csv"
411
411
  fi
412
412
 
413
413
  if [[ -n "$manifest_file" ]]; then
@@ -26,20 +26,19 @@ VOICE="${2:-en_US-lessac-medium}"
26
26
  ENCODED_AGENT="${3:-}"
27
27
  ENCODED_INTRO="${4:-}"
28
28
 
29
- # SECURITY: Whitelist of allowed voice names
30
- ALLOWED_VOICES="en_US-amy-medium|en_US-lessac-medium|es_ES-mls_9972-low|es_ES-davefx-medium|en_US-joe-medium"
31
-
32
29
  # Validate inputs
33
30
  if [[ -z "$ENCODED_TEXT" ]]; then
34
- echo " No encoded text provided" >&2
31
+ echo "Error: No encoded text provided" >&2
35
32
  echo "Usage: $0 <base64_text> <voice> <base64_agent_name> [base64_intro]" >&2
36
33
  exit 1
37
34
  fi
38
35
 
39
- # SECURITY: Validate VOICE parameter against whitelist
40
- if ! [[ "$VOICE" =~ ^($ALLOWED_VOICES)$ ]]; then
41
- echo "❌ Invalid voice name: $VOICE" >&2
42
- echo "Allowed voices: $(echo "$ALLOWED_VOICES" | tr '|' ', ')" >&2
36
+ # SECURITY: Validate base64 format (reject shell metacharacters)
37
+ [[ "$ENCODED_TEXT" =~ ^[A-Za-z0-9+/=]+$ ]] || { echo "Error: invalid base64 text" >&2; exit 1; }
38
+
39
+ # SECURITY: Validate VOICE parameter format (alphanumeric, hyphens, underscores only)
40
+ if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then
41
+ echo "Error: Invalid voice name format: $VOICE" >&2
43
42
  exit 1
44
43
  fi
45
44
 
@@ -58,31 +57,30 @@ fi
58
57
 
59
58
  DECODED_AGENT="default"
60
59
  if [[ -n "$ENCODED_AGENT" ]]; then
61
- DECODED_AGENT=$(echo -n "$ENCODED_AGENT" | base64 -d 2>/dev/null) || DECODED_AGENT="default"
62
-
63
- # SECURITY: Validate agent name format (alphanumeric, dash, underscore only)
64
- if ! [[ "$DECODED_AGENT" =~ ^[a-zA-Z0-9_-]+$ ]]; then
65
- echo "⚠️ Invalid agent name format, using 'default'" >&2
66
- DECODED_AGENT="default"
60
+ # SECURITY: Validate base64 format before decoding
61
+ [[ "$ENCODED_AGENT" =~ ^[A-Za-z0-9+/=]+$ ]] || ENCODED_AGENT=""
62
+ if [[ -n "$ENCODED_AGENT" ]]; then
63
+ DECODED_AGENT=$(echo -n "$ENCODED_AGENT" | base64 -d 2>/dev/null) || DECODED_AGENT="default"
67
64
  fi
68
-
65
+
66
+ # SECURITY: Validate agent name format (alphanumeric, dash, underscore only)
67
+ [[ "$DECODED_AGENT" =~ ^[a-zA-Z0-9_-]+$ ]] || DECODED_AGENT="default"
68
+
69
69
  # SECURITY: Enforce length limit on agent name
70
- if [[ ${#DECODED_AGENT} -gt 50 ]]; then
71
- echo "⚠️ Agent name too long, using 'default'" >&2
72
- DECODED_AGENT="default"
73
- fi
70
+ [[ ${#DECODED_AGENT} -le 50 ]] || DECODED_AGENT="default"
74
71
  fi
75
72
 
76
73
  # Decode and prepend intro if provided
77
74
  DECODED_INTRO=""
78
75
  if [[ -n "$ENCODED_INTRO" ]]; then
79
- DECODED_INTRO=$(echo -n "$ENCODED_INTRO" | base64 -d 2>/dev/null) || DECODED_INTRO=""
80
-
81
- # SECURITY: Enforce length limit on intro message
82
- if [[ ${#DECODED_INTRO} -gt 200 ]]; then
83
- echo "⚠️ Intro message too long, truncating" >&2
84
- DECODED_INTRO="${DECODED_INTRO:0:200}"
76
+ # SECURITY: Validate base64 format before decoding
77
+ [[ "$ENCODED_INTRO" =~ ^[A-Za-z0-9+/=]+$ ]] || ENCODED_INTRO=""
78
+ if [[ -n "$ENCODED_INTRO" ]]; then
79
+ DECODED_INTRO=$(echo -n "$ENCODED_INTRO" | base64 -d 2>/dev/null) || DECODED_INTRO=""
85
80
  fi
81
+
82
+ # SECURITY: Enforce length limit on intro message
83
+ [[ ${#DECODED_INTRO} -le 200 ]] || DECODED_INTRO="${DECODED_INTRO:0:200}"
86
84
  fi
87
85
 
88
86
  # Prepend intro to text if configured
@@ -30,26 +30,50 @@ ENCODED_INTRO="${4:-}"
30
30
 
31
31
  # Validate inputs
32
32
  if [[ -z "$ENCODED_TEXT" ]]; then
33
- echo " No encoded text provided" >&2
33
+ echo "Error: No encoded text provided" >&2
34
34
  echo "Usage: $0 <base64_text> <voice> <base64_agent_name> [base64_intro]" >&2
35
35
  exit 1
36
36
  fi
37
37
 
38
+ # SECURITY: Validate base64 format (reject shell metacharacters)
39
+ [[ "$ENCODED_TEXT" =~ ^[A-Za-z0-9+/=]+$ ]] || { echo "Error: invalid base64 text" >&2; exit 1; }
40
+
41
+ # SECURITY: Validate voice name format (alphanumeric, hyphens, underscores only)
42
+ [[ "$VOICE" =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "Error: invalid voice format" >&2; exit 1; }
43
+
38
44
  # SECURITY: Decode base64 safely
39
45
  DECODED_TEXT=$(echo -n "$ENCODED_TEXT" | base64 -d 2>/dev/null) || {
40
- echo " Failed to decode text (invalid base64)" >&2
46
+ echo "Error: Failed to decode text (invalid base64)" >&2
41
47
  exit 1
42
48
  }
43
49
 
50
+ # SECURITY: Enforce length limit on decoded text (10KB max)
51
+ if [[ ${#DECODED_TEXT} -gt 10000 ]]; then
52
+ echo "Error: Decoded text too long (${#DECODED_TEXT} chars, max 10000)" >&2
53
+ exit 1
54
+ fi
55
+
44
56
  DECODED_AGENT="default"
45
57
  if [[ -n "$ENCODED_AGENT" ]]; then
46
- DECODED_AGENT=$(echo -n "$ENCODED_AGENT" | base64 -d 2>/dev/null) || DECODED_AGENT="default"
58
+ [[ "$ENCODED_AGENT" =~ ^[A-Za-z0-9+/=]+$ ]] || ENCODED_AGENT=""
59
+ if [[ -n "$ENCODED_AGENT" ]]; then
60
+ DECODED_AGENT=$(echo -n "$ENCODED_AGENT" | base64 -d 2>/dev/null) || DECODED_AGENT="default"
61
+ fi
62
+ # Validate agent name format (alphanumeric, dash, underscore only)
63
+ [[ "$DECODED_AGENT" =~ ^[a-zA-Z0-9_-]+$ ]] || DECODED_AGENT="default"
64
+ # Enforce length limit
65
+ [[ ${#DECODED_AGENT} -le 50 ]] || DECODED_AGENT="default"
47
66
  fi
48
67
 
49
68
  # Decode and prepend intro if provided
50
69
  DECODED_INTRO=""
51
70
  if [[ -n "$ENCODED_INTRO" ]]; then
52
- DECODED_INTRO=$(echo -n "$ENCODED_INTRO" | base64 -d 2>/dev/null) || DECODED_INTRO=""
71
+ [[ "$ENCODED_INTRO" =~ ^[A-Za-z0-9+/=]+$ ]] || ENCODED_INTRO=""
72
+ if [[ -n "$ENCODED_INTRO" ]]; then
73
+ DECODED_INTRO=$(echo -n "$ENCODED_INTRO" | base64 -d 2>/dev/null) || DECODED_INTRO=""
74
+ fi
75
+ # Enforce length limit on intro (200 chars max)
76
+ [[ ${#DECODED_INTRO} -le 200 ]] || DECODED_INTRO="${DECODED_INTRO:0:200}"
53
77
  fi
54
78
 
55
79
  # Prepend intro to text if configured
@@ -42,7 +42,7 @@
42
42
 
43
43
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
44
44
 
45
- if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then
45
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && [[ -d "${CLAUDE_PROJECT_DIR:-}/.claude" ]]; then
46
46
  # MCP context: Use the project directory where MCP was invoked
47
47
  CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude"
48
48
  else
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/path-resolver.sh
4
+ #
5
+ # AgentVibes Path Resolver Utility - Robust path resolution for all hooks
6
+ # Handles: symlinks, working directory changes, non-standard installations
7
+ #
8
+ # Usage in other scripts:
9
+ # source "$(dirname "${BASH_SOURCE[0]}")/path-resolver.sh"
10
+ # # Now use: $PROJECT_ROOT, $HOOKS_DIR, $SCRIPT_DIR
11
+ #
12
+
13
+ set -euo pipefail
14
+
15
+ # Resolve the actual script location (handles symlinks)
16
+ # This function must be called from the sourcing script
17
+ _resolve_agentvibes_paths() {
18
+ local calling_script="$1"
19
+
20
+ # Get real path (resolve symlinks)
21
+ local script_path
22
+ if command -v readlink &>/dev/null; then
23
+ script_path="$(readlink -f "$calling_script")"
24
+ else
25
+ # Fallback for systems without readlink -f
26
+ script_path="$(cd "$(dirname "$calling_script")" && pwd)/$(basename "$calling_script")"
27
+ fi
28
+
29
+ local script_dir="$(dirname "$script_path")"
30
+
31
+ # Find PROJECT_ROOT by searching up for .claude/hooks directory
32
+ # This is resilient to non-standard installations
33
+ local current_dir="$script_dir"
34
+ local project_root=""
35
+
36
+ while [[ "$current_dir" != "/" ]]; do
37
+ if [[ -d "$current_dir/.claude/hooks" ]]; then
38
+ # Found .claude/hooks - PROJECT_ROOT is 2 levels up
39
+ project_root="$(dirname "$(dirname "$current_dir")")"
40
+ break
41
+ fi
42
+ current_dir="$(dirname "$current_dir")"
43
+ done
44
+
45
+ # Validate we found a valid project root
46
+ if [[ -z "$project_root" ]] || [[ ! -d "$project_root/.claude/hooks" ]]; then
47
+ echo "❌ ERROR: Could not locate AgentVibes installation" >&2
48
+ echo " Script: $script_path" >&2
49
+ return 1
50
+ fi
51
+
52
+ # Export paths for use in sourcing script
53
+ export SCRIPT_PATH="$script_path"
54
+ export SCRIPT_DIR="$script_dir"
55
+ export HOOKS_DIR="$project_root/.claude/hooks"
56
+ export PROJECT_ROOT="$project_root"
57
+ }
58
+
59
+ # Call the resolver with the calling script
60
+ _resolve_agentvibes_paths "${BASH_SOURCE[1]}"
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh
4
+ #
5
+ # AgentVibes - AgentVibes Receiver Provider (for voiceless connections)
6
+ # Sends text to a remote device via SSH for local AgentVibes playback.
7
+ # Use this when the AI agent runs on a server/headless machine that has no
8
+ # audio output — the remote device (laptop, phone, etc.) plays the audio.
9
+ #
10
+ # Copyright (c) 2025 Paul Preibisch
11
+ # Licensed under the Apache License, Version 2.0
12
+ #
13
+
14
+ set -euo pipefail
15
+
16
+ TEXT="${1:-}"
17
+ VOICE="${2:-en_US-lessac-medium}"
18
+ AGENT_NAME="${3:-default}"
19
+
20
+ # Validate required input
21
+ if [[ -z "$TEXT" ]]; then
22
+ echo "❌ No text provided" >&2
23
+ echo "Usage: $0 <text> [voice] [agent_name]" >&2
24
+ exit 1
25
+ fi
26
+
27
+ # Get script directory
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
30
+
31
+ # Get SSH host from config
32
+ SSH_HOST=$(cat "$PROJECT_ROOT/.claude/agentvibes-receiver-host.txt" 2>/dev/null || \
33
+ cat "$HOME/.claude/agentvibes-receiver-host.txt" 2>/dev/null || echo "")
34
+
35
+ if [[ -z "$SSH_HOST" ]]; then
36
+ echo "❌ AgentVibes Receiver host not configured" >&2
37
+ echo "💡 Set host: echo 'your-device' > ~/.claude/agentvibes-receiver-host.txt" >&2
38
+ exit 1
39
+ fi
40
+
41
+ # SECURITY: Validate SSH_HOST to prevent option injection
42
+ # Must be a valid hostname, IP address, or SSH config alias (alphanumeric, dots, hyphens, underscores)
43
+ if [[ ! "$SSH_HOST" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
44
+ echo "❌ Invalid SSH host format: $SSH_HOST" >&2
45
+ echo "💡 Host must be alphanumeric (may contain dots, hyphens, underscores)" >&2
46
+ exit 1
47
+ fi
48
+
49
+ # SECURITY: Reject hosts starting with hyphen (SSH option injection)
50
+ if [[ "$SSH_HOST" == -* ]]; then
51
+ echo "❌ Invalid SSH host: cannot start with hyphen" >&2
52
+ exit 1
53
+ fi
54
+
55
+ # SECURITY: Validate VOICE to prevent injection (alphanumeric, hyphens, underscores only)
56
+ if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then
57
+ echo "❌ Invalid voice format: $VOICE" >&2
58
+ exit 1
59
+ fi
60
+
61
+ # SECURITY: Validate AGENT_NAME to prevent injection (alphanumeric, hyphens, underscores, spaces only)
62
+ if [[ ! "$AGENT_NAME" =~ ^[a-zA-Z0-9_\ -]+$ ]]; then
63
+ echo "❌ Invalid agent name format: $AGENT_NAME" >&2
64
+ exit 1
65
+ fi
66
+
67
+ # SECURITY: Encode text and agent name as base64 to prevent command injection
68
+ # The receiver will decode these safely
69
+ ENCODED_TEXT=$(printf '%s' "$TEXT" | base64 -w 0)
70
+ ENCODED_AGENT=$(printf '%s' "$AGENT_NAME" | base64 -w 0)
71
+
72
+ # Send text to remote for local AgentVibes playback
73
+ echo "📱 Sending to $SSH_HOST for local playback..." >&2
74
+
75
+ # Try receiver scripts in order — single SSH call, no separate probe
76
+ # SECURITY: Base64-encoded values are safe to pass as arguments (no shell metacharacters)
77
+ ssh "$SSH_HOST" "
78
+ if [ -f ~/.agentvibes/play-remote.sh ]; then
79
+ bash ~/.agentvibes/play-remote.sh '$ENCODED_TEXT' '$VOICE' '$ENCODED_AGENT'
80
+ elif [ -f ~/.termux/agentvibes-play.sh ]; then
81
+ bash ~/.termux/agentvibes-play.sh '$ENCODED_TEXT' '$VOICE' '$ENCODED_AGENT'
82
+ else
83
+ echo 'Error: Receiver script not found' >&2
84
+ exit 1
85
+ fi
86
+ " &
87
+ SSH_PID=$!
88
+
89
+ echo "Sent to $SSH_HOST (PID: $SSH_PID)" >&2
90
+ exit 0