Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:39:05 +08:00
commit 8c50b14c9d
6 changed files with 172 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# audio-notifications
Audio notifications for Claude Code - speaks notification messages out loud

View File

@@ -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()

View File

@@ -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

16
hooks/hooks.json Normal file
View File

@@ -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"
}
]
}
]
}
}

53
plugin.lock.json Normal file
View File

@@ -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": []
}
}