agentvibes 3.5.9 → 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 (71) 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 -10
  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 +11 -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 +1353 -676
  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/provider-validator.js +144 -132
  60. package/src/utils/secure-music-storage.js +412 -0
  61. package/templates/agentvibes-receiver.sh +11 -7
  62. package/voice-assignments.json +8245 -0
  63. package/.claude/config/background-music-volume.txt +0 -1
  64. package/.claude/config/background-music.cfg +0 -1
  65. package/.claude/config/background-music.txt +0 -1
  66. package/.claude/config/tts-speech-rate.txt +0 -1
  67. package/.claude/config/tts-verbosity.txt +0 -1
  68. package/.claude/hooks/bmad-party-manager.sh +0 -225
  69. package/.claude/hooks/stop.sh +0 -38
  70. package/.claude/piper-voices-dir.txt +0 -1
  71. package/.mcp.json +0 -34
@@ -35,11 +35,21 @@
35
35
  # @related play-tts.sh, piper-voice-manager.sh, language-manager.sh, GitHub Issue #25
36
36
  #
37
37
 
38
+ set -eo pipefail
39
+ # Note: -u (nounset) omitted because sourced scripts (piper-voice-manager.sh,
40
+ # language-manager.sh, audio-cache-utils.sh) use unset variables freely.
41
+ # Variables in THIS script use ${VAR:-} defaults for safety.
42
+
43
+ # Cleanup handler for temp files
44
+ _CLEANUP_FILES=()
45
+ cleanup() { rm -f "${_CLEANUP_FILES[@]+"${_CLEANUP_FILES[@]}"}"; }
46
+ trap cleanup EXIT
47
+
38
48
  # Fix locale warnings
39
49
  export LC_ALL=C
40
50
 
41
- TEXT="$1"
42
- VOICE_OVERRIDE="$2" # Optional: voice model name
51
+ TEXT="${1:-}"
52
+ VOICE_OVERRIDE="${2:-}" # Optional: voice model name
43
53
 
44
54
  # Source voice manager and language manager
45
55
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -74,7 +84,7 @@ else
74
84
  # 2. Script location (for direct slash command usage)
75
85
  # 3. Global ~/.claude (fallback)
76
86
 
77
- if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -f "$CLAUDE_PROJECT_DIR/.claude/tts-voice.txt" ]]; then
87
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && [[ "${CLAUDE_PROJECT_DIR:-}" != *".."* ]] && [[ -f "$CLAUDE_PROJECT_DIR/.claude/tts-voice.txt" ]]; then
78
88
  # MCP context: Use the project directory where MCP was invoked
79
89
  VOICE_FILE="$CLAUDE_PROJECT_DIR/.claude/tts-voice.txt"
80
90
  elif [[ -f "$SCRIPT_DIR/../tts-voice.txt" ]]; then
@@ -95,13 +105,32 @@ else
95
105
  SPEAKER_ID_FILE="$VOICE_DIR/tts-piper-speaker-id.txt"
96
106
 
97
107
  if [[ -f "$MODEL_FILE" ]] && [[ -f "$SPEAKER_ID_FILE" ]]; then
98
- # Multi-speaker voice
108
+ # Multi-speaker voice config found locally
99
109
  VOICE_MODEL=$(cat "$MODEL_FILE" 2>/dev/null)
100
110
  SPEAKER_ID=$(cat "$SPEAKER_ID_FILE" 2>/dev/null)
