diff --git a/README.md b/README.md index 7d2df4f..053b8f7 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,22 @@ # CCNotify -CCNotify provides desktop notifications for Claude Code, alerting you when Claude needs your input or completes tasks. +CCNotify provides notifications for Claude Code, alerting you when Claude needs your input or completes tasks. + ## Important Notes Starting from claude-code v1.0.95 (2025-08-31), any invalid settings in `~/.claude/settings.json` will disable hooks. See [Why not working](#why-not-working) for solutions. ## Features -- šŸ”” **Get notified** when Claude requires your input or completes a task. -- šŸ”— **Click to jump back** when notifications are clicked, automatically taking you to the corresponding project in VS Code. +- šŸ”” **Get notified** when Claude requires your input or completes a task +- šŸ”— **Click to jump back** when notifications are clicked, automatically taking you to the corresponding project in VS Code (macOS only) - ā±ļø **Task Duration**: Displays started time, and how long the task took to complete +- šŸ¤– **Telegram Support**: Cross-platform notifications via Telegram bot (works on any platform, including SSH environments) +- šŸ–„ļø **Dual Mode**: Use terminal notifications, Telegram, or both simultaneously -**Note**: Currently compatible with macOS only. +**Platform Support**: +- **macOS**: Terminal notifications + Telegram +- **All platforms**: Telegram notifications (Linux, Windows, SSH environments) ## Installation Guide @@ -33,14 +38,19 @@ chmod a+x ~/.claude/ccnotify/ccnotify.py ok ``` -### 2. Install terminal-notifier -ccnotify uses `terminal-notifier` for macOS notifications. Install it using Homebrew: +### 2. Choose Your Notification Method + +#### Option A: Terminal Notifications (macOS only) +Install `terminal-notifier` for native macOS notifications: ```bash brew install terminal-notifier ``` -For alternative installation methods and more information, visit: https://github.com/julienXX/terminal-notifier +For alternative installation methods, visit: https://github.com/julienXX/terminal-notifier + +#### Option B: Telegram Notifications (All platforms) +Set up a Telegram bot for cross-platform notifications. See [Telegram Bot Setup](#telegram-bot-setup) section below. ### 3. Configure Claude Hooks Add the following hooks to your Claude configuration to enable ccnotify: @@ -90,7 +100,65 @@ To verify the notification system works, start a new Claude Code session and run ``` after 1 second, echo 'hello' ``` -You should see a macOS notification appear. +You should see a notification appear (terminal notification on macOS, or Telegram message if configured). + +## Configuration + +CCNotify creates a `config.json` file in `~/.claude/ccnotify/` on first run. You can customize notification methods: + +```json +{ + "notifications": { + "terminal": { + "enabled": true + }, + "telegram": { + "enabled": false, + "bot_token": "your_bot_token_here", + "chat_id": "your_chat_id_here" + } + } +} +``` + +**Configuration Options:** +- Set `terminal.enabled: false` to disable macOS notifications +- Set `telegram.enabled: true` and add your bot credentials to enable Telegram +- You can enable both methods simultaneously + +## Telegram Bot Setup + +### Step 1: Create a Telegram Bot +1. Open Telegram and search for `@BotFather` +2. Send `/newbot` command +3. Choose a name for your bot (e.g., "My Claude Notifier") +4. Choose a username (must end with `bot`, e.g., "my_claude_notifier_bot") +5. Copy the bot token (looks like `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`) + +### Step 2: Get Your Chat ID +1. Send any message to your bot (e.g., `/start`) +2. Open this URL in your browser (replace `YOUR_BOT_TOKEN`): + ``` + https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates + ``` +3. Look for `"chat":{"id":123456789}` and copy the ID number + +### Step 3: Configure CCNotify +Edit `~/.claude/ccnotify/config.json`: +```json +{ + "notifications": { + "terminal": { + "enabled": true + }, + "telegram": { + "enabled": true, + "bot_token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz", + "chat_id": "123456789" + } + } +} +``` ## Why not working 1. Ensure hooks configuration is active. Here's an example where other configurations prevent hooks from working: @@ -126,6 +194,64 @@ ccnotify tracks Claude sessions and provides notifications at key moments: All activity is logged to `~/.claude/ccnotify/ccnotify.log` and session data is stored in `~/.claude/ccnotify/ccnotify.db` locally. No data is uploaded or shared externally. +## Telegram Sender Tool + +CCNotify includes a standalone `telegram_sender.py` script for sending messages directly to Telegram from command pipelines. This tool reuses the CCNotify Telegram configuration. + +### Features +- šŸ“¤ **Pipeline friendly**: Reads from stdin for easy integration +- šŸ”§ **Uses existing config**: Leverages CCNotify's Telegram bot setup +- šŸ“ **Optional titles**: Add custom titles to messages +- šŸš€ **Lightweight**: Simple script with no additional dependencies + +### Usage + +```bash +# Basic usage +echo "Task completed!" | ./telegram_sender.py + +# With custom title +echo "Training accuracy: 95.2%" | ./telegram_sender.py "ML Model Results" + +# In command pipelines +model_train | ./telegram_sender.py "Training Complete" + +# With Claude Code integration +your_command | claude "summarize this output" | ./telegram_sender.py "Command Summary" +``` + +### Creating a Convenient Alias + +For frequent use with Claude Code, create a `telecc` alias that combines Claude summarization with Telegram sending: + +```bash +# Add to your ~/.bashrc or ~/.zshrc +alias telecc='claude "Please provide a concise summary of this output, highlighting key results, errors, or important information:" | ./telegram_sender.py' + +# Usage examples: +model_training_script | telecc "Training Results" +long_running_process | telecc "Process Complete" +pytest --verbose | telecc "Test Results" +``` + +### Installation + +1. Ensure CCNotify is configured with Telegram enabled +2. Copy or link `telegram_sender.py` to your desired location: + ```bash + # Copy to local directory + cp telegram_sender.py ~/bin/ + + # Or create symlink in your PATH + ln -s $(pwd)/telegram_sender.py ~/bin/telegram_sender + ``` +3. Make sure the script is executable: `chmod +x telegram_sender.py` + +### Requirements + +- CCNotify must be installed and configured with Telegram enabled +- Valid `config.json` file at `~/.claude/ccnotify/config.json` +- Network access to Telegram Bot API ## Uninstall diff --git a/ccnotify.py b/ccnotify.py index dc7c5a8..cd1e84d 100755 --- a/ccnotify.py +++ b/ccnotify.py @@ -18,8 +18,10 @@ class ClaudePromptTracker: def __init__(self): """Initialize the prompt tracker with database setup""" script_dir = os.path.dirname(os.path.abspath(__file__)) + self.script_dir = script_dir self.db_path = os.path.join(script_dir, "ccnotify.db") self.setup_logging() + self.load_config() self.init_database() def setup_logging(self): @@ -48,6 +50,32 @@ def setup_logging(self): logger.setLevel(logging.INFO) logger.addHandler(handler) + def load_config(self): + """Load configuration from config.json""" + config_path = os.path.join(self.script_dir, "config.json") + default_config = { + "notifications": { + "terminal": { + "enabled": True + }, + "telegram": { + "enabled": False, + "bot_token": "", + "chat_id": "" + } + } + } + + if os.path.exists(config_path): + with open(config_path, 'r') as f: + self.config = json.load(f) + else: + self.config = default_config + with open(config_path, 'w') as f: + json.dump(default_config, f, indent=2) + + logging.info(f"Config loaded from {config_path}") + def init_database(self): """Create tables and triggers if they don't exist""" with sqlite3.connect(self.db_path) as conn: @@ -271,11 +299,21 @@ def calculate_duration(self, start_time, end_time): return "Unknown" def send_notification(self, title, subtitle, cwd=None): - """Send macOS notification using terminal-notifier""" + """Send notification via configured methods""" from datetime import datetime current_time = datetime.now().strftime("%B %d, %Y at %H:%M") + # Send terminal notification if enabled + if self.config["notifications"]["terminal"]["enabled"]: + self.send_terminal_notification(title, subtitle, current_time, cwd) + + # Send telegram notification if enabled + if self.config["notifications"]["telegram"]["enabled"]: + self.send_telegram_notification(title, subtitle, current_time, cwd) + + def send_terminal_notification(self, title, subtitle, current_time, cwd=None): + """Send macOS notification using terminal-notifier""" try: cmd = [ "terminal-notifier", @@ -291,11 +329,46 @@ def send_notification(self, title, subtitle, cwd=None): cmd.extend(["-execute", f'/usr/local/bin/code "{cwd}"']) subprocess.run(cmd, check=False, capture_output=True) - logging.info(f"Notification sent: {title} - {subtitle}") + logging.info(f"Terminal notification sent: {title} - {subtitle}") except FileNotFoundError: logging.warning("terminal-notifier not found, notification skipped") except Exception as e: - logging.error(f"Error sending notification: {e}") + logging.error(f"Error sending terminal notification: {e}") + + def send_telegram_notification(self, title, subtitle, current_time, cwd=None): + """Send notification via Telegram bot""" + try: + import urllib.request + import urllib.parse + + bot_token = self.config["notifications"]["telegram"]["bot_token"] + chat_id = self.config["notifications"]["telegram"]["chat_id"] + + if not bot_token or not chat_id: + logging.warning("Telegram bot_token and chat_id must be configured") + return + + # Format message + message = f"šŸ”” *{title}*\n{subtitle}\nšŸ“… {current_time}" + if cwd: + message += f"\nšŸ“ `{cwd}`" + + # Send via Telegram Bot API + url = f"https://api.telegram.org/bot{bot_token}/sendMessage" + data = urllib.parse.urlencode({ + 'chat_id': chat_id, + 'text': message, + 'parse_mode': 'Markdown' + }).encode('utf-8') + + req = urllib.request.Request(url, data=data, method='POST') + with urllib.request.urlopen(req) as response: + if response.status != 200: + raise Exception(f"Telegram API error: {response.status}") + + logging.info(f"Telegram notification sent: {title} - {subtitle}") + except Exception as e: + logging.error(f"Error sending Telegram notification: {e}") def validate_input_data(data, expected_event_name): diff --git a/telegram_sender.py b/telegram_sender.py new file mode 100755 index 0000000..69d5377 --- /dev/null +++ b/telegram_sender.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Simple Telegram Message Sender +Reuses CCNotify configuration for sending messages via Telegram Bot API. +Designed for use in command pipelines. + +Usage: + echo "message" | telegram_sender.py + echo "message" | telegram_sender.py "Custom Title" + command | telegram_sender.py "Command Output" +""" + +import os +import sys +import json +import urllib.request +import urllib.parse +from datetime import datetime + + +class TelegramSender: + def __init__(self): + """Initialize with CCNotify config""" + self.load_config() + + def load_config(self): + """Load configuration from CCNotify's config.json""" + # Use same config path as ccnotify + config_dir = os.path.expanduser("~/.claude/ccnotify") + config_path = os.path.join(config_dir, "config.json") + + if not os.path.exists(config_path): + raise FileNotFoundError( + f"CCNotify config not found at {config_path}. " + "Please run ccnotify.py first to create the configuration." + ) + + try: + with open(config_path, 'r') as f: + self.config = json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in config file: {e}") + + # Validate telegram configuration + telegram_config = self.config.get("notifications", {}).get("telegram", {}) + if not telegram_config.get("enabled", False): + raise ValueError("Telegram notifications are not enabled in CCNotify config") + + self.bot_token = telegram_config.get("bot_token", "") + self.chat_id = telegram_config.get("chat_id", "") + + if not self.bot_token or not self.chat_id: + raise ValueError("Telegram bot_token and chat_id must be configured in CCNotify config") + + def send_message(self, title, message): + """Send message via Telegram Bot API""" + # Format message - simpler than ccnotify notifications + if title: + formatted_message = f"*{title}*\n\n{message}" + else: + formatted_message = message + + # Send via Telegram Bot API + url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage" + data = urllib.parse.urlencode({ + 'chat_id': self.chat_id, + 'text': formatted_message, + 'parse_mode': 'Markdown' + }).encode('utf-8') + + try: + req = urllib.request.Request(url, data=data, method='POST') + with urllib.request.urlopen(req, timeout=10) as response: + if response.status != 200: + response_text = response.read().decode('utf-8') + raise Exception(f"Telegram API error {response.status}: {response_text}") + + return True + except urllib.error.URLError as e: + raise Exception(f"Network error: {e}") + except Exception as e: + raise Exception(f"Failed to send telegram message: {e}") + + +def main(): + """Main entry point""" + try: + # Get optional title from command line argument + title = sys.argv[1] if len(sys.argv) > 1 else None + + # Read message content from stdin + if sys.stdin.isatty(): + print("Error: No input provided. Use in a pipeline or redirect input.", file=sys.stderr) + print("Usage: echo 'message' | telegram_sender.py [title]", file=sys.stderr) + sys.exit(1) + + message = sys.stdin.read().strip() + if not message: + print("Error: Empty message received from stdin", file=sys.stderr) + sys.exit(1) + + # Initialize sender and send message + sender = TelegramSender() + sender.send_message(title, message) + + # Success output for pipeline debugging + if title: + print(f"Message sent to Telegram: {title}") + else: + print("Message sent to Telegram") + + except FileNotFoundError as e: + print(f"Config Error: {e}", file=sys.stderr) + sys.exit(1) + except ValueError as e: + print(f"Configuration Error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file