diff --git a/plugins/README.md b/plugins/README.md index cf4a21ecc5..975bcb07cc 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -21,6 +21,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do | [frontend-design](./frontend-design/) | Create distinctive, production-grade frontend interfaces that avoid generic AI aesthetics | **Skill:** `frontend-design` - Auto-invoked for frontend work, providing guidance on bold design choices, typography, animations, and visual details | | [hookify](./hookify/) | Easily create custom hooks to prevent unwanted behaviors by analyzing conversation patterns or explicit instructions | **Commands:** `/hookify`, `/hookify:list`, `/hookify:configure`, `/hookify:help`
**Agent:** `conversation-analyzer` - Analyzes conversations for problematic behaviors
**Skill:** `writing-rules` - Guidance on hookify rule syntax | | [learning-output-style](./learning-output-style/) | Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style) | **Hook:** SessionStart - Encourages users to write meaningful code (5-10 lines) at decision points while receiving educational insights | +| [notifier](./notifier/) | Cross-platform notifications when Claude Code completes tasks, requests permissions, or waits for input (macOS, Linux, Windows Native/WSL2) | **Hooks:** UserPromptSubmit, Stop, Notification, SessionEnd - Smart notifications for long-running tasks
**Command:** `/notifier` - Configure language, duration threshold, and preview length | | [plugin-dev](./plugin-dev/) | Comprehensive toolkit for developing Claude Code plugins with 7 expert skills and AI-assisted creation | **Command:** `/plugin-dev:create-plugin` - 8-phase guided workflow for building plugins
**Agents:** `agent-creator`, `plugin-validator`, `skill-reviewer`
**Skills:** Hook development, MCP integration, plugin structure, settings, commands, agents, and skill development | | [pr-review-toolkit](./pr-review-toolkit/) | Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification | **Command:** `/pr-review-toolkit:review-pr` - Run with optional review aspects (comments, tests, errors, types, code, simplify, all)
**Agents:** `comment-analyzer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer`, `code-reviewer`, `code-simplifier` | | [ralph-wiggum](./ralph-wiggum/) | Interactive self-referential AI loops for iterative development. Claude works on the same task repeatedly until completion | **Commands:** `/ralph-loop`, `/cancel-ralph` - Start/stop autonomous iteration loops
**Hook:** Stop - Intercepts exit attempts to continue iteration | diff --git a/plugins/notifier/.claude-plugin/plugin.json b/plugins/notifier/.claude-plugin/plugin.json new file mode 100644 index 0000000000..a17fc9b960 --- /dev/null +++ b/plugins/notifier/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "notifier", + "description": "Get notified when Claude Code completes tasks, requests permissions, or waits for input. Supports macOS, Linux, and Windows (Native/WSL2).", + "version": "2.0.0", + "author": { + "name": "Joo-Seung Koo", + "email": "dev.jooseung@gmail.com" + }, + "repository": "https://github.com/js-koo/claude-code-notifier", + "keywords": ["notifications", "productivity", "cross-platform"] +} diff --git a/plugins/notifier/README.md b/plugins/notifier/README.md new file mode 100644 index 0000000000..3fec0b0c25 --- /dev/null +++ b/plugins/notifier/README.md @@ -0,0 +1,154 @@ +# claude-code-notifier + +Get notified when Claude Code completes your tasks (Windows/macOS/Linux) + +## Features + +- **Cross-platform**: Windows (Native/WSL2), macOS, and Linux support +- **Smart notifications**: Only notifies for tasks taking 20+ seconds +- **Prompt preview**: Shows the first few characters of your prompt +- **Session-aware**: Multiple Claude Code sessions work independently +- **Zero config**: Works out of the box with sensible defaults +- **Slash command**: Easy configuration with `/notifier` command + +## Tested Environments + +| Platform | Status | +|----------|--------| +| macOS | ✅ Tested | +| Linux | ✅ Tested (Docker) | +| Windows (Native) | ✅ Tested | +| Windows (WSL2) | ✅ Tested | + +> Found an issue? Please [open an issue](https://github.com/js-koo/claude-code-notifier/issues)! + +## Requirements + +- [Claude Code CLI](https://claude.ai/code) +- `jq` (JSON processor) +- **Linux only**: `libnotify-bin` (for `notify-send`) + +## Installation + +This plugin is included in the official Claude Code plugins directory. + +For standalone installation, see [claude-code-notifier](https://github.com/js-koo/claude-code-notifier). + +## Configuration + +### Using Slash Command (Recommended) + +Use the `/notifier` command in Claude Code: + +| Command | Description | +|---------|-------------| +| `/notifier help` | Show available commands | +| `/notifier status` | Show current configuration | +| `/notifier lang ` | Set language (en: English, ko: 한국어) | +| `/notifier duration ` | Set minimum task duration (default: 20) | +| `/notifier preview ` | Set prompt preview length (default: 45) | +| `/notifier test` | Send a test notification | +| `/notifier uninstall` | Uninstall claude-code-notifier | + +### Manual Configuration + +Edit `config.sh` in the plugin's `hooks-handlers/` directory: + +```bash +# Language setting: "en" (English) or "ko" (한국어) +NOTIFIER_LANG="en" + +# Minimum task duration (seconds) to trigger notification +MIN_DURATION_SECONDS=20 + +# Number of characters to preview from the prompt +PROMPT_PREVIEW_LENGTH=45 +``` + +### Configuration Options Explained + +| Option | Default | Description | +|--------|---------|-------------| +| `NOTIFIER_LANG` | `en` | UI language. `en` for English, `ko` for Korean. Affects notification messages and slash command responses. | +| `MIN_DURATION_SECONDS` | `20` | Minimum task duration to trigger notification. Tasks completing faster than this won't show notifications. Set to `0` to notify on every task. | +| `PROMPT_PREVIEW_LENGTH` | `45` | Number of characters to show from your original prompt in the notification. | + +### Notification Messages + +Messages are automatically set based on `NOTIFIER_LANG`: + +| Event | English | 한국어 | +|-------|---------|--------| +| Task completed | Task completed! | 작업 완료! | +| Permission required | Permission required! | 권한 필요! | +| Waiting for input | Waiting for input... | 입력 대기 중... | + +### Examples + +**Quick tasks without notifications:** +```bash +# Only notify for tasks taking 60+ seconds +/notifier duration 60 +``` + +**Always notify:** +```bash +# Notify on every task completion +/notifier duration 0 +``` + +**Longer prompt preview:** +```bash +# Show more of the original prompt +/notifier preview 100 +``` + +**Switch to Korean:** +```bash +/notifier lang ko +``` + +## How It Works + +This tool uses Claude Code's [hooks system](https://docs.anthropic.com/en/docs/claude-code/hooks) to: + +1. **UserPromptSubmit**: Save the prompt and start time when you submit a task +2. **Stop**: Show a notification when Claude Code finishes (if duration > threshold) +3. **Notification**: Alert when permission is required or Claude is waiting for input +4. **SessionEnd**: Clean up temporary files when the session ends + +Session data is stored in `~/.claude-code-notifier/data/`. + +## Troubleshooting + +### Notifications not appearing + +**Windows (WSL)**: +- Ensure Windows notifications are enabled in Settings > System > Notifications +- Check that Focus Assist is not blocking notifications + +**macOS**: +- Allow notifications from "Script Editor" in System Preferences > Notifications + +**Linux**: +- Install `libnotify-bin`: `sudo apt install libnotify-bin` +- Check if `notify-send` works: `notify-send "Test" "Hello"` + +### jq not found + +Install jq using your package manager (see Installation section). + +### WSL path issues + +If you're using a non-default WSL distribution, the path conversion should still work automatically. If issues persist, check that `wslpath` is available. + +## License + +MIT License - see the [original repository](https://github.com/js-koo/claude-code-notifier) for details. + +## Contributing + +Contributions are welcome! + +- **Bug reports / Feature requests**: [Open an Issue](https://github.com/js-koo/claude-code-notifier/issues) +- **Code contributions**: Submit a Pull Request diff --git a/plugins/notifier/commands/notifier.md b/plugins/notifier/commands/notifier.md new file mode 100644 index 0000000000..444149e0ad --- /dev/null +++ b/plugins/notifier/commands/notifier.md @@ -0,0 +1,120 @@ +--- +allowed-tools: Bash(sed:*), Bash(cat:*), Bash(grep:*), Bash(mkdir:*), Bash(echo:*), Bash(~/.claude-code-notifier/uninstall.sh) +argument-hint: [value] +description: Configure claude-code-notifier settings +--- + +# Claude Code Notifier Configuration + +## Available Commands + +| Command | Description | +|---------|-------------| +| `help` | Show this help message | +| `status` | Show current configuration | +| `lang ` | Set language (en: English, ko: 한국어) | +| `duration ` | Set minimum task duration (default: 20) | +| `preview ` | Set prompt preview length (default: 45) | +| `test` | Send a test notification | +| `uninstall` | Uninstall claude-code-notifier | + +## Language Detection + +First, read NOTIFIER_LANG from ~/.claude-code-notifier/hooks-handlers/config.sh to determine response language. + +## Task + +User command: `$ARGUMENTS` + +Perform the appropriate action based on the detected language: + +### 1. `status` +Display current settings: + +**English (en):** +``` +📊 Current Configuration +━━━━━━━━━━━━━━━━━━━━━━━━ +🌐 Language: {en|ko} +⏱️ Min Duration: {value} seconds +📝 Preview Length: {value} characters +━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +**Korean (ko):** +``` +📊 현재 설정 +━━━━━━━━━━━━━━━━━━━━━━━━ +🌐 언어: {en|ko} +⏱️ 최소 시간: {value}초 +📝 미리보기 길이: {value}자 +━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 2. `lang ` +- Validate that the value is "en" or "ko" +- Update NOTIFIER_LANG in config.sh using sed +- **en:** "✅ Language updated to {value}" +- **ko:** "✅ 언어가 {value}(으)로 변경되었습니다" + +### 3. `duration ` +- Validate that the value is a positive number +- Update MIN_DURATION_SECONDS in config.sh using sed +- **en:** "✅ Duration updated to {value} seconds" +- **ko:** "✅ 최소 시간이 {value}초로 변경되었습니다" + +### 4. `preview ` +- Validate that the value is a positive number +- Update PROMPT_PREVIEW_LENGTH in config.sh using sed +- **en:** "✅ Preview length updated to {value} characters" +- **ko:** "✅ 미리보기 길이가 {value}자로 변경되었습니다" + +### 5. `test` +Run these commands: +```bash +mkdir -p ~/.claude-code-notifier/data +echo "Test notification from /notifier" > ~/.claude-code-notifier/data/prompt-test.txt +echo $(date +%s) > ~/.claude-code-notifier/data/timestamp-test.txt +echo '{"session_id": "test"}' | ~/.claude-code-notifier/hooks-handlers/notify.sh +``` +- **en:** "✅ Test notification sent!" +- **ko:** "✅ 테스트 알림을 전송했습니다!" + +### 6. `uninstall` +Run the uninstall script: +```bash +~/.claude-code-notifier/uninstall.sh +``` + +**English (en):** +``` +✅ claude-code-notifier has been uninstalled. + +⚠️ Please restart Claude Code to complete the uninstallation. + (Type /exit or press Ctrl+C) +``` + +**Korean (ko):** +``` +✅ claude-code-notifier가 삭제되었습니다. + +⚠️ 삭제를 완료하려면 Claude Code를 재시작하세요. + (/exit 입력 또는 Ctrl+C) +``` + +### 7. `help` or empty/invalid command +Show the available commands table above. + +### Error Handling + +**English (en):** +- If config.sh not found: "❌ claude-code-notifier is not installed. Run install.sh first." +- If invalid number provided: "❌ Please provide a valid positive number." +- If invalid language provided: "❌ Please provide a valid language (en or ko)." +- If unknown command: "❌ Unknown command. Use `/notifier help` to see available commands." + +**Korean (ko):** +- If config.sh not found: "❌ claude-code-notifier가 설치되지 않았습니다. install.sh를 먼저 실행하세요." +- If invalid number provided: "❌ 유효한 양수를 입력하세요." +- If invalid language provided: "❌ 유효한 언어를 입력하세요 (en 또는 ko)." +- If unknown command: "❌ 알 수 없는 명령어입니다. `/notifier help`로 사용 가능한 명령어를 확인하세요." diff --git a/plugins/notifier/hooks-handlers/cleanup-session.sh b/plugins/notifier/hooks-handlers/cleanup-session.sh new file mode 100755 index 0000000000..f9397863fb --- /dev/null +++ b/plugins/notifier/hooks-handlers/cleanup-session.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# cleanup-session.sh - Cleans up session files when Claude Code session ends +# Hook: SessionEnd + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Read stdin (JSON from Claude Code) +INPUT=$(cat) + +SESSION_ID=$(get_session_id "$INPUT") + +# Clean up session files +rm -f "$DATA_DIR/prompt-${SESSION_ID}.txt" +rm -f "$DATA_DIR/timestamp-${SESSION_ID}.txt" + +exit 0 diff --git a/plugins/notifier/hooks-handlers/common.sh b/plugins/notifier/hooks-handlers/common.sh new file mode 100755 index 0000000000..ce783d9ee0 --- /dev/null +++ b/plugins/notifier/hooks-handlers/common.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# common.sh - Shared utilities for claude-code-notifier +# Sourced by notifier scripts + +DATA_DIR="$HOME/.claude-code-notifier/data" + +# Extract value from JSON using jq or fallback to grep/sed +# Usage: json_get "$JSON_STRING" "field_name" +json_get() { + local json="$1" + local field="$2" + + if command -v jq &> /dev/null; then + echo "$json" | jq -r ".$field // empty" 2>/dev/null + else + echo "$json" | grep -oE "\"$field\":[[:space:]]*\"[^\"]*\"" | sed "s/\"$field\":[[:space:]]*\"\(.*\)\"/\1/" | head -1 + fi +} + +# Get session ID from JSON, defaulting to "default" +# Usage: get_session_id "$JSON_STRING" +get_session_id() { + local json="$1" + local session_id + + session_id=$(json_get "$json" "session_id") + echo "${session_id:-default}" +} + +# Check if notification should be shown based on duration +# Returns 0 if should notify, 1 if should skip +# Usage: should_notify "$SESSION_ID" "$MIN_DURATION" +should_notify() { + local session_id="$1" + local min_duration="$2" + local timestamp_file="$DATA_DIR/timestamp-${session_id}.txt" + + if [ ! -f "$timestamp_file" ]; then + return 1 + fi + + local start_time + start_time=$(cat "$timestamp_file" 2>/dev/null) + local current_time + current_time=$(date +%s) + local elapsed=$((current_time - start_time)) + + if [ "$elapsed" -lt "$min_duration" ]; then + return 1 + fi + + return 0 +} + +# Get saved prompt text for session +# Usage: get_prompt_text "$SESSION_ID" +get_prompt_text() { + local session_id="$1" + local prompt_file="$DATA_DIR/prompt-${session_id}.txt" + + if [ -f "$prompt_file" ]; then + cat "$prompt_file" 2>/dev/null + fi +} diff --git a/plugins/notifier/hooks-handlers/config.sh b/plugins/notifier/hooks-handlers/config.sh new file mode 100755 index 0000000000..c2c8954817 --- /dev/null +++ b/plugins/notifier/hooks-handlers/config.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# claude-code-notifier configuration +# Edit these values to customize notification behavior +# shellcheck disable=SC2034 # Variables are used by sourcing scripts + +# Language setting: "en" (English) or "ko" (한국어) +NOTIFIER_LANG="en" + +# Minimum task duration (seconds) to trigger notification +# Tasks shorter than this will not show notifications +MIN_DURATION_SECONDS=20 + +# Number of characters to show from the original prompt +PROMPT_PREVIEW_LENGTH=45 + +# ============================================================================= +# Messages (auto-set based on NOTIFIER_LANG, or customize below) +# ============================================================================= +if [ "$NOTIFIER_LANG" = "ko" ]; then + MSG_COMPLETED="작업 완료!" + MSG_PERMISSION="권한 필요!" + MSG_IDLE="입력 대기 중..." +else + MSG_COMPLETED="Task completed!" + MSG_PERMISSION="Permission required!" + MSG_IDLE="Waiting for input..." +fi diff --git a/plugins/notifier/hooks-handlers/notifiers/linux.sh b/plugins/notifier/hooks-handlers/notifiers/linux.sh new file mode 100755 index 0000000000..be801ddc7d --- /dev/null +++ b/plugins/notifier/hooks-handlers/notifiers/linux.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# linux.sh - Linux notification using notify-send +# Called from notify.sh + +set -e + +# Check if notify-send is available +if ! command -v notify-send &> /dev/null; then + exit 0 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../common.sh" + +# Read config from environment or use defaults +MIN_DURATION=${MIN_DURATION_SECONDS:-20} +NOTIFY_MSG=${NOTIFY_MESSAGE:-"Task completed!"} +NOTIF_TYPE=${NOTIFICATION_TYPE:-""} + +# Read stdin (JSON from Claude Code) +INPUT=$(cat) + +SESSION_ID=$(get_session_id "$INPUT") + +# For Notification hooks (permission_prompt, idle_prompt), show immediately +if [ -n "$NOTIF_TYPE" ] && [ "$NOTIF_TYPE" != "null" ]; then + notify-send "Claude Code" "$NOTIFY_MSG" + exit 0 +fi + +# Stop hook: check duration threshold +if ! should_notify "$SESSION_ID" "$MIN_DURATION"; then + exit 0 +fi + +# Get prompt preview and show notification +PROMPT_TEXT=$(get_prompt_text "$SESSION_ID") + +if [ -n "$PROMPT_TEXT" ]; then + notify-send "Claude Code" "$NOTIFY_MSG\n$PROMPT_TEXT" +else + notify-send "Claude Code" "$NOTIFY_MSG" +fi + +exit 0 diff --git a/plugins/notifier/hooks-handlers/notifiers/macos.sh b/plugins/notifier/hooks-handlers/notifiers/macos.sh new file mode 100755 index 0000000000..3493c4e39c --- /dev/null +++ b/plugins/notifier/hooks-handlers/notifiers/macos.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# macos.sh - macOS notification using AppleScript +# Called from notify.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../common.sh" + +# Read config from environment or use defaults +MIN_DURATION=${MIN_DURATION_SECONDS:-20} +NOTIFY_MSG=${NOTIFY_MESSAGE:-"Task completed!"} +NOTIF_TYPE=${NOTIFICATION_TYPE:-""} + +# Read stdin (JSON from Claude Code) +INPUT=$(cat) + +SESSION_ID=$(get_session_id "$INPUT") + +# For Notification hooks (permission_prompt, idle_prompt), show immediately +if [ -n "$NOTIF_TYPE" ] && [ "$NOTIF_TYPE" != "null" ]; then + osascript -e "display notification \"$NOTIFY_MSG\" with title \"Claude Code\"" + exit 0 +fi + +# Stop hook: check duration threshold +if ! should_notify "$SESSION_ID" "$MIN_DURATION"; then + exit 0 +fi + +# Get prompt preview and show notification +PROMPT_TEXT=$(get_prompt_text "$SESSION_ID") + +if [ -n "$PROMPT_TEXT" ]; then + osascript -e "display notification \"$PROMPT_TEXT\" with title \"Claude Code\" subtitle \"$NOTIFY_MSG\"" +else + osascript -e "display notification \"$NOTIFY_MSG\" with title \"Claude Code\"" +fi + +exit 0 diff --git a/plugins/notifier/hooks-handlers/notifiers/windows.ps1 b/plugins/notifier/hooks-handlers/notifiers/windows.ps1 new file mode 100644 index 0000000000..119183a407 --- /dev/null +++ b/plugins/notifier/hooks-handlers/notifiers/windows.ps1 @@ -0,0 +1,115 @@ +# windows.ps1 - Windows Toast Notification for Claude Code +# Called from notify.sh via Windows Native (Git Bash/MSYS2) or WSL + +# Read configuration from environment variables +$MIN_DURATION = [int]$env:MIN_DURATION_SECONDS +if ($MIN_DURATION -eq 0) { $MIN_DURATION = 20 } + +$NOTIFY_MSG = $env:NOTIFY_MESSAGE +if (-not $NOTIFY_MSG) { $NOTIFY_MSG = "Task completed!" } + +$NOTIF_TYPE = $env:NOTIFICATION_TYPE + +$DATA_DIR = $env:NOTIFIER_DATA_DIR_WIN +if (-not $DATA_DIR) { + Write-Host "Error: NOTIFIER_DATA_DIR_WIN not set" + exit 0 +} + +# UTF-8 encoding setup +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# Load Toast Notification assemblies +[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null +[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null + +# Read session_id from stdin (piped JSON) +$sessionId = "default" +try { + $inputData = [Console]::In.ReadToEnd() + if ($inputData) { + $jsonData = $inputData | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($jsonData.session_id) { + $sessionId = $jsonData.session_id + } + } +} catch { + # Use default session ID on error +} + +$escapedPrompt = "" + +# For Notification hooks (permission_prompt, idle_prompt), skip duration check +if (-not $NOTIF_TYPE -or $NOTIF_TYPE -eq "null") { + # Stop hook: check elapsed time + $timestampFile = Join-Path $DATA_DIR "timestamp-$sessionId.txt" + if (-not (Test-Path $timestampFile)) { + exit 0 + } + + try { + $startTime = [int](Get-Content $timestampFile -Raw -ErrorAction SilentlyContinue) + $currentTime = [int](Get-Date -UFormat %s) + $elapsed = $currentTime - $startTime + + # Skip notification if task was too short + if ($elapsed -lt $MIN_DURATION) { + exit 0 + } + } catch { + # Continue with notification on error + } + + # Read prompt preview + $promptFile = Join-Path $DATA_DIR "prompt-$sessionId.txt" + + if (Test-Path $promptFile) { + try { + $savedPrompt = Get-Content $promptFile -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ($savedPrompt -and $savedPrompt.Trim()) { + # Escape XML special characters + $escapedPrompt = $savedPrompt.Trim() + $escapedPrompt = $escapedPrompt -replace '&', '&' + $escapedPrompt = $escapedPrompt -replace '<', '<' + $escapedPrompt = $escapedPrompt -replace '>', '>' + $escapedPrompt = $escapedPrompt -replace '"', '"' + $escapedPrompt = $escapedPrompt -replace "'", ''' + } + } catch { + # Empty prompt on error + } + } +} + +# Build Toast XML +$xml = [Windows.Data.Xml.Dom.XmlDocument]::new() + +if ($escapedPrompt) { + $toastXml = @" + + + + $NOTIFY_MSG + $escapedPrompt + + + +"@ +} else { + $toastXml = @" + + + + $NOTIFY_MSG + + + +"@ +} + +$xml.LoadXml($toastXml) +$toast = [Windows.UI.Notifications.ToastNotification]::new($xml) +[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Claude Code').Show($toast) + +exit 0 diff --git a/plugins/notifier/hooks-handlers/notify.sh b/plugins/notifier/hooks-handlers/notify.sh new file mode 100755 index 0000000000..962ffa4410 --- /dev/null +++ b/plugins/notifier/hooks-handlers/notify.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# notify.sh - OS Router for Claude Code notifications +# Hooks: Stop, Notification +# Detects OS and routes to appropriate notifier + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" +source "$SCRIPT_DIR/config.sh" + +# Read stdin into variable (for passing to child scripts) +INPUT=$(cat) + +# Detect notification type from JSON +NOTIFICATION_TYPE=$(json_get "$INPUT" "notification_type") + +# Determine message based on notification type +case "$NOTIFICATION_TYPE" in + "permission_prompt") + NOTIFY_MESSAGE="$MSG_PERMISSION" + ;; + "idle_prompt") + NOTIFY_MESSAGE="$MSG_IDLE" + ;; + *) + NOTIFY_MESSAGE="$MSG_COMPLETED" + ;; +esac + +# Export config as environment variables for child scripts +export MIN_DURATION_SECONDS +export NOTIFY_MESSAGE +export NOTIFICATION_TYPE + +# Detect OS and route to appropriate notifier +if [ "$OS" = "Windows_NT" ]; then + # Windows Native (Git Bash, MSYS2) + # Convert Unix path to Windows path + NOTIFIER_DATA_DIR_WIN=$(cygpath -w "$DATA_DIR" 2>/dev/null || echo "$DATA_DIR") + export NOTIFIER_DATA_DIR_WIN + PS1_PATH_WIN=$(cygpath -w "$SCRIPT_DIR/notifiers/windows.ps1" 2>/dev/null || echo "$SCRIPT_DIR/notifiers/windows.ps1") + echo "$INPUT" | powershell.exe -ExecutionPolicy Bypass -File "$PS1_PATH_WIN" +elif grep -qi microsoft /proc/version 2>/dev/null; then + # WSL (Windows Subsystem for Linux) + NOTIFIER_DATA_DIR_WIN=$(wslpath -w "$DATA_DIR") + export NOTIFIER_DATA_DIR_WIN + # WSLENV is required to pass environment variables from WSL to Windows processes + export WSLENV="MIN_DURATION_SECONDS:NOTIFY_MESSAGE:NOTIFICATION_TYPE:NOTIFIER_DATA_DIR_WIN" + PS1_PATH_WIN=$(wslpath -w "$SCRIPT_DIR/notifiers/windows.ps1") + echo "$INPUT" | powershell.exe -ExecutionPolicy Bypass -File "$PS1_PATH_WIN" +elif [ "$(uname)" = "Darwin" ]; then + # macOS + echo "$INPUT" | "$SCRIPT_DIR/notifiers/macos.sh" +else + # Linux (native) + echo "$INPUT" | "$SCRIPT_DIR/notifiers/linux.sh" +fi + +exit 0 diff --git a/plugins/notifier/hooks-handlers/save-prompt.sh b/plugins/notifier/hooks-handlers/save-prompt.sh new file mode 100755 index 0000000000..061720f904 --- /dev/null +++ b/plugins/notifier/hooks-handlers/save-prompt.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# save-prompt.sh - Saves user prompt and timestamp for notifications +# Hook: UserPromptSubmit + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" +source "$SCRIPT_DIR/config.sh" + +# Ensure data directory exists +mkdir -p "$DATA_DIR" + +# Read stdin (JSON from Claude Code) +INPUT=$(cat) + +SESSION_ID=$(get_session_id "$INPUT") +PROMPT=$(json_get "$INPUT" "prompt") + +# Exit silently if no prompt +if [ -z "$PROMPT" ]; then + exit 0 +fi + +# Format prompt preview +LENGTH=${#PROMPT} +if [ "$LENGTH" -le "$PROMPT_PREVIEW_LENGTH" ]; then + FORMATTED_PROMPT="$PROMPT" +else + FORMATTED_PROMPT="${PROMPT:0:$PROMPT_PREVIEW_LENGTH}..." +fi + +# Save prompt and timestamp +echo "$FORMATTED_PROMPT" > "$DATA_DIR/prompt-${SESSION_ID}.txt" +date +%s > "$DATA_DIR/timestamp-${SESSION_ID}.txt" + +exit 0 diff --git a/plugins/notifier/hooks/hooks.json b/plugins/notifier/hooks/hooks.json new file mode 100644 index 0000000000..097966c01b --- /dev/null +++ b/plugins/notifier/hooks/hooks.json @@ -0,0 +1,55 @@ +{ + "description": "Notification hooks for task completion, permission requests, and input waiting", + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/save-prompt.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/notify.sh" + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/cleanup-session.sh" + } + ] + } + ], + "Notification": [ + { + "matcher": "permission_prompt", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/notify.sh" + } + ] + }, + { + "matcher": "idle_prompt", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks-handlers/notify.sh" + } + ] + } + ] + } +}