101
- echo "🎭 Using multi-speaker voice: $FILE_VOICE (Model: $VOICE_MODEL, Speaker ID: $SPEAKER_ID)"
102
- # Check if it's a standard Piper model name or custom voice (just use as-is)
111
+ # Validate speaker ID is numeric
112
+ if [[ -n "$SPEAKER_ID" ]] && ! [[ "$SPEAKER_ID" =~ ^[0-9]+$ ]]; then
113
+ echo "Warning: Invalid speaker ID '$SPEAKER_ID', ignoring" >&2
114
+ SPEAKER_ID=""
115
+ fi
116
+ echo "🎭 Using multi-speaker voice: $FILE_VOICE (Model: $VOICE_MODEL, Speaker ID: ${SPEAKER_ID:-none})"
117
+ # Fallback: check global ~/.claude/ for multi-speaker config files
118
+ elif [[ -f "$HOME/.claude/tts-piper-model.txt" ]] && [[ -f "$HOME/.claude/tts-piper-speaker-id.txt" ]]; then
119
+ VOICE_MODEL=$(cat "$HOME/.claude/tts-piper-model.txt" 2>/dev/null)
120
+ SPEAKER_ID=$(cat "$HOME/.claude/tts-piper-speaker-id.txt" 2>/dev/null)
121
+ if [[ -n "$SPEAKER_ID" ]] && ! [[ "$SPEAKER_ID" =~ ^[0-9]+$ ]]; then
122
+ echo "Warning: Invalid speaker ID '$SPEAKER_ID', ignoring" >&2
123
+ SPEAKER_ID=""
124
+ fi
125
+ echo "🎭 Using multi-speaker voice (global): $FILE_VOICE (Model: $VOICE_MODEL, Speaker ID: ${SPEAKER_ID:-none})"
126
+ # Single-speaker voice or custom voice name
103
127
  elif [[ -n "$FILE_VOICE" ]]; then
104
- VOICE_MODEL="$FILE_VOICE"
128
+ # Strip multi-speaker suffix if present (model::SpeakerName-Label)
129
+ if [[ "$FILE_VOICE" == *"::"* ]]; then
130
+ VOICE_MODEL="${FILE_VOICE%%::*}"
131
+ else
132
+ VOICE_MODEL="$FILE_VOICE"
133
+ fi
105
134
  fi
106
135
  fi
107
136
 
@@ -178,7 +207,7 @@ fi
178
207
  # @intent Find appropriate directory for audio file storage
179
208
  # @why Supports project-local and global storage
180
209
  # @returns Sets $AUDIO_DIR global variable
181
- if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then
210
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
182
211
  AUDIO_DIR="$CLAUDE_PROJECT_DIR/.claude/audio"
183
212
  else
184
213
  # Fallback: try to find .claude directory in current path
@@ -191,13 +220,13 @@ else
191
220
  CURRENT_DIR=$(dirname "$CURRENT_DIR")
192
221
  done
193
222
  # Final fallback to global if no project .claude found
194
- if [[ -z "$AUDIO_DIR" ]]; then
223
+ if [[ -z "${AUDIO_DIR:-}" ]]; then
195
224
  AUDIO_DIR="$HOME/.claude/audio"
196
225
  fi
197
226
  fi
198
227
 
199
228
  mkdir -p "$AUDIO_DIR"
200
- TEMP_FILE="$AUDIO_DIR/tts-$(date +%s).wav"
229
+ TEMP_FILE=$(mktemp "$AUDIO_DIR/tts-XXXXXX.wav")
201
230
 
202
231
  # @function get_speech_rate
203
232
  # @intent Determine speech rate for Piper synthesis
