voice-mode 4.0.1__tar.gz → 4.2.0__tar.gz
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.
- {voice_mode-4.0.1 → voice_mode-4.2.0}/CHANGELOG.md +56 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/PKG-INFO +1 -1
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/__version__.py +1 -1
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/cli.py +73 -0
- voice_mode-4.2.0/voice_mode/cli_commands/claude.py +208 -0
- voice_mode-4.2.0/voice_mode/cli_commands/hook.py +197 -0
- voice_mode-4.2.0/voice_mode/cli_commands/pronounce_commands.py +223 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/config.py +25 -0
- voice_mode-4.2.0/voice_mode/data/default_pronunciation.yaml +268 -0
- voice_mode-4.2.0/voice_mode/pronounce.py +397 -0
- voice_mode-4.2.0/voice_mode/tools/claude_thinking.py +285 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/converse.py +11 -0
- voice_mode-4.2.0/voice_mode/tools/pronounce.py +245 -0
- voice_mode-4.2.0/voice_mode/tools/sound_fonts/__init__.py +1 -0
- voice_mode-4.2.0/voice_mode/tools/sound_fonts/audio_player.py +87 -0
- voice_mode-4.2.0/voice_mode/tools/sound_fonts/hook_handler.py +127 -0
- voice_mode-4.2.0/voice_mode/tools/sound_fonts/player.py +180 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/.gitignore +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/README.md +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/build_hooks.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/pyproject.toml +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/__main__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/cli_commands/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/cli_commands/exchanges.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/cli_commands/transcribe.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/conversation_logger.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/core.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/data/versions.json +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/exchanges/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/exchanges/conversations.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/exchanges/filters.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/exchanges/formatters.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/exchanges/models.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/exchanges/reader.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/exchanges/stats.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/README.md +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/app/api/connection-details/route.ts +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/app/favicon.ico +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/app/globals.css +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/app/layout.tsx +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/app/page.tsx +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/components/CloseIcon.tsx +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/components/NoAgentNotification.tsx +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/components/TranscriptionView.tsx +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/hooks/useCombinedTranscriptions.ts +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/hooks/useLocalMicTrack.ts +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/next-env.d.ts +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/next.config.mjs +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/package-lock.json +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/package.json +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/pnpm-lock.yaml +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/postcss.config.mjs +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/tailwind.config.ts +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/frontend/tsconfig.json +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/prompts/README.md +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/prompts/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/prompts/converse.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/prompts/release_notes.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/prompts/services.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/provider_discovery.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/providers.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/resources/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/resources/audio_files.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/resources/changelog.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/resources/configuration.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/resources/statistics.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/resources/version.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/resources/whisper_models.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/server.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/shared.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/simple_failover.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/statistics.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/streaming.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/launchd/com.voicemode.frontend.plist +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/launchd/com.voicemode.kokoro.plist +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/launchd/com.voicemode.livekit.plist +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/launchd/com.voicemode.whisper.plist +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/launchd/start-kokoro-with-health-check.sh +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/launchd/start-whisper-with-health-check.sh +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/scripts/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/scripts/start-whisper-server.sh +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/systemd/voicemode-frontend.service +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/systemd/voicemode-kokoro.service +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/systemd/voicemode-livekit.service +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/templates/systemd/voicemode-whisper.service +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/configuration_management.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/dependencies.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/devices.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/diagnostics.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/providers.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/service.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/kokoro/install.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/kokoro/uninstall.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/list_versions.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/livekit/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/livekit/frontend.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/livekit/install.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/livekit/production_server.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/livekit/uninstall.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/version_info.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/install.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/list_models.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/model_active.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/model_benchmark.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/model_install.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/model_remove.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/models.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/services/whisper/uninstall.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/statistics.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/transcription/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/transcription/backends.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/transcription/core.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/transcription/formats.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/transcription/types.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/tools/voice_registry.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/__init__.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/audio_diagnostics.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/event_logger.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/ffmpeg_check.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/format_migration.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/gpu_detection.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/migration_helpers.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/services/common.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/services/coreml_setup.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/services/kokoro_helpers.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/services/livekit_helpers.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/services/whisper_helpers.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/services/whisper_version.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/utils/version_helpers.py +0 -0
- {voice_mode-4.0.1 → voice_mode-4.2.0}/voice_mode/version.py +0 -0
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [4.2.0] - 2025-09-03
|
11
|
+
|
12
|
+
### Added
|
13
|
+
- **🧠 Think Out Loud Mode - AI Reasoning Made Audible**
|
14
|
+
- Revolutionary feature that transforms AI's internal thinking into spoken performances
|
15
|
+
- Extracts and voices Claude's reasoning blocks using multiple personas
|
16
|
+
- Herman's Head / Inside Out style multi-voice performances for different reasoning types
|
17
|
+
- Theditor agent that orchestrates thinking performances with distinct voices
|
18
|
+
- Makes AI decision-making transparent and engaging through voice
|
19
|
+
|
20
|
+
- **🔊 Sound Fonts Integration - Audio Feedback for Every Action**
|
21
|
+
- Play custom sounds for tool operations, errors, and completions
|
22
|
+
- Filesystem-based sound font system with automatic discovery
|
23
|
+
- Claude Code integration via stdin-receiver for hook-based audio
|
24
|
+
- CLI command `play-sound` with theme, action, and sound selection
|
25
|
+
- Enhances user experience with auditory feedback during operations
|
26
|
+
|
27
|
+
- **🎭 Claude Code Deep Integration**
|
28
|
+
- Extract and analyze Claude's conversation logs in real-time
|
29
|
+
- Access Claude's internal thinking blocks for transparency
|
30
|
+
- CLI commands for message extraction with multiple output formats
|
31
|
+
- Automatic context detection for Claude Code sessions
|
32
|
+
- Foundation for advanced AI introspection features
|
33
|
+
|
34
|
+
### Changed
|
35
|
+
- **Enhanced Message Extraction**
|
36
|
+
- Generic and flexible extraction supporting full conversations
|
37
|
+
- Multiple output formats: full messages, text only, or thinking only
|
38
|
+
- Better filtering by message type (user/assistant)
|
39
|
+
- Improved integration with voice mode tools
|
40
|
+
|
41
|
+
### Removed
|
42
|
+
- **Redundant get_claude_thinking MCP tool**
|
43
|
+
- Consolidated into more powerful get_claude_messages tool
|
44
|
+
|
45
|
+
### Documentation
|
46
|
+
- **Comprehensive Think Out Loud Documentation**
|
47
|
+
- Agent specifications for theditor
|
48
|
+
- Claude orchestration instructions
|
49
|
+
- Voice persona mapping guide
|
50
|
+
- Integration patterns and examples
|
51
|
+
|
52
|
+
## [4.1.0] - 2025-09-01
|
53
|
+
|
54
|
+
### Added
|
55
|
+
- **Pronunciation middleware for TTS/STT text processing**
|
56
|
+
- Configurable pronunciation rules system that processes text before TTS and after STT
|
57
|
+
- Regex-based text substitution rules with YAML configuration
|
58
|
+
- Separate TTS and STT rule sets for bidirectional corrections
|
59
|
+
- Privacy support - rules can be marked private to hide from LLM tool listings
|
60
|
+
- Default rules for common patterns (3M, PoE, GbE, etc.)
|
61
|
+
- Full CLI interface for managing pronunciation rules
|
62
|
+
- MCP tool for LLM-based rule management with `pronounce` tool
|
63
|
+
- Integrated into converse tool for automatic text processing
|
64
|
+
- New configuration file: `voice_mode/data/default_pronunciation.yaml`
|
65
|
+
|
10
66
|
## [4.0.1] - 2025-09-01
|
11
67
|
|
12
68
|
### Removed
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: voice-mode
|
3
|
-
Version: 4.0
|
3
|
+
Version: 4.2.0
|
4
4
|
Summary: VoiceMode - Voice interaction capabilities for AI assistants (formerly voice-mcp)
|
5
5
|
Project-URL: Homepage, https://github.com/mbailey/voicemode
|
6
6
|
Project-URL: Repository, https://github.com/mbailey/voicemode
|
@@ -1360,17 +1360,27 @@ def cli():
|
|
1360
1360
|
# Import subcommand groups
|
1361
1361
|
from voice_mode.cli_commands import exchanges as exchanges_cmd
|
1362
1362
|
from voice_mode.cli_commands import transcribe as transcribe_cmd
|
1363
|
+
from voice_mode.cli_commands import pronounce_commands
|
1364
|
+
from voice_mode.cli_commands import claude
|
1365
|
+
from voice_mode.cli_commands import hook as hook_cmd
|
1363
1366
|
|
1364
1367
|
# Add subcommands to legacy CLI
|
1365
1368
|
cli.add_command(exchanges_cmd.exchanges)
|
1366
1369
|
cli.add_command(transcribe_cmd.transcribe)
|
1370
|
+
cli.add_command(pronounce_commands.pronounce_group)
|
1371
|
+
cli.add_command(claude.claude_group)
|
1367
1372
|
|
1368
1373
|
# Add exchanges to main CLI
|
1369
1374
|
voice_mode_main_cli.add_command(exchanges_cmd.exchanges)
|
1375
|
+
voice_mode_main_cli.add_command(pronounce_commands.pronounce_group)
|
1376
|
+
voice_mode_main_cli.add_command(claude.claude_group)
|
1370
1377
|
|
1371
1378
|
# Add transcribe to main CLI
|
1372
1379
|
voice_mode_main_cli.add_command(transcribe_cmd.transcribe)
|
1373
1380
|
|
1381
|
+
# Add hook command to main CLI
|
1382
|
+
voice_mode_main_cli.add_command(hook_cmd.hook)
|
1383
|
+
|
1374
1384
|
|
1375
1385
|
# Converse command - direct voice conversation from CLI
|
1376
1386
|
@voice_mode_main_cli.command()
|
@@ -1748,6 +1758,69 @@ def update(force):
|
|
1748
1758
|
click.echo("Try running: pip install --upgrade voice-mode")
|
1749
1759
|
|
1750
1760
|
|
1761
|
+
# Sound Fonts command
|
1762
|
+
@voice_mode_main_cli.command("play-sound")
|
1763
|
+
@click.help_option('-h', '--help')
|
1764
|
+
@click.option('-t', '--tool', help='Tool name for direct command-line usage')
|
1765
|
+
@click.option('-a', '--action', default='start', type=click.Choice(['start', 'end']), help='Action type')
|
1766
|
+
@click.option('-s', '--subagent', help='Subagent type (for Task tool)')
|
1767
|
+
def play_sound(tool, action, subagent):
|
1768
|
+
"""Play sound based on tool events (primarily for Claude Code hooks).
|
1769
|
+
|
1770
|
+
This command is designed to be called by Claude Code hooks to play sounds
|
1771
|
+
when tools are used. It reads hook data from stdin by default, or can be
|
1772
|
+
used directly with command-line options.
|
1773
|
+
|
1774
|
+
Examples:
|
1775
|
+
echo '{"tool_name":"Task","tool_input":{"subagent_type":"mama-bear"}}' | voicemode play-sound
|
1776
|
+
voicemode play-sound --tool Task --action start --subagent mama-bear
|
1777
|
+
"""
|
1778
|
+
import sys
|
1779
|
+
from .tools.sound_fonts.player import AudioPlayer
|
1780
|
+
from .tools.sound_fonts.hook_handler import (
|
1781
|
+
read_hook_data_from_stdin,
|
1782
|
+
parse_claude_code_hook
|
1783
|
+
)
|
1784
|
+
|
1785
|
+
# Try to read hook data from stdin first
|
1786
|
+
hook_data = None
|
1787
|
+
if not sys.stdin.isatty():
|
1788
|
+
hook_data = read_hook_data_from_stdin()
|
1789
|
+
|
1790
|
+
if hook_data:
|
1791
|
+
# Parse Claude Code hook format
|
1792
|
+
parsed_data = parse_claude_code_hook(hook_data)
|
1793
|
+
if not parsed_data:
|
1794
|
+
sys.exit(1)
|
1795
|
+
|
1796
|
+
tool_name = parsed_data["tool_name"]
|
1797
|
+
action_type = parsed_data["action"]
|
1798
|
+
subagent_type = parsed_data["subagent_type"]
|
1799
|
+
metadata = parsed_data["metadata"]
|
1800
|
+
else:
|
1801
|
+
# Use command-line arguments
|
1802
|
+
if not tool:
|
1803
|
+
click.echo("Error: --tool is required when not reading from stdin", err=True)
|
1804
|
+
sys.exit(1)
|
1805
|
+
|
1806
|
+
tool_name = tool
|
1807
|
+
action_type = action
|
1808
|
+
subagent_type = subagent
|
1809
|
+
metadata = {}
|
1810
|
+
|
1811
|
+
# Play the sound
|
1812
|
+
player = AudioPlayer()
|
1813
|
+
success = player.play_sound_for_event(
|
1814
|
+
tool_name=tool_name,
|
1815
|
+
action=action_type,
|
1816
|
+
subagent_type=subagent_type,
|
1817
|
+
metadata=metadata
|
1818
|
+
)
|
1819
|
+
|
1820
|
+
# Silent exit for hooks - don't clutter Claude Code output
|
1821
|
+
sys.exit(0 if success else 1)
|
1822
|
+
|
1823
|
+
|
1751
1824
|
# Completions command
|
1752
1825
|
@voice_mode_main_cli.command()
|
1753
1826
|
@click.help_option('-h', '--help')
|
@@ -0,0 +1,208 @@
|
|
1
|
+
"""CLI commands for Claude Code message extraction."""
|
2
|
+
|
3
|
+
import click
|
4
|
+
import json
|
5
|
+
import os
|
6
|
+
import logging
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Optional, List
|
9
|
+
from datetime import datetime
|
10
|
+
|
11
|
+
from voice_mode.tools.claude_thinking import (
|
12
|
+
find_claude_log_file,
|
13
|
+
extract_messages_from_log,
|
14
|
+
extract_thinking_from_messages
|
15
|
+
)
|
16
|
+
|
17
|
+
logger = logging.getLogger("voice-mode")
|
18
|
+
|
19
|
+
|
20
|
+
@click.group(name='claude')
|
21
|
+
def claude_group():
|
22
|
+
"""Extract messages from Claude Code conversation logs."""
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
26
|
+
@claude_group.command(name='messages')
|
27
|
+
@click.option('--last-n', '-n', default=2, type=int,
|
28
|
+
help='Number of recent messages to extract (default: 2)')
|
29
|
+
@click.option('--type', '-t', 'message_types', multiple=True,
|
30
|
+
type=click.Choice(['user', 'assistant']),
|
31
|
+
help='Filter by message type (can specify multiple)')
|
32
|
+
@click.option('--format', '-f',
|
33
|
+
type=click.Choice(['full', 'text', 'thinking']),
|
34
|
+
default='full',
|
35
|
+
help='Output format: full (complete), text (text only), thinking (thinking only)')
|
36
|
+
@click.option('--working-dir', '-d', type=click.Path(exists=True),
|
37
|
+
help='Working directory to find logs for (defaults to CWD)')
|
38
|
+
@click.option('--output', '-o', type=click.Path(),
|
39
|
+
help='Save output to file')
|
40
|
+
@click.option('--json', 'as_json', is_flag=True,
|
41
|
+
help='Output as JSON (overrides format)')
|
42
|
+
def messages_command(last_n: int, message_types: tuple, format: str,
|
43
|
+
working_dir: Optional[str], output: Optional[str],
|
44
|
+
as_json: bool):
|
45
|
+
"""
|
46
|
+
Extract recent messages from Claude Code logs.
|
47
|
+
|
48
|
+
Examples:
|
49
|
+
|
50
|
+
voicemode claude messages
|
51
|
+
|
52
|
+
voicemode claude messages -n 5 --format thinking
|
53
|
+
|
54
|
+
voicemode claude messages --type assistant --format text
|
55
|
+
|
56
|
+
voicemode claude messages --json -o messages.json
|
57
|
+
"""
|
58
|
+
# Find log file
|
59
|
+
log_file = find_claude_log_file(working_dir)
|
60
|
+
if not log_file:
|
61
|
+
click.echo(f"Error: Could not find Claude Code logs for directory: {working_dir or os.getcwd()}", err=True)
|
62
|
+
return
|
63
|
+
|
64
|
+
# Extract messages
|
65
|
+
message_type_list = list(message_types) if message_types else None
|
66
|
+
messages = extract_messages_from_log(log_file, last_n, message_type_list)
|
67
|
+
|
68
|
+
if not messages:
|
69
|
+
click.echo("No messages found in recent logs", err=True)
|
70
|
+
return
|
71
|
+
|
72
|
+
# Format output
|
73
|
+
if as_json:
|
74
|
+
content = json.dumps(messages, indent=2)
|
75
|
+
elif format == 'thinking':
|
76
|
+
thinking_texts = extract_thinking_from_messages(messages)
|
77
|
+
if not thinking_texts:
|
78
|
+
click.echo("No thinking content found", err=True)
|
79
|
+
return
|
80
|
+
content = "\n\n=== Next Thinking ===\n\n".join(thinking_texts)
|
81
|
+
elif format == 'text':
|
82
|
+
result = []
|
83
|
+
for msg in messages:
|
84
|
+
content_text = []
|
85
|
+
content_items = msg.get('content', [])
|
86
|
+
logger.debug(f"Message has {len(content_items)} content items")
|
87
|
+
for item in content_items:
|
88
|
+
if isinstance(item, dict):
|
89
|
+
item_type = item.get('type')
|
90
|
+
logger.debug(f"Content item type: {item_type}")
|
91
|
+
if item_type == 'text':
|
92
|
+
text = item.get('text', '')
|
93
|
+
if text:
|
94
|
+
content_text.append(text)
|
95
|
+
elif item_type == 'thinking':
|
96
|
+
text = item.get('text', '')
|
97
|
+
if text:
|
98
|
+
content_text.append(f"[Thinking: {text}]")
|
99
|
+
if content_text:
|
100
|
+
result.append(f"{msg['type'].title()}: {' '.join(content_text)}")
|
101
|
+
content = "\n\n".join(result)
|
102
|
+
else: # full format
|
103
|
+
# Format as human-readable
|
104
|
+
result = []
|
105
|
+
for i, msg in enumerate(messages, 1):
|
106
|
+
result.append(f"=== Message {i} ===")
|
107
|
+
result.append(f"Type: {msg['type']}")
|
108
|
+
result.append(f"Timestamp: {msg.get('timestamp', 'Unknown')}")
|
109
|
+
if msg.get('model'):
|
110
|
+
result.append(f"Model: {msg['model']}")
|
111
|
+
|
112
|
+
# Format content
|
113
|
+
content_items = msg.get('content', [])
|
114
|
+
if content_items:
|
115
|
+
result.append("Content:")
|
116
|
+
for item in content_items:
|
117
|
+
if isinstance(item, dict):
|
118
|
+
item_type = item.get('type', 'unknown')
|
119
|
+
if item_type == 'text':
|
120
|
+
result.append(f" [Text]: {item.get('text', '')}")
|
121
|
+
elif item_type == 'thinking':
|
122
|
+
result.append(f" [Thinking]: {item.get('text', '')}")
|
123
|
+
elif item_type == 'tool_use':
|
124
|
+
result.append(f" [Tool Use]: {item.get('name', '')}")
|
125
|
+
elif item_type == 'tool_result':
|
126
|
+
result.append(f" [Tool Result]: {item.get('content', '')[:100]}...")
|
127
|
+
result.append("")
|
128
|
+
content = "\n".join(result).strip()
|
129
|
+
|
130
|
+
# Output
|
131
|
+
if output:
|
132
|
+
Path(output).write_text(content)
|
133
|
+
click.echo(f"Output saved to {output}")
|
134
|
+
else:
|
135
|
+
# Debug: ensure content is printed
|
136
|
+
logger.debug(f"About to output {len(content)} characters")
|
137
|
+
click.echo(content)
|
138
|
+
logger.debug("Output complete")
|
139
|
+
|
140
|
+
|
141
|
+
@claude_group.command(name='thinking')
|
142
|
+
@click.option('--last-n', '-n', default=1, type=int,
|
143
|
+
help='Number of messages to search for thinking (default: 1)')
|
144
|
+
@click.option('--working-dir', '-d', type=click.Path(exists=True),
|
145
|
+
help='Working directory to find logs for (defaults to CWD)')
|
146
|
+
def thinking_command(last_n: int, working_dir: Optional[str]):
|
147
|
+
"""
|
148
|
+
Extract only thinking content from Claude Code logs.
|
149
|
+
|
150
|
+
Convenience command equivalent to:
|
151
|
+
voicemode claude messages --format thinking --type assistant
|
152
|
+
|
153
|
+
Examples:
|
154
|
+
|
155
|
+
voicemode claude thinking
|
156
|
+
|
157
|
+
voicemode claude thinking -n 3
|
158
|
+
"""
|
159
|
+
# Delegate to messages command with thinking format
|
160
|
+
ctx = click.get_current_context()
|
161
|
+
ctx.invoke(messages_command,
|
162
|
+
last_n=last_n,
|
163
|
+
message_types=('assistant',),
|
164
|
+
format='thinking',
|
165
|
+
working_dir=working_dir,
|
166
|
+
output=None,
|
167
|
+
as_json=False)
|
168
|
+
|
169
|
+
|
170
|
+
@claude_group.command(name='check')
|
171
|
+
def check_command():
|
172
|
+
"""
|
173
|
+
Check if Claude Code context is available.
|
174
|
+
|
175
|
+
Shows information about the Claude Code environment including:
|
176
|
+
- Whether Claude Code logs are accessible
|
177
|
+
- Current working directory
|
178
|
+
- Log file location if found
|
179
|
+
- Last update time
|
180
|
+
|
181
|
+
Example:
|
182
|
+
voicemode claude check
|
183
|
+
"""
|
184
|
+
working_dir = os.getcwd()
|
185
|
+
log_file = find_claude_log_file(working_dir)
|
186
|
+
|
187
|
+
click.echo(f"Working Directory: {working_dir}")
|
188
|
+
click.echo(f"Claude Logs Found: {'Yes' if log_file else 'No'}")
|
189
|
+
|
190
|
+
if log_file:
|
191
|
+
click.echo(f"Log File: {log_file}")
|
192
|
+
click.echo(f"Log Size: {log_file.stat().st_size:,} bytes")
|
193
|
+
|
194
|
+
mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
|
195
|
+
now = datetime.now()
|
196
|
+
age = now - mtime
|
197
|
+
|
198
|
+
if age.total_seconds() < 60:
|
199
|
+
click.echo(f"Last Updated: {int(age.total_seconds())} seconds ago")
|
200
|
+
elif age.total_seconds() < 3600:
|
201
|
+
click.echo(f"Last Updated: {int(age.total_seconds() / 60)} minutes ago")
|
202
|
+
else:
|
203
|
+
click.echo(f"Last Updated: {int(age.total_seconds() / 3600)} hours ago")
|
204
|
+
else:
|
205
|
+
project_dir = working_dir.replace('/', '-')
|
206
|
+
expected_path = Path.home() / '.claude' / 'projects' / project_dir
|
207
|
+
click.echo(f"Expected Log Location: {expected_path}")
|
208
|
+
click.echo("Note: Logs are only created when using Claude Code (claude.ai/code)")
|
@@ -0,0 +1,197 @@
|
|
1
|
+
"""
|
2
|
+
Hook commands for Voice Mode - primarily for Claude Code integration.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import click
|
6
|
+
import sys
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Optional
|
11
|
+
|
12
|
+
|
13
|
+
@click.group()
|
14
|
+
@click.help_option('-h', '--help', help='Show this message and exit')
|
15
|
+
def hook():
|
16
|
+
"""Manage Voice Mode hooks and event handlers."""
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
@hook.command('stdin-receiver')
|
21
|
+
@click.option('--tool-name', help='Override tool name (for testing)')
|
22
|
+
@click.option('--action', type=click.Choice(['start', 'end']), help='Override action (for testing)')
|
23
|
+
@click.option('--subagent-type', help='Override subagent type (for testing)')
|
24
|
+
@click.option('--event', type=click.Choice(['PreToolUse', 'PostToolUse']), help='Override event (for testing)')
|
25
|
+
@click.option('--debug', is_flag=True, help='Enable debug output')
|
26
|
+
def stdin_receiver(tool_name, action, subagent_type, event, debug):
|
27
|
+
"""Receive and process hook events from Claude Code via stdin.
|
28
|
+
|
29
|
+
This command reads JSON from stdin when called by Claude Code hooks,
|
30
|
+
or accepts command-line arguments for testing.
|
31
|
+
|
32
|
+
The filesystem structure defines sound mappings:
|
33
|
+
~/.voicemode/soundfonts/current/PreToolUse/task/subagent/baby-bear.wav
|
34
|
+
|
35
|
+
Examples:
|
36
|
+
# Called by Claude Code (reads JSON from stdin)
|
37
|
+
voicemode hook stdin-receiver
|
38
|
+
|
39
|
+
# Testing with defaults
|
40
|
+
voicemode hook stdin-receiver --debug
|
41
|
+
|
42
|
+
# Testing with specific values
|
43
|
+
voicemode hook stdin-receiver --tool-name Task --action start --subagent-type mama-bear
|
44
|
+
"""
|
45
|
+
from voice_mode.tools.sound_fonts.audio_player import Player
|
46
|
+
|
47
|
+
# Try to read JSON from stdin if available
|
48
|
+
hook_data = {}
|
49
|
+
if not sys.stdin.isatty():
|
50
|
+
try:
|
51
|
+
hook_data = json.load(sys.stdin)
|
52
|
+
if debug:
|
53
|
+
print(f"[DEBUG] Received JSON: {json.dumps(hook_data, indent=2)}", file=sys.stderr)
|
54
|
+
except Exception as e:
|
55
|
+
if debug:
|
56
|
+
print(f"[DEBUG] Failed to parse JSON from stdin: {e}", file=sys.stderr)
|
57
|
+
# Silent fail for hooks
|
58
|
+
sys.exit(0)
|
59
|
+
|
60
|
+
# Extract values from JSON or use command-line overrides/defaults
|
61
|
+
if not tool_name:
|
62
|
+
tool_name = hook_data.get('tool_name', 'Task')
|
63
|
+
|
64
|
+
if not event:
|
65
|
+
event_name = hook_data.get('hook_event_name', 'PreToolUse')
|
66
|
+
else:
|
67
|
+
event_name = event
|
68
|
+
|
69
|
+
# Map event to action if not specified
|
70
|
+
if not action:
|
71
|
+
if event_name == 'PreToolUse':
|
72
|
+
action = 'start'
|
73
|
+
elif event_name == 'PostToolUse':
|
74
|
+
action = 'end'
|
75
|
+
else:
|
76
|
+
action = 'start' # Default
|
77
|
+
|
78
|
+
# Get subagent_type from tool_input if not specified
|
79
|
+
if not subagent_type and tool_name == 'Task':
|
80
|
+
tool_input = hook_data.get('tool_input', {})
|
81
|
+
subagent_type = tool_input.get('subagent_type', 'baby-bear')
|
82
|
+
elif not subagent_type:
|
83
|
+
subagent_type = None
|
84
|
+
|
85
|
+
if debug:
|
86
|
+
print(f"[DEBUG] Processing: event={event_name}, tool={tool_name}, "
|
87
|
+
f"action={action}, subagent={subagent_type}", file=sys.stderr)
|
88
|
+
|
89
|
+
# Find sound file using filesystem conventions
|
90
|
+
sound_file = find_sound_file(event_name, tool_name, subagent_type)
|
91
|
+
|
92
|
+
if sound_file:
|
93
|
+
if debug:
|
94
|
+
print(f"[DEBUG] Found sound file: {sound_file}", file=sys.stderr)
|
95
|
+
|
96
|
+
# Play the sound
|
97
|
+
player = Player()
|
98
|
+
success = player.play(str(sound_file))
|
99
|
+
|
100
|
+
if debug:
|
101
|
+
if success:
|
102
|
+
print(f"[DEBUG] Sound played successfully", file=sys.stderr)
|
103
|
+
else:
|
104
|
+
print(f"[DEBUG] Failed to play sound", file=sys.stderr)
|
105
|
+
else:
|
106
|
+
if debug:
|
107
|
+
print(f"[DEBUG] No sound file found for this event", file=sys.stderr)
|
108
|
+
|
109
|
+
# Always exit 0 to not disrupt Claude Code
|
110
|
+
sys.exit(0)
|
111
|
+
|
112
|
+
|
113
|
+
def find_sound_file(event: str, tool: str, subagent: Optional[str] = None) -> Optional[Path]:
|
114
|
+
"""
|
115
|
+
Find sound file using filesystem conventions.
|
116
|
+
|
117
|
+
Tries paths in order:
|
118
|
+
1. Most specific: {event}/{tool}/subagent/{subagent}.wav (Task tool only)
|
119
|
+
2. Tool default: {event}/{tool}/default.wav
|
120
|
+
3. Event default: {event}/default.wav
|
121
|
+
4. Global fallback: fallback.wav
|
122
|
+
|
123
|
+
Args:
|
124
|
+
event: Event name (PreToolUse, PostToolUse)
|
125
|
+
tool: Tool name (lowercase)
|
126
|
+
subagent: Optional subagent type (lowercase)
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
Path to sound file if found, None otherwise
|
130
|
+
"""
|
131
|
+
# Get base path (follow symlink if exists)
|
132
|
+
base_path = Path.home() / '.voicemode' / 'soundfonts' / 'current'
|
133
|
+
|
134
|
+
# Resolve symlink if it exists
|
135
|
+
if base_path.is_symlink():
|
136
|
+
base_path = base_path.resolve()
|
137
|
+
|
138
|
+
if not base_path.exists():
|
139
|
+
return None
|
140
|
+
|
141
|
+
# Normalize names to lowercase for filesystem
|
142
|
+
event = event.lower() if event else 'pretooluse'
|
143
|
+
tool = tool.lower() if tool else 'default'
|
144
|
+
subagent = subagent.lower() if subagent else None
|
145
|
+
|
146
|
+
# Map event names to directory names
|
147
|
+
event_map = {
|
148
|
+
'pretooluse': 'PreToolUse',
|
149
|
+
'posttooluse': 'PostToolUse',
|
150
|
+
'start': 'PreToolUse',
|
151
|
+
'end': 'PostToolUse'
|
152
|
+
}
|
153
|
+
event_dir = event_map.get(event, event)
|
154
|
+
|
155
|
+
# Build list of paths to try (most specific to least specific)
|
156
|
+
paths_to_try = []
|
157
|
+
|
158
|
+
# 1. Most specific: subagent sound (Task tool only)
|
159
|
+
if tool == 'task' and subagent:
|
160
|
+
paths_to_try.append(base_path / event_dir / tool / 'subagent' / f'{subagent}.wav')
|
161
|
+
|
162
|
+
# 2. Tool-specific default
|
163
|
+
paths_to_try.append(base_path / event_dir / tool / 'default.wav')
|
164
|
+
|
165
|
+
# 3. Event-level default
|
166
|
+
paths_to_try.append(base_path / event_dir / 'default.wav')
|
167
|
+
|
168
|
+
# 4. Global fallback
|
169
|
+
paths_to_try.append(base_path / 'fallback.wav')
|
170
|
+
|
171
|
+
# Find first existing file
|
172
|
+
for path in paths_to_try:
|
173
|
+
if path.exists():
|
174
|
+
return path
|
175
|
+
|
176
|
+
return None
|
177
|
+
|
178
|
+
|
179
|
+
# Keep the old receiver command for backwards compatibility (deprecated)
|
180
|
+
@hook.command('receiver', hidden=True)
|
181
|
+
@click.argument('tool_name')
|
182
|
+
@click.argument('action', type=click.Choice(['start', 'end', 'complete']))
|
183
|
+
@click.argument('subagent_type', required=False)
|
184
|
+
@click.option('--debug', is_flag=True, help='Enable debug output')
|
185
|
+
def receiver_deprecated(tool_name, action, subagent_type, debug):
|
186
|
+
"""[DEPRECATED] Use stdin-receiver instead."""
|
187
|
+
# Map old action to event
|
188
|
+
event = 'PreToolUse' if action == 'start' else 'PostToolUse'
|
189
|
+
|
190
|
+
# Call the new command
|
191
|
+
ctx = click.get_current_context()
|
192
|
+
ctx.invoke(stdin_receiver,
|
193
|
+
tool_name=tool_name,
|
194
|
+
action=action if action != 'complete' else 'end',
|
195
|
+
subagent_type=subagent_type,
|
196
|
+
event=event,
|
197
|
+
debug=debug)
|