diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8eee1bd..9123fd7 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,13 +6,13 @@ }, "metadata": { "description": "AI-operated audio notification system for Claude Code. 26 hooks, native matcher routing, TTS, webhook fan-out, status line, focus flow, rate-limit alerts.", - "version": "5.1.1" + "version": "5.1.2" }, "plugins": [ { "name": "audio-hooks", "source": "./plugins/audio-hooks", - "version": "5.1.1", + "version": "5.1.2", "description": "Audio notifications + AI-controlled config for every Claude Code event", "author": { "name": "Chan Meng", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e192e1..8ddc03d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to Claude Code Audio Hooks will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.1.2] - 2026-04-20 + +Windows audio-playback fix. Every default clip >= ~3.0 s was silently truncated on Windows and WSL because all four PowerShell snippets in `play_audio_windows` and `play_audio_wsl` used a **fixed** `Start-Sleep -Seconds 3` (4 s for WSL) before calling `$player.Stop(); $player.Close()`. The bundled `audio/default/permission-request.mp3` is ~3.4 s — users heard the last ~0.4 s cut off every time Claude Code asked for permission. `elicitation.mp3` (~3.1 s) was also clipped; `subagent-start.mp3` and `notification-urgent.mp3` were on the edge. + +### Fixed + +- **`hooks/hook_runner.py` — `play_audio_windows` and `play_audio_wsl`** ([#14](https://github.com/ChanMeng666/claude-code-audio-hooks/issues/14), reported by [@Basdanucha](https://github.com/Basdanucha)). All four sites (PowerShell `-Command`, PowerShell `-File` heredoc, WMPlayer.OCX COM, WSL `-Command`) now poll the media player for the actual clip length and sleep for `duration + 500 ms` tail buffer before tearing down the player. For PresentationCore MediaPlayer, we poll `$player.NaturalDuration.HasTimeSpan` with a 1.5 s ceiling (Open() is async), then use `TotalMilliseconds`. For WMPlayer.OCX, we poll `$w.currentMedia.duration`. If the media never reports a duration (corrupt file, etc.), we fall back to `Start-Sleep -Seconds 10` — generous enough to cover any plausible default clip, still bounded so the PowerShell host process doesn't leak. The Python subprocess layer was already fire-and-forget (`subprocess.Popen`) — the fix closes the gap that was *inside* the PowerShell command string. macOS `afplay` and Linux `mpg123`/`ffplay`/`paplay`/`aplay` are unaffected: those players block until playback completes by default. + ## [5.1.1] - 2026-04-18 Critical import-time crash fix plus regression-prevention CI. Everyone on v5.0.3 or v5.1.0 should upgrade. diff --git a/CLAUDE.md b/CLAUDE.md index c9ede9d..71b0af6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code Audio Hooks — AI Operator Guide -> **Version:** 5.1.1 | **Last Updated:** 2026-04-18 +> **Version:** 5.1.2 | **Last Updated:** 2026-04-20 This document is the canonical machine-facing guide for the claude-code-audio-hooks project. It is written for **Claude Code (and other AI agents) as the operator** — not for humans. A human user says "install audio hooks for me" or "snooze audio for an hour" and you do everything via the project's structured CLI without any human-in-the-loop confirmation, menu navigation, or log file reading. @@ -317,6 +317,7 @@ After editing any canonical file in `/hooks/`, `/bin/`, `/audio/`, or `/config/` | Version | Date | Highlights | |---|---|---| +| 5.1.2 | 2026-04-20 | **Fix:** Windows playback truncated clips > 3 s because every PowerShell snippet in `play_audio_windows` and `play_audio_wsl` used a fixed `Start-Sleep -Seconds 3` (the default `permission-request.mp3` is ~3.4 s — the last ~0.4 s was cut off; `elicitation.mp3` at ~3.1 s was also affected) ([#14](https://github.com/ChanMeng666/claude-code-audio-hooks/issues/14)). All four snippets now poll `NaturalDuration.HasTimeSpan` (or `currentMedia.duration` for WMPlayer.OCX) and sleep for the real clip length + 500 ms tail buffer, falling back to 10 s only if the media never reports a duration. | | 5.1.1 | 2026-04-18 | **Critical fix:** `hook_runner.py` crashed on import with `NameError: name 'Tuple' is not defined`, blocking every `audio-hooks` subcommand on v5.0.3 and v5.1.0 ([#10](https://github.com/ChanMeng666/claude-code-audio-hooks/issues/10)). Adds CI import-smoke workflow (`.github/workflows/smoke.yml`) — 9-job matrix (Ubuntu/Windows/macOS × Python 3.9/3.12/3.13) plus plugin-in-sync check, runs on every push and PR. All version references realigned to `5.1.1` (v5.1.0 had shipped with in-tree strings still reading `5.0.3`). | | 5.1.0 | 2026-04-13 | **Context window monitor.** Status line gains real-time context usage tracking with colour-coded thresholds (🟢 <50% safe, 🟡 50-80% should `/compact`, 🔴 >80% agent perf degrades). 10 customisable segments (`model`, `version`, `sounds`, `webhook`, `theme`, `snooze`, `focus`, `branch`, `api_quota`, `context`) controlled via `statusline_settings.visible_segments`. | | 5.0.0 | 2026-04-11 | **AI-first redesign.** Single JSON `audio-hooks` CLI binary (27 subcommands). NDJSON structured logging with stable error codes. SKILL for natural-language project management. Plugin packaging (`/plugin install audio-hooks@chanmeng-audio-hooks`). Status line script with rate-limit bar. Four new hook events (PermissionDenied, CwdChanged, FileChanged, TaskCreated → 26 total). Native matcher routing via synthetic event names. New stdin field parsing (`last_assistant_message`, `worktree`, `agent`, `rate_limits`, `notification_type`, `error_type`, `source`). TTS speak_assistant_message. Rate-limit pre-check with marker debounce. Async webhook subprocess dispatch. Webhook payload schema `audio-hooks.webhook.v1`. All scripts auto-non-interactive on non-TTY. | diff --git a/README.md b/README.md index c573fff..a443339 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ You type one slash command at install time. Then natural language forever.
26 hook events, 2 audio themes, rate-limit alerts, webhooks, TTS, context monitor — all operated by Claude Code on your behalf. [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Version](https://img.shields.io/badge/version-5.1.1-blue.svg)](https://github.com/ChanMeng666/claude-code-audio-hooks) +[![Version](https://img.shields.io/badge/version-5.1.2-blue.svg)](https://github.com/ChanMeng666/claude-code-audio-hooks) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-green.svg)](https://github.com/ChanMeng666/claude-code-audio-hooks) [![Claude Code](https://img.shields.io/badge/Claude_Code-v2.1.80%2B-brightgreen.svg)](https://claude.ai/download) [![Plugin](https://img.shields.io/badge/install-just_talk_to_Claude-purple.svg)](#-install-in-60-seconds) @@ -286,7 +286,7 @@ sequenceDiagram You->>CC: Show me the last 20 errors and clear the log. CC-->>You: 2 errors found (WEBHOOK_TIMEOUT). Log cleared. You->>CC: What version of audio-hooks am I running? - CC-->>You: v5.1.1, plugin install. + CC-->>You: v5.1.2, plugin install. You->>CC: Please uninstall audio-hooks completely. CC-->>You: Plugin uninstalled. All hooks removed. end @@ -505,7 +505,7 @@ Real-time context window and API quota bars — color-coded warnings before Clau

```text -[Opus] Audio Hooks v5.1.1 | 6/26 Sounds | Webhook: ntfy | Theme: Voice +[Opus] Audio Hooks v5.1.2 | 6/26 Sounds | Webhook: ntfy | Theme: Voice [MUTED 23m] feat/audio-v5 API Quota: 78% Context: 65% /compact ``` diff --git a/bin/audio-hooks.py b/bin/audio-hooks.py index 0f057e6..eacb75b 100644 --- a/bin/audio-hooks.py +++ b/bin/audio-hooks.py @@ -132,7 +132,7 @@ def require_project_root() -> int: # Project state — version, install detection, hook catalogue # --------------------------------------------------------------------------- -PROJECT_VERSION = "5.1.1" +PROJECT_VERSION = "5.1.2" # Canonical hook catalogue. Order matches CLAUDE.md and the install scripts. HOOK_CATALOG: List[Dict[str, Any]] = [ diff --git a/config/default_preferences.json b/config/default_preferences.json index 86edfc4..d008325 100644 --- a/config/default_preferences.json +++ b/config/default_preferences.json @@ -1,10 +1,10 @@ { "$schema": "./user_preferences.schema.json", - "_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.1)", - "_version": "5.1.1", + "_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.2)", + "_version": "5.1.2", "_description": "This file is the default template for user_preferences.json. Plugin installs auto-copy this to ${CLAUDE_PLUGIN_DATA}/user_preferences.json on first read. AI agents should NOT edit this file directly — use 'audio-hooks set ' instead.", - "version": "5.1.1", + "version": "5.1.2", "_comment_audio_theme": "Audio theme: 'default' for voice recordings, 'custom' for non-voice chimes. Change this one line to switch all audio.", "audio_theme": "default", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2b719fc..d79ac65 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -207,7 +207,7 @@ Native matcher routing happens at the `settings.json` layer (Claude Code's match Two-line bottom bar registered in `~/.claude/settings.json` via `audio-hooks statusline install`. Reads stdin JSON Claude Code provides (model name, session_id, workspace.git_worktree, rate_limits, context_window) and emits two lines of plain text with ANSI colors: ```text -[Opus] 🔊 Audio Hooks v5.1.1 | 6/26 Sounds | Webhook: ntfy | Theme: Voice +[Opus] 🔊 Audio Hooks v5.1.2 | 6/26 Sounds | Webhook: ntfy | Theme: Voice [MUTED 23m] 🌿 feat/audio-v5 ████░░░░ API Quota: 78% █████░░░ Context: 65% ⚠️ /compact ``` diff --git a/hooks/hook_runner.py b/hooks/hook_runner.py index c03aaf4..037b32d 100644 --- a/hooks/hook_runner.py +++ b/hooks/hook_runner.py @@ -31,7 +31,7 @@ # Version used for auto-sync: when the installed copy in ~/.claude/hooks/ # detects a newer version in the project directory, it self-updates. -HOOK_RUNNER_VERSION = "5.1.1" +HOOK_RUNNER_VERSION = "5.1.2" # ============================================================================= # STRUCTURED LOGGING (NDJSON) @@ -994,15 +994,23 @@ def play_audio_windows(audio_file: Path) -> bool: log_debug(f"Windows audio playback: {win_path}") - # Method 1: Direct PowerShell command with MediaPlayer + # Method 1: Direct PowerShell command with MediaPlayer. + # MediaPlayer.Open() is async: we poll NaturalDuration.HasTimeSpan with a + # short ceiling, then sleep for the real clip length + a tail buffer so + # Stop()/Close() don't truncate playback (issue #14). Fallback to a + # generous-but-bounded sleep if Open() never resolves. try: ps_cmd = ( 'Add-Type -AssemblyName presentationCore; ' '$p = New-Object System.Windows.Media.MediaPlayer; ' f'$p.Open("{win_path_escaped}"); ' - 'Start-Sleep -Milliseconds 500; ' + '$deadline = (Get-Date).AddMilliseconds(1500); ' + 'while (-not $p.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) ' + '{ Start-Sleep -Milliseconds 50 }; ' '$p.Play(); ' - 'Start-Sleep -Seconds 3; ' + 'if ($p.NaturalDuration.HasTimeSpan) ' + '{ Start-Sleep -Milliseconds ([int]($p.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) } ' + 'else { Start-Sleep -Seconds 10 }; ' '$p.Stop(); $p.Close()' ) proc = subprocess.Popen( @@ -1027,9 +1035,16 @@ def play_audio_windows(audio_file: Path) -> bool: Add-Type -AssemblyName presentationCore $player = New-Object System.Windows.Media.MediaPlayer $player.Open("{win_path_escaped}") -Start-Sleep -Milliseconds 500 +$deadline = (Get-Date).AddMilliseconds(1500) +while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{ + Start-Sleep -Milliseconds 50 +}} $player.Play() -Start-Sleep -Seconds 3 +if ($player.NaturalDuration.HasTimeSpan) {{ + Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) +}} else {{ + Start-Sleep -Seconds 10 +}} $player.Stop() $player.Close() Remove-Item -Path $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue @@ -1050,7 +1065,15 @@ def play_audio_windows(audio_file: Path) -> bool: # Method 3: Use WMPlayer.OCX COM object try: - ps_cmd = f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; Start-Sleep -Seconds 3' + ps_cmd = ( + f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; ' + '$deadline = (Get-Date).AddMilliseconds(1500); ' + 'while (($w.currentMedia -eq $null -or $w.currentMedia.duration -eq 0) ' + '-and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 50 }; ' + 'if ($w.currentMedia -ne $null -and $w.currentMedia.duration -gt 0) ' + '{ Start-Sleep -Milliseconds ([int]($w.currentMedia.duration * 1000 + 500)) } ' + 'else { Start-Sleep -Seconds 10 }' + ) proc = subprocess.Popen( ["powershell.exe", "-Command", ps_cmd], stdout=subprocess.DEVNULL, @@ -1208,9 +1231,16 @@ def play_audio_wsl(audio_file: Path) -> bool: Add-Type -AssemblyName presentationCore $player = New-Object System.Windows.Media.MediaPlayer $player.Open("{win_path_escaped}") -Start-Sleep -Milliseconds 500 +$deadline = (Get-Date).AddMilliseconds(1500) +while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{ + Start-Sleep -Milliseconds 50 +}} $player.Play() -Start-Sleep -Seconds 4 +if ($player.NaturalDuration.HasTimeSpan) {{ + Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) +}} else {{ + Start-Sleep -Seconds 10 +}} $player.Stop() $player.Close() Remove-Item -Path "{win_path_escaped}" -ErrorAction SilentlyContinue diff --git a/plugins/audio-hooks/.claude-plugin/plugin.json b/plugins/audio-hooks/.claude-plugin/plugin.json index 4a9e93e..02567c3 100644 --- a/plugins/audio-hooks/.claude-plugin/plugin.json +++ b/plugins/audio-hooks/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "audio-hooks", - "version": "5.1.1", + "version": "5.1.2", "description": "Audio notifications + AI-controlled config for every Claude Code event. 26 hooks, native matcher routing, TTS, webhooks, status line, focus flow, rate-limit alerts.", "author": { "name": "Chan Meng", diff --git a/plugins/audio-hooks/bin/audio-hooks.py b/plugins/audio-hooks/bin/audio-hooks.py index 0f057e6..eacb75b 100644 --- a/plugins/audio-hooks/bin/audio-hooks.py +++ b/plugins/audio-hooks/bin/audio-hooks.py @@ -132,7 +132,7 @@ def require_project_root() -> int: # Project state — version, install detection, hook catalogue # --------------------------------------------------------------------------- -PROJECT_VERSION = "5.1.1" +PROJECT_VERSION = "5.1.2" # Canonical hook catalogue. Order matches CLAUDE.md and the install scripts. HOOK_CATALOG: List[Dict[str, Any]] = [ diff --git a/plugins/audio-hooks/config/default_preferences.json b/plugins/audio-hooks/config/default_preferences.json index 86edfc4..d008325 100644 --- a/plugins/audio-hooks/config/default_preferences.json +++ b/plugins/audio-hooks/config/default_preferences.json @@ -1,10 +1,10 @@ { "$schema": "./user_preferences.schema.json", - "_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.1)", - "_version": "5.1.1", + "_comment": "Claude Code Audio Hooks - Default Configuration Template (v5.1.2)", + "_version": "5.1.2", "_description": "This file is the default template for user_preferences.json. Plugin installs auto-copy this to ${CLAUDE_PLUGIN_DATA}/user_preferences.json on first read. AI agents should NOT edit this file directly — use 'audio-hooks set ' instead.", - "version": "5.1.1", + "version": "5.1.2", "_comment_audio_theme": "Audio theme: 'default' for voice recordings, 'custom' for non-voice chimes. Change this one line to switch all audio.", "audio_theme": "default", diff --git a/plugins/audio-hooks/hooks/hook_runner.py b/plugins/audio-hooks/hooks/hook_runner.py index c03aaf4..037b32d 100644 --- a/plugins/audio-hooks/hooks/hook_runner.py +++ b/plugins/audio-hooks/hooks/hook_runner.py @@ -31,7 +31,7 @@ # Version used for auto-sync: when the installed copy in ~/.claude/hooks/ # detects a newer version in the project directory, it self-updates. -HOOK_RUNNER_VERSION = "5.1.1" +HOOK_RUNNER_VERSION = "5.1.2" # ============================================================================= # STRUCTURED LOGGING (NDJSON) @@ -994,15 +994,23 @@ def play_audio_windows(audio_file: Path) -> bool: log_debug(f"Windows audio playback: {win_path}") - # Method 1: Direct PowerShell command with MediaPlayer + # Method 1: Direct PowerShell command with MediaPlayer. + # MediaPlayer.Open() is async: we poll NaturalDuration.HasTimeSpan with a + # short ceiling, then sleep for the real clip length + a tail buffer so + # Stop()/Close() don't truncate playback (issue #14). Fallback to a + # generous-but-bounded sleep if Open() never resolves. try: ps_cmd = ( 'Add-Type -AssemblyName presentationCore; ' '$p = New-Object System.Windows.Media.MediaPlayer; ' f'$p.Open("{win_path_escaped}"); ' - 'Start-Sleep -Milliseconds 500; ' + '$deadline = (Get-Date).AddMilliseconds(1500); ' + 'while (-not $p.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) ' + '{ Start-Sleep -Milliseconds 50 }; ' '$p.Play(); ' - 'Start-Sleep -Seconds 3; ' + 'if ($p.NaturalDuration.HasTimeSpan) ' + '{ Start-Sleep -Milliseconds ([int]($p.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) } ' + 'else { Start-Sleep -Seconds 10 }; ' '$p.Stop(); $p.Close()' ) proc = subprocess.Popen( @@ -1027,9 +1035,16 @@ def play_audio_windows(audio_file: Path) -> bool: Add-Type -AssemblyName presentationCore $player = New-Object System.Windows.Media.MediaPlayer $player.Open("{win_path_escaped}") -Start-Sleep -Milliseconds 500 +$deadline = (Get-Date).AddMilliseconds(1500) +while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{ + Start-Sleep -Milliseconds 50 +}} $player.Play() -Start-Sleep -Seconds 3 +if ($player.NaturalDuration.HasTimeSpan) {{ + Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) +}} else {{ + Start-Sleep -Seconds 10 +}} $player.Stop() $player.Close() Remove-Item -Path $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue @@ -1050,7 +1065,15 @@ def play_audio_windows(audio_file: Path) -> bool: # Method 3: Use WMPlayer.OCX COM object try: - ps_cmd = f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; Start-Sleep -Seconds 3' + ps_cmd = ( + f'$w = New-Object -ComObject WMPlayer.OCX; $w.URL = "{win_path_escaped}"; ' + '$deadline = (Get-Date).AddMilliseconds(1500); ' + 'while (($w.currentMedia -eq $null -or $w.currentMedia.duration -eq 0) ' + '-and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 50 }; ' + 'if ($w.currentMedia -ne $null -and $w.currentMedia.duration -gt 0) ' + '{ Start-Sleep -Milliseconds ([int]($w.currentMedia.duration * 1000 + 500)) } ' + 'else { Start-Sleep -Seconds 10 }' + ) proc = subprocess.Popen( ["powershell.exe", "-Command", ps_cmd], stdout=subprocess.DEVNULL, @@ -1208,9 +1231,16 @@ def play_audio_wsl(audio_file: Path) -> bool: Add-Type -AssemblyName presentationCore $player = New-Object System.Windows.Media.MediaPlayer $player.Open("{win_path_escaped}") -Start-Sleep -Milliseconds 500 +$deadline = (Get-Date).AddMilliseconds(1500) +while (-not $player.NaturalDuration.HasTimeSpan -and (Get-Date) -lt $deadline) {{ + Start-Sleep -Milliseconds 50 +}} $player.Play() -Start-Sleep -Seconds 4 +if ($player.NaturalDuration.HasTimeSpan) {{ + Start-Sleep -Milliseconds ([int]($player.NaturalDuration.TimeSpan.TotalMilliseconds + 500)) +}} else {{ + Start-Sleep -Seconds 10 +}} $player.Stop() $player.Close() Remove-Item -Path "{win_path_escaped}" -ErrorAction SilentlyContinue diff --git a/plugins/audio-hooks/skills/audio-hooks/SKILL.md b/plugins/audio-hooks/skills/audio-hooks/SKILL.md index b4377ce..6916d1f 100644 --- a/plugins/audio-hooks/skills/audio-hooks/SKILL.md +++ b/plugins/audio-hooks/skills/audio-hooks/SKILL.md @@ -110,7 +110,7 @@ The status line displays real-time audio-hooks state and context window usage at After installing, the status line updates every 60 seconds and shows two lines: ``` -[Opus] 🔊 Audio Hooks v5.1.1 | 6/26 Sounds | Webhook: off | Theme: Voice +[Opus] 🔊 Audio Hooks v5.1.2 | 6/26 Sounds | Webhook: off | Theme: Voice 🌿 main ████░░░░ API Quota: 60% █████░░░ Context: 65% ⚠️ /compact ```