voice-mode 4.1.0__tar.gz → 4.3.1__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.
Files changed (134) hide show
  1. {voice_mode-4.1.0 → voice_mode-4.3.1}/CHANGELOG.md +50 -0
  2. {voice_mode-4.1.0 → voice_mode-4.3.1}/PKG-INFO +1 -1
  3. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/__version__.py +1 -1
  4. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/cli.py +91 -3
  5. voice_mode-4.3.1/voice_mode/cli_commands/claude.py +208 -0
  6. voice_mode-4.3.1/voice_mode/cli_commands/hook.py +209 -0
  7. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/config.py +114 -0
  8. voice_mode-4.3.1/voice_mode/tools/claude_thinking.py +285 -0
  9. voice_mode-4.3.1/voice_mode/tools/sound_fonts/__init__.py +1 -0
  10. voice_mode-4.3.1/voice_mode/tools/sound_fonts/audio_player.py +87 -0
  11. voice_mode-4.3.1/voice_mode/tools/sound_fonts/hook_handler.py +127 -0
  12. voice_mode-4.3.1/voice_mode/tools/sound_fonts/player.py +180 -0
  13. {voice_mode-4.1.0 → voice_mode-4.3.1}/.gitignore +0 -0
  14. {voice_mode-4.1.0 → voice_mode-4.3.1}/README.md +0 -0
  15. {voice_mode-4.1.0 → voice_mode-4.3.1}/build_hooks.py +0 -0
  16. {voice_mode-4.1.0 → voice_mode-4.3.1}/pyproject.toml +0 -0
  17. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/__init__.py +0 -0
  18. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/__main__.py +0 -0
  19. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/cli_commands/__init__.py +0 -0
  20. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/cli_commands/exchanges.py +0 -0
  21. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/cli_commands/pronounce_commands.py +0 -0
  22. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/cli_commands/transcribe.py +0 -0
  23. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/conversation_logger.py +0 -0
  24. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/core.py +0 -0
  25. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/data/default_pronunciation.yaml +0 -0
  26. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/data/versions.json +0 -0
  27. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/exchanges/__init__.py +0 -0
  28. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/exchanges/conversations.py +0 -0
  29. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/exchanges/filters.py +0 -0
  30. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/exchanges/formatters.py +0 -0
  31. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/exchanges/models.py +0 -0
  32. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/exchanges/reader.py +0 -0
  33. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/exchanges/stats.py +0 -0
  34. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/README.md +0 -0
  35. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/app/api/connection-details/route.ts +0 -0
  36. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/app/favicon.ico +0 -0
  37. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/app/globals.css +0 -0
  38. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/app/layout.tsx +0 -0
  39. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/app/page.tsx +0 -0
  40. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/components/CloseIcon.tsx +0 -0
  41. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/components/NoAgentNotification.tsx +0 -0
  42. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/components/TranscriptionView.tsx +0 -0
  43. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/hooks/useCombinedTranscriptions.ts +0 -0
  44. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/hooks/useLocalMicTrack.ts +0 -0
  45. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/next-env.d.ts +0 -0
  46. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/next.config.mjs +0 -0
  47. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/package-lock.json +0 -0
  48. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/package.json +0 -0
  49. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/pnpm-lock.yaml +0 -0
  50. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/postcss.config.mjs +0 -0
  51. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/tailwind.config.ts +0 -0
  52. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/frontend/tsconfig.json +0 -0
  53. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/prompts/README.md +0 -0
  54. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/prompts/__init__.py +0 -0
  55. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/prompts/converse.py +0 -0
  56. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/prompts/release_notes.py +0 -0
  57. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/prompts/services.py +0 -0
  58. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/pronounce.py +0 -0
  59. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/provider_discovery.py +0 -0
  60. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/providers.py +0 -0
  61. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/resources/__init__.py +0 -0
  62. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/resources/audio_files.py +0 -0
  63. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/resources/changelog.py +0 -0
  64. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/resources/configuration.py +0 -0
  65. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/resources/statistics.py +0 -0
  66. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/resources/version.py +0 -0
  67. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/resources/whisper_models.py +0 -0
  68. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/server.py +0 -0
  69. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/shared.py +0 -0
  70. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/simple_failover.py +0 -0
  71. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/statistics.py +0 -0
  72. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/streaming.py +0 -0
  73. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/__init__.py +0 -0
  74. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/launchd/com.voicemode.frontend.plist +0 -0
  75. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/launchd/com.voicemode.kokoro.plist +0 -0
  76. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/launchd/com.voicemode.livekit.plist +0 -0
  77. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/launchd/com.voicemode.whisper.plist +0 -0
  78. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/launchd/start-kokoro-with-health-check.sh +0 -0
  79. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/launchd/start-whisper-with-health-check.sh +0 -0
  80. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/scripts/__init__.py +0 -0
  81. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/scripts/start-whisper-server.sh +0 -0
  82. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/systemd/voicemode-frontend.service +0 -0
  83. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/systemd/voicemode-kokoro.service +0 -0
  84. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/systemd/voicemode-livekit.service +0 -0
  85. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/templates/systemd/voicemode-whisper.service +0 -0
  86. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/__init__.py +0 -0
  87. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/configuration_management.py +0 -0
  88. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/converse.py +0 -0
  89. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/dependencies.py +0 -0
  90. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/devices.py +0 -0
  91. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/diagnostics.py +0 -0
  92. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/pronounce.py +0 -0
  93. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/providers.py +0 -0
  94. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/service.py +0 -0
  95. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/kokoro/install.py +0 -0
  96. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/kokoro/uninstall.py +0 -0
  97. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/list_versions.py +0 -0
  98. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/livekit/__init__.py +0 -0
  99. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/livekit/frontend.py +0 -0
  100. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/livekit/install.py +0 -0
  101. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/livekit/production_server.py +0 -0
  102. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/livekit/uninstall.py +0 -0
  103. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/version_info.py +0 -0
  104. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/__init__.py +0 -0
  105. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/install.py +0 -0
  106. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/list_models.py +0 -0
  107. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/model_active.py +0 -0
  108. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/model_benchmark.py +0 -0
  109. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/model_install.py +0 -0
  110. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/model_remove.py +0 -0
  111. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/models.py +0 -0
  112. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/services/whisper/uninstall.py +0 -0
  113. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/statistics.py +0 -0
  114. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/transcription/__init__.py +0 -0
  115. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/transcription/backends.py +0 -0
  116. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/transcription/core.py +0 -0
  117. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/transcription/formats.py +0 -0
  118. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/transcription/types.py +0 -0
  119. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/tools/voice_registry.py +0 -0
  120. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/__init__.py +0 -0
  121. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/audio_diagnostics.py +0 -0
  122. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/event_logger.py +0 -0
  123. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/ffmpeg_check.py +0 -0
  124. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/format_migration.py +0 -0
  125. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/gpu_detection.py +0 -0
  126. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/migration_helpers.py +0 -0
  127. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/services/common.py +0 -0
  128. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/services/coreml_setup.py +0 -0
  129. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/services/kokoro_helpers.py +0 -0
  130. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/services/livekit_helpers.py +0 -0
  131. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/services/whisper_helpers.py +0 -0
  132. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/services/whisper_version.py +0 -0
  133. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/utils/version_helpers.py +0 -0
  134. {voice_mode-4.1.0 → voice_mode-4.3.1}/voice_mode/version.py +0 -0
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.3.1] - 2025-09-03
11
+
12
+ ## [4.3.0] - 2025-09-03
13
+
14
+ ## [4.2.0] - 2025-09-03
15
+
16
+ ### Added
17
+ - **🧠 Think Out Loud Mode - AI Reasoning Made Audible**
18
+ - Revolutionary feature that transforms AI's internal thinking into spoken performances
19
+ - Extracts and voices Claude's reasoning blocks using multiple personas
20
+ - Herman's Head / Inside Out style multi-voice performances for different reasoning types
21
+ - Theditor agent that orchestrates thinking performances with distinct voices
22
+ - Makes AI decision-making transparent and engaging through voice
23
+
24
+ - **🔊 Sound Fonts Integration - Audio Feedback for Every Action**
25
+ - Play custom sounds for tool operations, errors, and completions
26
+ - Filesystem-based sound font system with automatic discovery
27
+ - Claude Code integration via stdin-receiver for hook-based audio
28
+ - CLI command `play-sound` with theme, action, and sound selection
29
+ - Enhances user experience with auditory feedback during operations
30
+ - MP3 support added for 90% file size reduction over WAV
31
+ - Recursive directory copying for complete sound font structure
32
+ - Three Bears sound fonts for baby-bear, mama-bear, and papa-bear agents
33
+ - Sound fonts disabled by default (VOICEMODE_SOUNDFONTS_ENABLED=false)
34
+
35
+ - **🎭 Claude Code Deep Integration**
36
+ - Extract and analyze Claude's conversation logs in real-time
37
+ - Access Claude's internal thinking blocks for transparency
38
+ - CLI commands for message extraction with multiple output formats
39
+ - Automatic context detection for Claude Code sessions
40
+ - Foundation for advanced AI introspection features
41
+
42
+ ### Changed
43
+ - **Enhanced Message Extraction**
44
+ - Generic and flexible extraction supporting full conversations
45
+ - Multiple output formats: full messages, text only, or thinking only
46
+ - Better filtering by message type (user/assistant)
47
+ - Improved integration with voice mode tools
48
+
49
+ ### Removed
50
+ - **Redundant get_claude_thinking MCP tool**
51
+ - Consolidated into more powerful get_claude_messages tool
52
+
53
+ ### Documentation
54
+ - **Comprehensive Think Out Loud Documentation**
55
+ - Agent specifications for theditor
56
+ - Claude orchestration instructions
57
+ - Voice persona mapping guide
58
+ - Integration patterns and examples
59
+
10
60
  ## [4.1.0] - 2025-09-01