@@ -233,6 +262,11 @@ get_speech_rate() {
233
262
  # If this is a non-English voice and target config exists, use it
234
263
  if [[ "$CURRENT_LANGUAGE" != "english" ]] && [[ -n "$target_config" ]]; then
235
264
  local user_speed=$(cat "$target_config" 2>/dev/null)
265
+ # Validate speed is a positive number
266
+ if ! [[ "$user_speed" =~ ^[0-9]*\.?[0-9]+$ ]] || [[ "$user_speed" == "0" ]] || [[ "$user_speed" == "0.0" ]]; then
267
+ echo "1.0"
268
+ return
269
+ fi
236
270
  # Convert user speed to Piper length-scale (invert)
237
271
  # User: 0.5=slower, 1.0=normal, 2.0=faster
238
272
  # Piper: 2.0=slower, 1.0=normal, 0.5=faster
@@ -244,6 +278,11 @@ get_speech_rate() {
244
278
  # Otherwise use main config if available
245
279
  if [[ -n "$main_config" ]]; then
246
280
  local user_speed=$(grep -v '^#' "$main_config" 2>/dev/null | grep -v '^$' | tail -1)
281
+ # Validate speed is a positive number
282
+ if ! [[ "$user_speed" =~ ^[0-9]*\.?[0-9]+$ ]] || [[ "$user_speed" == "0" ]] || [[ "$user_speed" == "0.0" ]]; then
283
+ echo "1.0"
284
+ return
285
+ fi
247
286
  echo "scale=2; 1.0 / $user_speed" | bc -l 2>/dev/null || echo "1.0"
248
287
  return
249
288
  fi
@@ -266,7 +305,7 @@ SPEECH_RATE=$(get_speech_rate)
266
305
  # @exitcode 0=success, 4=synthesis error
267
306
  # @sideeffects Creates audio file
268
307
  # @edgecases Handles piper errors, invalid models, multi-speaker voices
269
- if [[ -n "$SPEAKER_ID" ]]; then
308
+ if [[ -n "${SPEAKER_ID:-}" ]]; then
270
309
  # Multi-speaker voice: Pass speaker ID
271
310
  # Add 2-second pause between sentences for better pacing
272
311
  echo "$TEXT" | piper --model "$VOICE_PATH" --speaker "$SPEAKER_ID" --length-scale "$SPEECH_RATE" --sentence-silence 2.0 --output_file "$TEMP_FILE" 2>/dev/null
@@ -303,7 +342,8 @@ fi
303
342
  # @returns Updates $TEMP_FILE to compressed version
304
343
  # @sideeffects Converts to mono 22kHz for lower bandwidth
305
344
  if [[ "${AGENTVIBES_RDP_MODE:-false}" == "true" ]] && command -v ffmpeg &> /dev/null; then
306
- COMPRESSED_FILE="$AUDIO_DIR/tts-compressed-$(date +%s).wav"
345
+ COMPRESSED_FILE=$(mktemp "$AUDIO_DIR/tts-compressed-XXXXXX.wav")
346
+ _CLEANUP_FILES+=("$COMPRESSED_FILE")
307
347
  # Convert to mono, 22kHz, 64kbps for remote sessions
308
348
  ffmpeg -i "$TEMP_FILE" -ac 1 -ar 22050 -b:a 64k -y "$COMPRESSED_FILE" 2>/dev/null
309
349
 
@@ -321,7 +361,8 @@ fi
321
361
  # @sideeffects Modifies audio file
322
362
  # AI NOTE: Use ffmpeg if available, otherwise skip padding (degraded experience)
323
363
  if command -v ffmpeg &> /dev/null; then
324
- PADDED_FILE="$AUDIO_DIR/tts-padded-$(date +%s).wav"
364
+ PADDED_FILE=$(mktemp "$AUDIO_DIR/tts-padded-XXXXXX.wav")
365
+ _CLEANUP_FILES+=("$PADDED_FILE")
325
366
  # Add 200ms of silence at the beginning
326
367
  ffmpeg -f lavfi -i anullsrc=r=44100:cl=stereo:d=0.2 -i "$TEMP_FILE" \
327
368
  -filter_complex "[0:a][1:a]concat=n=2:v=0:a=1[out]" \
@@ -340,7 +381,8 @@ fi
340
381
  # @sideeffects Applies audio effects and background music
341
382
  BACKGROUND_MUSIC=""
342
383
  if [[ -f "$SCRIPT_DIR/audio-processor.sh" ]]; then
343
- PROCESSED_FILE="$AUDIO_DIR/tts-processed-$(date +%s).wav"
384
+ PROCESSED_FILE=$(mktemp "$AUDIO_DIR/tts-processed-XXXXXX.wav")
385
+ _CLEANUP_FILES+=("$PROCESSED_FILE")
344
386
  # audio-processor.sh returns: FILE_PATH|BACKGROUND_FILE
345
387
  PROCESSOR_OUTPUT=$("$SCRIPT_DIR/audio-processor.sh" "$TEMP_FILE" "default" "$PROCESSED_FILE" 2>/dev/null) || {
346
388
  echo "Warning: Audio processing failed, using unprocessed audio" >&2
@@ -363,7 +405,10 @@ fi
363
405
  # @why Support multiple audio players and prevent overlapping audio in learning mode
364
406
  # @param Uses global: $TEMP_FILE, $CURRENT_LANGUAGE
365
407
  # @sideeffects Plays audio with lock mechanism for sequential playback
366
- LOCK_FILE="/tmp/agentvibes-audio.lock"
408
+ _LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp/agentvibes-$(id -u)}"
409
+ mkdir -p "$_LOCK_DIR"
410
+ chmod 700 "$_LOCK_DIR"
411
+ LOCK_FILE="$_LOCK_DIR/agentvibes-audio.lock"
367
412
 
368
413
  # Wait for previous audio to finish (max 2 seconds to prevent blocking)
369
414
  for i in {1..4}; do
@@ -381,7 +426,7 @@ fi
381
426
 
382
427
  # Track last target language audio for replay command
383
428
  if [[ "$CURRENT_LANGUAGE" != "english" ]]; then
384
- TARGET_AUDIO_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/last-target-audio.txt"
429
+ TARGET_AUDIO_FILE="${CLAUDE_PROJECT_DIR:-${HOME}}/.claude/last-target-audio.txt"
385
430
  echo "$TEMP_FILE" > "$TARGET_AUDIO_FILE"
386
431
  fi
387
432
 
@@ -389,12 +434,13 @@ fi
389
434
  touch "$LOCK_FILE"
390
435
 
391
436
  # Create write lock file in audio directory to signal file is in-use (prevents race condition in cleanup)
392
- AUDIO_DIR="${TEMP_FILE%/*}"
393
- WRITE_LOCK_FILE="$AUDIO_DIR/$(basename "$TEMP_FILE" .wav).lock"
437
+ _TEMP_DIR="${TEMP_FILE%/*}"
438
+ WRITE_LOCK_FILE="$_TEMP_DIR/$(basename "$TEMP_FILE" .wav).lock"
394
439
  touch "$WRITE_LOCK_FILE"
440
+ _CLEANUP_FILES+=("$LOCK_FILE" "$WRITE_LOCK_FILE")
395
441
 
396
442
  # Get audio duration for proper lock timing
397
- DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$TEMP_FILE" 2>/dev/null)
443
+ DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$TEMP_FILE" 2>/dev/null || true)
398
444
  DURATION=${DURATION%.*} # Round to integer
399
445
  DURATION=${DURATION:-1} # Default to 1 second if detection fails
400
446
 
@@ -473,10 +519,15 @@ if [[ -n "$BACKGROUND_MUSIC" ]]; then
473
519
  MUSIC_FILENAME=$(basename "$BACKGROUND_MUSIC")
474
520
  echo -e "${WHITE}🎵 Background music:${NC} ${PURPLE}$MUSIC_FILENAME${NC}"
475
521
  fi
476
- echo -e "${WHITE}🎤 Voice used:${NC} ${BLUE}$VOICE_MODEL${NC} ${WHITE}(Piper TTS)${NC}"
522
+ # Show speaker name for multi-speaker voices, otherwise show model name
523
+ if [[ -n "${SPEAKER_ID:-}" ]] && [[ -n "${FILE_VOICE:-}" ]]; then
524
+ echo -e "${WHITE}🎤 Voice used:${NC} ${BLUE}$FILE_VOICE${NC} ${WHITE}(Piper TTS)${NC}"
525
+ else
526
+ echo -e "${WHITE}🎤 Voice used:${NC} ${BLUE}$VOICE_MODEL${NC} ${WHITE}(Piper TTS)${NC}"
527
+ fi
477
528
 
478
529
  # Show personality if configured
479
- PERSONALITY=$(cat "$PROJECT_ROOT/.claude/tts-personality.txt" 2>/dev/null || cat "$HOME/.claude/tts-personality.txt" 2>/dev/null || echo "")
530
+ PERSONALITY=$(cat "${PROJECT_ROOT:-/nonexistent}/.claude/tts-personality.txt" 2>/dev/null || cat "$HOME/.claude/tts-personality.txt" 2>/dev/null || echo "")
480
531
  if [[ -n "$PERSONALITY" ]] && [[ "$PERSONALITY" != "none" ]] && [[ "$PERSONALITY" != "normal" ]]; then
481
532
  echo -e "${WHITE}💫 Personality:${NC} ${YELLOW}$PERSONALITY${NC}"
482
533
  fi
@@ -491,9 +542,10 @@ fi
491
542
 
492
543
  # Show status indicators
493
544
  GLOBAL_MUTE_FILE="$HOME/.agentvibes-muted"
494
- PROJECT_MUTE_FILE="$PROJECT_ROOT/.claude/agentvibes-muted"
495
- PROJECT_UNMUTE_FILE="$PROJECT_ROOT/.claude/agentvibes-unmuted"
496
- BACKGROUND_ENABLED_FILE="$PROJECT_ROOT/.claude/config/background-music-enabled.txt"
545
+ PROJECT_MUTE_FILE="${PROJECT_ROOT:-/nonexistent}/.claude/agentvibes-muted"
546
+ PROJECT_UNMUTE_FILE="${PROJECT_ROOT:-/nonexistent}/.claude/agentvibes-unmuted"
547
+ BACKGROUND_ENABLED_FILE="${PROJECT_ROOT:-/nonexistent}/.claude/config/background-music-enabled.txt"
548
+ GLOBAL_BACKGROUND_ENABLED_FILE="$HOME/.claude/config/background-music-enabled.txt"
497
549
 
498
550
  # Mute status indicator
499
551
  if [[ -f "$PROJECT_UNMUTE_FILE" ]] && [[ -f "$GLOBAL_MUTE_FILE" ]]; then
@@ -506,7 +558,13 @@ fi
506
558
 
507
559
  # Background music status indicator
508
560
  if [[ -z "$BACKGROUND_MUSIC" ]]; then
561
+ _bg_enabled=false
509
562
  if [[ -f "$BACKGROUND_ENABLED_FILE" ]] && grep -q "true" "$BACKGROUND_ENABLED_FILE" 2>/dev/null; then
563
+ _bg_enabled=true
564
+ elif [[ -f "$GLOBAL_BACKGROUND_ENABLED_FILE" ]] && grep -q "true" "$GLOBAL_BACKGROUND_ENABLED_FILE" 2>/dev/null; then
565
+ _bg_enabled=true
566
+ fi
567
+ if [[ "$_bg_enabled" == "true" ]]; then
510
568
  echo -e "${WHITE}🎵 Background music:${NC} ${PURPLE}Enabled but not playing (check config)${NC}"
511
569
  else
512
570
  echo -e "${WHITE}🎵 Background music:${NC} ${PURPLE}Disabled${NC}"
@@ -70,21 +70,19 @@ ENCODED_AGENT=$(printf '%s' "$AGENT_NAME" | base64 -w 0)
70
70
  # Send text to remote for local AgentVibes playback
71
71
  echo "📱 Sending to $SSH_HOST for local playback..." >&2
72
72
 
73
- # Determine which receiver script exists and send encoded text, voice, and agent name
73
+ # Try receiver scripts in order single SSH call, no separate probe
74
74
  # SECURITY: Base64-encoded values are safe to pass as arguments (no shell metacharacters)
75
- # The receiver auto-detects and decodes base64 input
76
- if ssh "$SSH_HOST" "test -f ~/.termux/agentvibes-play.sh" 2>/dev/null; then
77
- ssh "$SSH_HOST" "bash ~/.termux/agentvibes-play.sh '$ENCODED_TEXT' '$VOICE' '$ENCODED_AGENT'" &
78
- SSH_PID=$!
79
- elif ssh "$SSH_HOST" "test -f ~/.agentvibes/play-remote.sh" 2>/dev/null; then
80
- ssh "$SSH_HOST" "bash ~/.agentvibes/play-remote.sh '$ENCODED_TEXT' '$VOICE' '$ENCODED_AGENT'" &
81
- SSH_PID=$!
82
- else
83
- echo "⚠️ Receiver script not found on $SSH_HOST" >&2
84
- echo "💡 Install: agentvibes install --ssh-receiver" >&2
85
- exit 1
86
- fi
75
+ ssh "$SSH_HOST" "
76
+ if [ -f ~/.agentvibes/play-remote.sh ]; then
77
+ bash ~/.agentvibes/play-remote.sh '$ENCODED_TEXT' '$VOICE' '$ENCODED_AGENT'
78
+ elif [ -f ~/.termux/agentvibes-play.sh ]; then
79
+ bash ~/.termux/agentvibes-play.sh '$ENCODED_TEXT' '$VOICE' '$ENCODED_AGENT'
80
+ else
81
+ echo 'Error: Receiver script not found' >&2
82
+ exit 1
83
+ fi
84
+ " &
85
+ SSH_PID=$!
87
86
 
88
- # Log the background PID for debugging (non-blocking)
89
- echo "✓ Sent to $SSH_HOST (PID: $SSH_PID, playing remotely)" >&2
87
+ echo "Sent to $SSH_HOST (PID: $SSH_PID)" >&2
90
88
  exit 0
@@ -66,14 +66,14 @@ if [[ -f "$PROJECT_UNMUTE_FILE" ]]; then
66
66
  elif [[ -f "$PROJECT_MUTE_FILE" ]]; then
67
67
  # Project explicitly muted
68
68
  if [[ -f "$GLOBAL_MUTE_FILE" ]]; then
69
- echo "🔇 TTS muted (project + global)"
69
+ echo "🔇 TTS muted (project + global)" >&2
70
70
  else
71
- echo "🔇 TTS muted (project)"
71
+ echo "🔇 TTS muted (project)" >&2
72
72
  fi
73
73
  exit 0
74
74
  elif [[ -f "$GLOBAL_MUTE_FILE" ]]; then
75
75
  # Global mute and no project-level override
76
- echo "🔇 TTS muted (global)"
76
+ echo "🔇 TTS muted (global)" >&2
77
77
  exit 0
78
78
  fi
79
79
 
@@ -101,6 +101,17 @@ TEXT="${TEXT//\\,/,}" # Remove \,
101
101
  TEXT="${TEXT//\\./.}" # Remove \. (keep the period)
102
102
  TEXT="${TEXT//\\\\/\\}" # Remove \\ (escaped backslash)
103
103
 
104
+ # Prepend intro text (pretext) if configured
105
+ # Check project-local first, then global
106
+ _PRETEXT_FILE="$PROJECT_ROOT/.claude/config/intro-text.txt"
107
+ [[ -f "$_PRETEXT_FILE" ]] || _PRETEXT_FILE="$HOME/.claude/config/intro-text.txt"
108
+ if [[ -f "$_PRETEXT_FILE" ]]; then
109
+ _PRETEXT="$(head -1 "$_PRETEXT_FILE" 2>/dev/null || true)"
110
+ if [[ -n "$_PRETEXT" ]]; then
111
+ TEXT="${_PRETEXT} ${TEXT}"
112
+ fi
113
+ fi
114
+
104
115
  # Source provider manager to get active provider
105
116
  source "$SCRIPT_DIR/provider-manager.sh"
106
117
 
@@ -280,8 +291,8 @@ case "$ACTIVE_PROVIDER" in
280
291
  exec "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$TEXT" "$VOICE_OVERRIDE"
281
292
  ;;
282
293
  *)
283
- echo "❌ Unknown provider: $ACTIVE_PROVIDER"
284
- echo " Run: /agent-vibes:provider list"
294
+ echo "❌ Unknown provider: $ACTIVE_PROVIDER" >&2
295
+ echo " Run: /agent-vibes:provider list" >&2
285
296
  exit 1
286
297
  ;;
287
298
  esac
@@ -4,7 +4,7 @@ set -euo pipefail
4
4
  # File: .claude/hooks/session-start-tts.sh
5
5
  #
6
6
  # AgentVibes SessionStart Hook - Optimized (Issue #80, Phase 1)
7
- # Token target: ~250 (down from ~500)
7
+ # Outputs JSON with additionalContext for reliable context injection
8
8
  #
9
9
 
10
10
  # Fix locale warnings
@@ -33,69 +33,39 @@ fi
33
33
  # Get verbosity level (Issue #32)
34
34
  VERBOSITY=$(cat .claude/tts-verbosity.txt 2>/dev/null || cat ~/.claude/tts-verbosity.txt 2>/dev/null || echo "low")
35
35
 
36
- # Output optimized TTS protocol
37
- cat <<'EOF'
36
+ # Build context string
37
+ CONTEXT="# AgentVibes TTS Protocol\n\n"
38
+ CONTEXT+="**CRITICAL: You MUST execute TTS at TWO points for EVERY user interaction:**\n\n"
39
+ CONTEXT+="1. **Acknowledgment** - Start of task: \`Bash: .claude/hooks/play-tts.sh \"[action]\"\`\n"
40
+ CONTEXT+="2. **Completion** - End of task: \`Bash: .claude/hooks/play-tts.sh \"[result + key details]\"\`\n\n"
41
+ CONTEXT+="**Example:**\n\`\`\`\n[Bash: .claude/hooks/play-tts.sh \"Checking git status\"]\n[work...]\n"
42
+ CONTEXT+="[Bash: .claude/hooks/play-tts.sh \"Repository is clean, no changes\"]\n\`\`\`\n\n"
38
43
 
39
- # AgentVibes TTS Protocol
40
-
41
- **CRITICAL: You MUST execute TTS at TWO points for EVERY user interaction:**
42
-
43
- 1. **Acknowledgment** - Start of task: `Bash: .claude/hooks/play-tts.sh "[action]"`
44
- 2. **Completion** - End of task: `Bash: .claude/hooks/play-tts.sh "[result + key details]"`
45
-
46
- **Example:**
47
- ```
48
- [Bash: .claude/hooks/play-tts.sh "Checking git status"]
49
- [work...]
50
- [Bash: .claude/hooks/play-tts.sh "Repository is clean, no changes"]
51
- ```
52
-
53
- EOF
54
-
55
- # Add verbosity-specific protocol (Issue #32)
44
+ # Add verbosity-specific protocol
56
45
  case "$VERBOSITY" in
57
46
  low)
