Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# audio-notifications
|
||||
|
||||
Audio notifications for Claude Code - speaks notification messages out loud
|
||||
83
hooks/audio_notification_hook.py
Executable file
83
hooks/audio_notification_hook.py
Executable 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()
|
||||
5
hooks/audio_notification_hook.sh
Executable file
5
hooks/audio_notification_hook.sh
Executable 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
16
hooks/hooks.json
Normal 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
53
plugin.lock.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user