11
61
 
12
62
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-mode
3
- Version: 4.1.0
3
+ Version: 4.3.1
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
@@ -1,3 +1,3 @@
1
1
  # This file is automatically updated by 'make release'
2
2
  # Do not edit manually
3
- __version__ = "4.1.0"
3
+ __version__ = "4.3.1"
@@ -57,6 +57,14 @@ def voice_mode() -> None:
57
57
  voice_mode_main_cli()
58
58
 
59
59
 
60
+ # Audio group for audio-related commands
61
+ @voice_mode_main_cli.group()
62
+ @click.help_option('-h', '--help', help='Show this message and exit')
63
+ def audio():
64
+ """Audio transcription and playback commands."""
65
+ pass
66
+
67
+
60
68
  # Service group commands
61
69
  @voice_mode_main_cli.group()
62
70
  @click.help_option('-h', '--help', help='Show this message and exit')
@@ -1361,19 +1369,36 @@ def cli():
1361
1369
  from voice_mode.cli_commands import exchanges as exchanges_cmd
1362
1370
  from voice_mode.cli_commands import transcribe as transcribe_cmd
1363
1371
  from voice_mode.cli_commands import pronounce_commands
1372
+ from voice_mode.cli_commands import claude
1373
+ from voice_mode.cli_commands import hook as hook_cmd
1364
1374
 