58
- cat <<'EOF'
59
- ## Verbosity: LOW
60
- - Acknowledgment: Action only
61
- - Completion: Result + errors only
62
- - Skip: Reasoning, decisions
63
-
64
- EOF
47
+ CONTEXT+="## Verbosity: LOW\n- Acknowledgment: Action only\n- Completion: Result + errors only\n- Skip: Reasoning, decisions\n\n"
65
48
  ;;
66
-
67
49
  medium)
68
- cat <<'EOF'
69
- ## Verbosity: MEDIUM
70
- - Acknowledgment: Action + key approach
71
- - Completion: Result + important decisions
72
- - Include: Major choices only
73
-
74
- EOF
50
+ CONTEXT+="## Verbosity: MEDIUM\n- Acknowledgment: Action + key approach\n- Completion: Result + important decisions\n- Include: Major choices only\n\n"
75
51
  ;;
76
-
77
52
  high)
78
- cat <<'EOF'
79
- ## Verbosity: HIGH
80
- - Acknowledgment: Action + approach + why
81
- - Completion: Result + decisions + trade-offs
82
- - Include: Full reasoning, alternatives
83
-
84
- EOF
53
+ CONTEXT+="## Verbosity: HIGH\n- Acknowledgment: Action + approach + why\n- Completion: Result + decisions + trade-offs\n- Include: Full reasoning, alternatives\n\n"
85
54
  ;;
