Skip to main content
Glama

Voice Mode

by mbailey
hook.py•7.37 kB
""" Hook commands for Voice Mode - primarily for Claude Code integration. """ import click import sys import json import os from pathlib import Path from typing import Optional @click.group() @click.help_option('-h', '--help', help='Show this message and exit') def hooks(): """Manage Voice Mode hooks and event handlers.""" pass @hooks.command('receiver') @click.option('--tool-name', help='Override tool name (for testing)') @click.option('--action', type=click.Choice(['start', 'end']), help='Override action (for testing)') @click.option('--subagent-type', help='Override subagent type (for testing)') @click.option('--event', type=click.Choice(['PreToolUse', 'PostToolUse']), help='Override event (for testing)') @click.option('--debug', is_flag=True, help='Enable debug output') def receiver(tool_name, action, subagent_type, event, debug): """Receive and process hook events from Claude Code via stdin. This command reads JSON from stdin when called by Claude Code hooks, or accepts command-line arguments for testing. The filesystem structure defines sound mappings: ~/.voicemode/soundfonts/current/PreToolUse/task/subagent/baby-bear.wav Examples: # Called by Claude Code (reads JSON from stdin) voicemode claude hooks receiver # Testing with defaults voicemode claude hooks receiver --debug # Testing with specific values voicemode claude hooks receiver --tool-name Task --action start --subagent-type mama-bear """ from voice_mode.tools.sound_fonts.audio_player import Player # Try to read JSON from stdin if available hook_data = {} if not sys.stdin.isatty(): try: hook_data = json.load(sys.stdin) if debug: print(f"[DEBUG] Received JSON: {json.dumps(hook_data, indent=2)}", file=sys.stderr) except Exception as e: if debug: print(f"[DEBUG] Failed to parse JSON from stdin: {e}", file=sys.stderr) # Silent fail for hooks sys.exit(0) # Extract values from JSON or use command-line overrides/defaults if not tool_name: tool_name = hook_data.get('tool_name', 'Task') if not event: event_name = hook_data.get('hook_event_name', 'PreToolUse') else: event_name = event # Map event to action if not specified if not action: if event_name == 'PreToolUse': action = 'start' elif event_name == 'PostToolUse': action = 'end' else: action = 'start' # Default # Get subagent_type from tool_input if not specified if not subagent_type and tool_name == 'Task': tool_input = hook_data.get('tool_input', {}) subagent_type = tool_input.get('subagent_type', 'baby-bear') elif not subagent_type: subagent_type = None if debug: print(f"[DEBUG] Processing: event={event_name}, tool={tool_name}, " f"action={action}, subagent={subagent_type}", file=sys.stderr) # Check if sound fonts are enabled from voice_mode.config import SOUNDFONTS_ENABLED if not SOUNDFONTS_ENABLED: if debug: print(f"[DEBUG] Sound fonts are disabled (VOICEMODE_SOUNDFONTS_ENABLED=false)", file=sys.stderr) else: # Find sound file using filesystem conventions sound_file = find_sound_file(event_name, tool_name, subagent_type) if sound_file: if debug: print(f"[DEBUG] Found sound file: {sound_file}", file=sys.stderr) # Play the sound player = Player() success = player.play(str(sound_file)) if debug: if success: print(f"[DEBUG] Sound played successfully", file=sys.stderr) else: print(f"[DEBUG] Failed to play sound", file=sys.stderr) else: if debug: print(f"[DEBUG] No sound file found for this event", file=sys.stderr) # Always exit 0 to not disrupt Claude Code sys.exit(0) def find_sound_file(event: str, tool: str, subagent: Optional[str] = None) -> Optional[Path]: """ Find sound file using filesystem conventions. Tries paths in order (mp3 preferred over wav for size): 1. Most specific: {event}/{tool}/subagent/{subagent}.{mp3,wav} (Task tool only) 2. Tool default: {event}/{tool}/default.{mp3,wav} 3. Event default: {event}/default.{mp3,wav} 4. Global fallback: fallback.{mp3,wav} Args: event: Event name (PreToolUse, PostToolUse) tool: Tool name (lowercase) subagent: Optional subagent type (lowercase) Returns: Path to sound file if found, None otherwise """ # Get base path (follow symlink if exists) base_path = Path.home() / '.voicemode' / 'soundfonts' / 'current' # Resolve symlink if it exists if base_path.is_symlink(): base_path = base_path.resolve() if not base_path.exists(): return None # Normalize names to lowercase for filesystem event = event.lower() if event else 'pretooluse' tool = tool.lower() if tool else 'default' subagent = subagent.lower() if subagent else None # Map event names to directory names event_map = { 'pretooluse': 'PreToolUse', 'posttooluse': 'PostToolUse', 'start': 'PreToolUse', 'end': 'PostToolUse' } event_dir = event_map.get(event, event) # Build list of paths to try (most specific to least specific) paths_to_try = [] # 1. Most specific: subagent sound (Task tool only) if tool == 'task' and subagent: # Try mp3 first (smaller), then wav paths_to_try.append(base_path / event_dir / tool / 'subagent' / f'{subagent}.mp3') paths_to_try.append(base_path / event_dir / tool / 'subagent' / f'{subagent}.wav') # 2. Tool-specific default paths_to_try.append(base_path / event_dir / tool / 'default.mp3') paths_to_try.append(base_path / event_dir / tool / 'default.wav') # 3. Event-level default paths_to_try.append(base_path / event_dir / 'default.mp3') paths_to_try.append(base_path / event_dir / 'default.wav') # 4. Global fallback paths_to_try.append(base_path / 'fallback.mp3') paths_to_try.append(base_path / 'fallback.wav') # Find first existing file for path in paths_to_try: if path.exists(): return path return None # Keep the old stdin-receiver command for backwards compatibility (deprecated) @hooks.command('stdin-receiver', hidden=True) @click.argument('tool_name') @click.argument('action', type=click.Choice(['start', 'end', 'complete'])) @click.argument('subagent_type', required=False) @click.option('--debug', is_flag=True, help='Enable debug output') def stdin_receiver_deprecated(tool_name, action, subagent_type, event, debug): """[DEPRECATED] Use receiver instead.""" # Call the new receiver command ctx = click.get_current_context() ctx.invoke(receiver, tool_name=tool_name, action=action, subagent_type=subagent_type, event=event, debug=debug)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mbailey/voicemode'

If you have feedback or need assistance with the MCP directory API, please join our Discord server