1365
1375
  # Add subcommands to legacy CLI
1366
1376
  cli.add_command(exchanges_cmd.exchanges)
1367
1377
  cli.add_command(transcribe_cmd.transcribe)
1368
1378
  cli.add_command(pronounce_commands.pronounce_group)
1379
+ cli.add_command(claude.claude_group)
1369
1380
 
1370
1381
  # Add exchanges to main CLI
1371
1382
  voice_mode_main_cli.add_command(exchanges_cmd.exchanges)
1372
- voice_mode_main_cli.add_command(pronounce_commands.pronounce_group)
1383
+ voice_mode_main_cli.add_command(claude.claude_group)
1384
+
1385
+ # Note: We'll add these commands after the groups are defined
1386
+ # audio group will get transcribe and play commands
1387
+ # claude group will get hook command
1388
+ # config group will get pronounce command
1373
1389
 
1374
- # Add transcribe to main CLI
1375
- voice_mode_main_cli.add_command(transcribe_cmd.transcribe)
1376
1390
 
1391
+ # Now add the subcommands to their respective groups
1392
+ # Add transcribe command to audio group
1393
+ transcribe_audio_cmd = transcribe_cmd.transcribe.commands['audio']
1394
+ transcribe_audio_cmd.name = 'transcribe'
1395
+ audio.add_command(transcribe_audio_cmd)
1396
+
1397
+ # Add hook command under claude group
1398
+ claude.claude_group.add_command(hook_cmd.hook)
1399
+
1400
+ # Add pronounce under config group
1401
+ config.add_command(pronounce_commands.pronounce_group)
1377
1402
 