86
55
  esac
87
56
 
88
57
  # Add style info and rules
89
- cat << EOF
90
- ## Style: $STYLE
91
-
92
- ## Rules
93
- 1. Never skip acknowledgment TTS
94
- 2. Never skip completion TTS
95
- 3. Match verbosity level
96
- 4. Keep under 150 chars
97
- 5. Always include errors
98
-
99
- Quick Ref: low=action+result | medium=+key decisions | high=+full reasoning
100
-
101
- EOF
58
+ CONTEXT+="## Style: $STYLE\n\n"
59
+ CONTEXT+="## Rules\n"
60
+ CONTEXT+="1. Never skip acknowledgment TTS\n"
61
+ CONTEXT+="2. Never skip completion TTS\n"
62
+ CONTEXT+="3. Match verbosity level\n"
63
+ CONTEXT+="4. Keep under 150 chars\n"
64
+ CONTEXT+="5. Always include errors\n\n"
65
+ CONTEXT+="Quick Ref: low=action+result | medium=+key decisions | high=+full reasoning"
66
+
67
+ # Escape for JSON (handle newlines, quotes, backslashes)
68
+ ESCAPED=$(printf '%s' "$CONTEXT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g')
69
+
70
+ # Output structured JSON for reliable context injection
71
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$ESCAPED"
@@ -76,7 +76,7 @@ def submit_request(base: str, payload: bytes) -> str:
76
76
  headers={"Content-Type": "application/json"},
77
77
  )
