From 8c50b14c9d1fd04ff3c6c325cf4e8fdd8f0657d0 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:39:05 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 +++++ README.md | 3 ++ hooks/audio_notification_hook.py | 83 ++++++++++++++++++++++++++++++++ hooks/audio_notification_hook.sh | 5 ++ hooks/hooks.json | 16 ++++++ plugin.lock.json | 53 ++++++++++++++++++++ 6 files changed, 172 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100755 hooks/audio_notification_hook.py create mode 100755 hooks/audio_notification_hook.sh create mode 100644 hooks/hooks.json create mode 100644 plugin.lock.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..aa7663f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "audio-notifications", + "description": "Audio notifications for Claude Code - speaks notification messages out loud", + "version": "1.0.0", + "author": { + "name": "Fred Lacs", + "email": "fredlacs@gmail.com" + }, + "hooks": [ + "./hooks" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ecf8ba --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# audio-notifications + +Audio notifications for Claude Code - speaks notification messages out loud diff --git a/hooks/audio_notification_hook.py b/hooks/audio_notification_hook.py new file mode 100755 index 0000000..74cba8c --- /dev/null +++ b/hooks/audio_notification_hook.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +from dataclasses import dataclass +import json +import shlex +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Optional + +CONFIG_PATH = Path.home() / ".claude" / "audio_notifications.json" + + +def detect_default_command() -> Optional[str]: + for cmd in ["say", "spd-say", "espeak"]: + if shutil.which(cmd): + return cmd + return None + + +@dataclass +class UserConfig: + audio_off: Optional[bool] = None + speech_command: Optional[str] = None + + +def get_user_config() -> UserConfig: + config = UserConfig() + # attempt to load user config, return defaults if fail + try: + user_config_json = json.loads(CONFIG_PATH.read_text()) + except Exception: + return config + + if not isinstance(user_config_json, dict): + return config + + user_audio_off = user_config_json.get("audio_off") + if isinstance(user_audio_off, bool): + config.audio_off = user_audio_off + + user_speech_command = user_config_json.get("speech_command") + if isinstance(user_speech_command, str): + config.speech_command = user_speech_command.strip() + + return config + + +def main(): + user_config = get_user_config() + if user_config.audio_off is True: + sys.exit(0) + + try: + hook_data = json.loads(sys.stdin.read()) + except Exception: + sys.exit(2) + if not isinstance(hook_data, dict): + sys.exit(2) + + # only trigger audio if hook is a notification + if hook_data.get("hook_event_name") != "Notification": + sys.exit(0) + + message = hook_data.get("message") + if not isinstance(message, str): + sys.exit(2) + message = message.strip() + + # attempt command set by user if available, else default options + speech_command = user_config.speech_command or detect_default_command() + if not isinstance(speech_command, str): + sys.exit(2) + speech_command = shlex.split(speech_command) + + try: + subprocess.run(speech_command + [message], check=True, timeout=10) + except Exception: + sys.exit(2) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/hooks/audio_notification_hook.sh b/hooks/audio_notification_hook.sh new file mode 100755 index 0000000..e5060b0 --- /dev/null +++ b/hooks/audio_notification_hook.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +# bash wrapper used to run python in background +cat | python3 "${CLAUDE_PLUGIN_ROOT}/hooks/audio_notification_hook.py" >/dev/null 2>&1 & +exit 0 \ No newline at end of file diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..34f72ff --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "description": "Audio notification hook that warns user of notification", + "hooks": { + "Notification": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/audio_notification_hook.sh" + } + ] + } + ] + } +} diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..91320c9 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,53 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:marcioaltoe/claude-craftkit:plugins/audio-notifications", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "2135c828ed71f66db9cdee715eb5c86eaa37b6f3", + "treeHash": "386e651bb16f7fed1fe7df1ce6edbe63490a66277c866ba5e59f9e03e7f3d026", + "generatedAt": "2025-11-28T10:27:00.624830Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "audio-notifications", + "description": "Audio notifications for Claude Code - speaks notification messages out loud", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "ee059741b5d3fc50192a2e1b9a0741ab7c665928a690790de168a90e428f6f55" + }, + { + "path": "hooks/audio_notification_hook.sh", + "sha256": "9ab93e438e473bfe54998a2b1e4669357f00654cfe8ab750cf8b50a20b75866b" + }, + { + "path": "hooks/audio_notification_hook.py", + "sha256": "298e881c3c79cdb71c7a215ead7659c0e2995e49ca808e085abd5dc71bd220d8" + }, + { + "path": "hooks/hooks.json", + "sha256": "36e1c913d78aa12c6187435abf633829ddc7560c5d95e6b410ab6f471710b905" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "f743c1210d78260f06f5942beebe50fed3a8cae645c889f7f71f293dc0bf1452" + } + ], + "dirSha256": "386e651bb16f7fed1fe7df1ce6edbe63490a66277c866ba5e59f9e03e7f3d026" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file