1378
1403
  # Converse command - direct voice conversation from CLI
1379
1404
  @voice_mode_main_cli.command()
@@ -1751,6 +1776,69 @@ def update(force):
1751
1776
  click.echo("Try running: pip install --upgrade voice-mode")
1752
1777
 
1753
1778
 
1779
+ # Sound Fonts command
1780
+ @audio.command("play")
1781
+ @click.help_option('-h', '--help')
1782
+ @click.option('-t', '--tool', help='Tool name for direct command-line usage')
1783
+ @click.option('-a', '--action', default='start', type=click.Choice(['start', 'end']), help='Action type')
1784
+ @click.option('-s', '--subagent', help='Subagent type (for Task tool)')
1785
+ def play_sound(tool, action, subagent):
1786
+ """Play sound based on tool events (primarily for Claude Code hooks).
1787
+
1788
+ This command is designed to be called by Claude Code hooks to play sounds
1789
+ when tools are used. It reads hook data from stdin by default, or can be
1790
+ used directly with command-line options.
1791
+
1792
+ Examples:
1793
+ echo '{"tool_name":"Task","tool_input":{"subagent_type":"mama-bear"}}' | voicemode play-sound
1794
+ voicemode play-sound --tool Task --action start --subagent mama-bear
1795
+ """
1796
+ import sys
1797
+ from .tools.sound_fonts.player import AudioPlayer
1798
+ from .tools.sound_fonts.hook_handler import (
1799
+ read_hook_data_from_stdin,
1800
+ parse_claude_code_hook
1801
+ )
1802
+
1803
+ # Try to read hook data from stdin first
1804
+ hook_data = None
1805
+ if not sys.stdin.isatty():
1806
+ hook_data = read_hook_data_from_stdin()
1807
+
1808
+ if hook_data:
1809
+ # Parse Claude Code hook format
1810
+ parsed_data = parse_claude_code_hook(hook_data)
1811
+ if not parsed_data:
1812
+ sys.exit(1)
1813
+
1814
+ tool_name = parsed_data["tool_name"]
1815
+ action_type = parsed_data["action"]
1816
+ subagent_type = parsed_data["subagent_type"]
1817
+ metadata = parsed_data["metadata"]
1818
+ else:
1819
+ # Use command-line arguments
1820
+ if not tool:
1821
+ click.echo("Error: --tool is required when not reading from stdin", err=True)
1822
+ sys.exit(1)
1823
+
1824
+ tool_name = tool
1825
+ action_type = action
1826
+ subagent_type = subagent
1827
+ metadata = {}
1828
+
1829
+ # Play the sound
1830
+ player = AudioPlayer()
1831
+ success = player.play_sound_for_event(
1832
+ tool_name=tool_name,
1833
+ action=action_type,
1834
+ subagent_type=subagent_type,
1835
+ metadata=metadata
1836
+ )
1837
+
1838
+ # Silent exit for hooks - don't clutter Claude Code output
1839
+ sys.exit(0 if success else 1)
1840
+
1841
+
1754
1842
  # Completions command
1755
1843
  @voice_mode_main_cli.command()