78
78
  try:
79
- with urllib.request.urlopen(req, timeout=30) as resp:
79
+ with urllib.request.urlopen(req, timeout=2) as resp:
80
80
  return json.loads(resp.read())["event_id"]
81
81
  except urllib.error.URLError:
82
82
  continue
@@ -44,7 +44,13 @@ export LC_ALL=C
44
44
 
45
45
  # Get script directory for accessing other scripts
46
46
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
47
- CLAUDE_DIR="$(dirname "$SCRIPT_DIR")"
47
+
48
+ # Respect CLAUDE_PROJECT_DIR when set by MCP (project context vs global installation)
49
+ if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then
50
+ CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude"
51
+ else
52
+ CLAUDE_DIR="$(dirname "$SCRIPT_DIR")"
53
+ fi
48
54
 
49
55
  # Config file locations
50
56
  VERBOSITY_FILE="$CLAUDE_DIR/tts-verbosity.txt"
@@ -53,10 +59,10 @@ GLOBAL_VERBOSITY_FILE="$HOME/.claude/tts-verbosity.txt"
53
59
  #
54
60
  # @function get_verbosity
55
61
  # @context Returns the current verbosity level (low/medium/high)
56
- # @architecture Checks project-local first, then global, then defaults to "low"
62
+ # @architecture Checks project-local first, then global, then defaults to "high"
57
63
  # @dependencies tts-verbosity.txt config file
58
64
  # @entrypoints Called by session-start hook, MCP tools, slash commands
59
- # @aiNotes Default to "low" for backward compatibility with existing installations
65
+ # @aiNotes Default to "high" to match installer default for new installations
60
66
  #
61
67
  get_verbosity() {
62
68
  if [[ -f "$VERBOSITY_FILE" ]]; then
@@ -64,7 +70,7 @@ get_verbosity() {
64
70
  elif [[ -f "$GLOBAL_VERBOSITY_FILE" ]]; then
65
71
  cat "$GLOBAL_VERBOSITY_FILE"
66
72
  else
67
- echo "low"
73
+ echo "high"
68
74
  fi
69
75
  }
70
76
 
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"$CLAUDE_PROJECT_DIR\\.claude\\hooks-windows\\session-start-tts.ps1\""
9
+ "command": "bash .claude/hooks/session-start-tts.sh"
10
10
  }
11
11
  ]
12
12
  }