1756
1844
  @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,209 @@
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
+ # Check if sound fonts are enabled
90
+ from voice_mode.config import SOUNDFONTS_ENABLED
91
+
92
+ if not SOUNDFONTS_ENABLED:
93
+ if debug:
94
+ print(f"[DEBUG] Sound fonts are disabled (VOICEMODE_SOUNDFONTS_ENABLED=false)", file=sys.stderr)
95
+ else:
96
+ # Find sound file using filesystem conventions
97
+ sound_file = find_sound_file(event_name, tool_name, subagent_type)
98
+
99
+ if sound_file:
100
+ if debug:
101
+ print(f"[DEBUG] Found sound file: {sound_file}", file=sys.stderr)
102
+
103
+ # Play the sound
104
+ player = Player()
105
+ success = player.play(str(sound_file))
106
+
107
+ if debug:
108
+ if success:
109
+ print(f"[DEBUG] Sound played successfully", file=sys.stderr)
110
+ else:
111
+ print(f"[DEBUG] Failed to play sound", file=sys.stderr)
112
+ else:
113
+ if debug:
114
+ print(f"[DEBUG] No sound file found for this event", file=sys.stderr)
115
+
116
+ # Always exit 0 to not disrupt Claude Code
117
+ sys.exit(0)
118
+
119
+
120
+ def find_sound_file(event: str, tool: str, subagent: Optional[str] = None) -> Optional[Path]:
121
+ """
122
+ Find sound file using filesystem conventions.
123
+
124
+ Tries paths in order (mp3 preferred over wav for size):
125
+ 1. Most specific: {event}/{tool}/subagent/{subagent}.{mp3,wav} (Task tool only)
126
+ 2. Tool default: {event}/{tool}/default.{mp3,wav}
127
+ 3. Event default: {event}/default.{mp3,wav}
128
+ 4. Global fallback: fallback.{mp3,wav}
129
+
130
+ Args:
131
+ event: Event name (PreToolUse, PostToolUse)
132
+ tool: Tool name (lowercase)
133
+ subagent: Optional subagent type (lowercase)
134
+
135
+ Returns:
136
+ Path to sound file if found, None otherwise
137
+ """
138
+ # Get base path (follow symlink if exists)
139
+ base_path = Path.home() / '.voicemode' / 'soundfonts' / 'current'
140
+
141
+ # Resolve symlink if it exists
142
+ if base_path.is_symlink():
143
+ base_path = base_path.resolve()
144
+
145
+ if not base_path.exists():
146
+ return None
147
+
148
+ # Normalize names to lowercase for filesystem
149
+ event = event.lower() if event else 'pretooluse'
150
+ tool = tool.lower() if tool else 'default'
151
+ subagent = subagent.lower() if subagent else None
152
+
153
+ # Map event names to directory names
154
+ event_map = {
155
+ 'pretooluse': 'PreToolUse',
156
+ 'posttooluse': 'PostToolUse',
157
+ 'start': 'PreToolUse',
158
+ 'end': 'PostToolUse'
159
+ }
160
+ event_dir = event_map.get(event, event)
161
+
162
+ # Build list of paths to try (most specific to least specific)
163
+ paths_to_try = []
164
+
165
+ # 1. Most specific: subagent sound (Task tool only)
166
+ if tool == 'task' and subagent:
167
+ # Try mp3 first (smaller), then wav
168
+ paths_to_try.append(base_path / event_dir / tool / 'subagent' / f'{subagent}.mp3')
169
+ paths_to_try.append(base_path / event_dir / tool / 'subagent' / f'{subagent}.wav')
170
+
171
+ # 2. Tool-specific default
172
+ paths_to_try.append(base_path / event_dir / tool / 'default.mp3')
173
+ paths_to_try.append(base_path / event_dir / tool / 'default.wav')
174
+
175
+ # 3. Event-level default
176
+ paths_to_try.append(base_path / event_dir / 'default.mp3')
177
+ paths_to_try.append(base_path / event_dir / 'default.wav')
178
+
179
+ # 4. Global fallback
180
+ paths_to_try.append(base_path / 'fallback.mp3')
181
+ paths_to_try.append(base_path / 'fallback.wav')
182
+
183
+ # Find first existing file
184
+ for path in paths_to_try:
185
+ if path.exists():
186
+ return path
187
+
188
+ return None
189
+
190
+
191
+ # Keep the old receiver command for backwards compatibility (deprecated)
192
+ @hook.command('receiver', hidden=True)
193
+ @click.argument('tool_name')
194
+ @click.argument('action', type=click.Choice(['start', 'end', 'complete']))
195
+ @click.argument('subagent_type', required=False)
196
+ @click.option('--debug', is_flag=True, help='Enable debug output')
197
+ def receiver_deprecated(tool_name, action, subagent_type, debug):
198
+ """[DEPRECATED] Use stdin-receiver instead."""
199
+ # Map old action to event
200
+ event = 'PreToolUse' if action == 'start' else 'PostToolUse'
201
+
202
+ # Call the new command
203
+ ctx = click.get_current_context()
204
+ ctx.invoke(stdin_receiver,
205
+ tool_name=tool_name,
206
+ action=action if action != 'complete' else 'end',
207
+ subagent_type=subagent_type,
208
+ event=event,
209
+ debug=debug)