Telegram&Line support (#17)

* feat: Add LINE and Telegram messaging support

This major enhancement extends Claude Code Remote with multi-platform messaging support:

## 🚀 New Features

### LINE Messaging Support
- LINE Bot API integration with token-based commands
- Secure webhook handler with signature verification
- Session management with 24-hour expiration
- Support for both individual and group chats
- User/Group ID whitelist security

### Telegram Bot Support
- Telegram Bot API with interactive buttons
- Slash command support (/cmd TOKEN command)
- Callback query handling for better UX
- Group and private chat support
- Chat ID-based authorization

### Multi-Platform Architecture
- Unified notification system supporting all platforms
- Platform-agnostic session management
- Configurable channel enabling/disabling
- Parallel webhook server support

## 🛠️ Technical Implementation

### Core Components
- `src/channels/line/` - LINE messaging implementation
- `src/channels/telegram/` - Telegram bot implementation
- `src/utils/controller-injector.js` - Command injection utility
- Multi-platform webhook servers with Express.js

### Configuration & Documentation
- Updated `.env.example` with all platform options
- Comprehensive setup guides for each platform
- Testing guide with ngrok instructions
- Updated README with multi-platform support

### Developer Experience
- npm scripts for easy platform startup
- Unified webhook launcher (`start-all-webhooks.js`)
- Individual platform launchers
- Enhanced error handling and logging

## 🔧 Usage Examples

**Telegram:** `/cmd ABC12345 analyze this code`
**LINE:** `Token ABC12345 analyze this code`
**Email:** Reply to notification emails

## 📋 Backward Compatibility
- All existing email functionality preserved
- Configuration migration path provided
- No breaking changes to existing hooks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Complete Telegram Remote Control System with Direct Chat Mode

### 🚀 Major Features Added
- **Direct Chat Mode**: Users can chat with Claude naturally without /cmd tokens
- **Smart Monitoring**: Intelligent response detection with historical processing
- **One-Click Startup**: Complete system management via startup script
- **Auto-Notification**: Real-time Claude response detection and Telegram delivery

### 📱 Telegram Integration
- `telegram-direct-mode.js`: Direct conversation interface
- `telegram-polling.js`: Command polling with token validation
- Enhanced notification system with markdown formatting

### 🧠 Smart Monitoring System
- `smart-monitor.js`: Advanced response detection with history awareness
- `simple-monitor.js`: Lightweight monitoring alternative
- `auto-notification-daemon.js`: Background notification service

### 🛠️ System Management
- `start-telegram-claude.sh`: Complete service management script
- Environment validation and dependency checking
- Color-coded status reporting and log management
- Process lifecycle management (start/stop/restart/status)

### 📖 Documentation & Testing
- `TELEGRAM_CLAUDE_GUIDE.md`: Comprehensive user guide
- Complete test suite for all components
- Usage examples and troubleshooting guide

### 🔧 Core Improvements
- Enhanced `controller-injector.js` with proper Enter key handling
- Updated `tmux-monitor.js` with real-time output monitoring
- Improved error handling and logging throughout
- Session management with automatic cleanup

### 🎯 Key Capabilities
- **Seamless Communication**: Direct Telegram ⟷ Claude integration
- **Full Automation**: No manual intervention required
- **Robust Monitoring**: Never miss Claude responses
- **Easy Deployment**: Single script startup and management
- **Multi-Modal Support**: Ready for LINE integration expansion

### 📊 System Architecture
```
User → Telegram Bot → Direct Injection → Claude Session → Smart Monitor → Auto Notification → User
```

This completes the transformation from email-based to messaging-app-based remote Claude control,
providing a modern, efficient, and user-friendly interface for remote AI interaction.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: Enhance documentation and smart monitoring system

- Add comprehensive CLAUDE.md for Claude Code development guidance
- Update README.md with multi-platform focus and improved instructions
- Enhance smart-monitor.js with auto-approval for tool permissions
- Improve start-telegram-claude.sh with better tmux session management
- Add auto-approver.js for automated tool permission handling

Key improvements:
- Multi-platform documentation (Telegram, LINE, Email, Local)
- Enhanced troubleshooting and command reference sections
- Smart monitoring with historical response detection
- Automated tool permission approval workflow
- Better tmux integration and session management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: Add CLAUDE.md to .gitignore and remove from tracking

- Add CLAUDE.md to .gitignore under "Claude Code development files" section
- Remove CLAUDE.md from git tracking while preserving local file
- CLAUDE.md should remain as local development documentation only

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Complete Telegram integration with multi-channel notifications

-  Enhanced Telegram bot with smart buttons for personal/group chats
-  Multi-channel notification system (Desktop, Telegram, Email, LINE)
-  Smart sound alerts with customizable audio feedback
-  Real-time command injection with tmux session management
-  Intelligent session detection and conversation content extraction
-  Unified README documentation with complete setup guides
- 🧹 Clean up legacy files and consolidate documentation
- 📱 Add setup scripts for easy Telegram configuration
- 🔧 Enhance webhook system with improved error handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update processed messages

---------

Co-authored-by: laihenyi <henyi@henyi.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: laihenyi <laihenyi@users.noreply.github.com>
This commit is contained in:
JessyTsui 2025-08-02 04:21:26 +08:00 committed by GitHub
parent 61dd2e30c3
commit b14f95d821
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 4625 additions and 687 deletions

View File

@ -1,7 +1,14 @@
# Claude Code Remote Email Configuration Example # Claude Code Remote Configuration
# Copy this file to .env and configure with your actual values # Copy this file to .env and configure with your actual values
# ===== SMTP 发送邮件配置 ===== # ===== 選擇通知方式Email、LINE 或 Telegram =====
# 可以同時啟用多個通知方式
EMAIL_ENABLED=false
LINE_ENABLED=false
TELEGRAM_ENABLED=true
# ===== Email 配置 (如果使用 Email) =====
# SMTP 发送邮件配置
SMTP_HOST=smtp.gmail.com SMTP_HOST=smtp.gmail.com
SMTP_PORT=465 SMTP_PORT=465
SMTP_SECURE=true SMTP_SECURE=true
@ -12,20 +19,56 @@ SMTP_PASS=your-app-password
EMAIL_FROM=your-email@gmail.com EMAIL_FROM=your-email@gmail.com
EMAIL_FROM_NAME=Claude Code Remote 通知系统 EMAIL_FROM_NAME=Claude Code Remote 通知系统
# ===== IMAP 接收邮件配置 ===== # IMAP 接收邮件配置
IMAP_HOST=imap.gmail.com IMAP_HOST=imap.gmail.com
IMAP_PORT=993 IMAP_PORT=993
IMAP_SECURE=true IMAP_SECURE=true
IMAP_USER=your-email@gmail.com IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password IMAP_PASS=your-app-password
# ===== 邮件路由配置 ===== # 邮件路由配置
# 接收通知的邮箱地址 # 接收通知的邮箱地址
EMAIL_TO=your-email@gmail.com EMAIL_TO=your-email@gmail.com
# 允许发送命令的邮箱地址(安全白名单) # 允许发送命令的邮箱地址(安全白名单)
ALLOWED_SENDERS=your-email@gmail.com ALLOWED_SENDERS=your-email@gmail.com
# ===== LINE 配置 (如果使用 LINE) =====
# 從 LINE Developers Console 獲取: https://developers.line.biz/
LINE_CHANNEL_ACCESS_TOKEN=your-line-channel-access-token
LINE_CHANNEL_SECRET=your-line-channel-secret
# LINE 接收者配置(設定一個或兩個)
# LINE_USER_ID=your-line-user-id
# LINE_GROUP_ID=your-line-group-id
# LINE 白名單(逗號分隔的使用者/群組 ID
# 如果不設定,只有配置的 USER_ID/GROUP_ID 可以使用
# LINE_WHITELIST=U1234567890abcdef,C1234567890abcdef
# LINE webhook 埠號預設3000
# LINE_WEBHOOK_PORT=3000
# ===== Telegram 配置 (如果使用 Telegram) =====
# 從 @BotFather 獲取 Bot Token
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
# Telegram 接收者配置(設定一個或兩個)
# 個人聊天 ID
# TELEGRAM_CHAT_ID=123456789
# 群組 ID通常是負數
# TELEGRAM_GROUP_ID=-1001234567890
# Telegram 白名單(逗號分隔的 Chat ID
# 如果不設定,只有配置的 CHAT_ID/GROUP_ID 可以使用
# TELEGRAM_WHITELIST=123456789,-1001234567890
# Telegram webhook 埠號預設3001
# TELEGRAM_WEBHOOK_PORT=3001
# Telegram webhook URL您的公開 HTTPS URL
# TELEGRAM_WEBHOOK_URL=https://your-domain.com
# ===== 系统配置 ===== # ===== 系统配置 =====
# 会话映射文件路径 # 会话映射文件路径
SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json

1
.gitignore vendored
View File

@ -48,7 +48,6 @@ build/
# Temporary files # Temporary files
tmp/ tmp/
temp/ temp/
# Data files (session-specific, should not be committed) # Data files (session-specific, should not be committed)
src/data/ src/data/
!src/data/.gitkeep !src/data/.gitkeep

388
README.md
View File

@ -1,6 +1,12 @@
# Claude Code Remote # Claude Code Remote
Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks locally, receive notifications when Claude completes them, and send new commands by simply replying to emails. Control [Claude Code](https://claude.ai/code) remotely via multiple messaging platforms. Start tasks locally, receive notifications when Claude completes them, and send new commands by simply replying to messages.
**Supported Platforms:**
- 📧 **Email** - Traditional SMTP/IMAP integration with execution trace
- 📱 **Telegram** - Interactive bot with smart buttons ✅ **NEW**
- 💬 **LINE** - Rich messaging with token-based commands
- 🖥️ **Desktop** - Sound alerts and system notifications
<div align="center"> <div align="center">
@ -18,18 +24,34 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
## ✨ Features ## ✨ Features
- **📧 Email Notifications**: Get notified when Claude completes tasks ![](./assets/email_demo.png) - **📧 Multiple Messaging Platforms**:
- **🔄 Email Control**: Reply to emails to send new commands to Claude - Email notifications with full execution trace and reply-to-send commands
- **📱 Remote Access**: Control Claude from anywhere with just email - Telegram Bot with interactive buttons and slash commands ✅ **NEW**
- **🔒 Secure**: Whitelist-based sender verification - LINE messaging with token-based commands
- Desktop notifications with sound alerts
- **🔄 Two-way Control**: Reply to messages or emails to send new commands
- **📱 Remote Access**: Control Claude from anywhere
- **🔒 Secure**: ID-based whitelist verification for all platforms
- **👥 Group Support**: Use in LINE groups or Telegram groups for team collaboration
- **🤖 Smart Commands**: Intuitive command formats for each platform
- **📋 Multi-line Support**: Send complex commands with formatting - **📋 Multi-line Support**: Send complex commands with formatting
- **⚡ Smart Monitoring**: Intelligent detection of Claude responses with historical tracking
- **🔄 tmux Integration**: Seamless command injection into active tmux sessions
- **📊 Execution Trace**: Full terminal output capture in email notifications
## 📅 Changelog ## 📅 Changelog
### August 2025 ### August 2025
- **2025-08-02**: Add full execution trace to email notifications ([#14](https://github.com/JessyTsui/Claude-Code-Remote/pull/14))
- **2025-08-01**: Enhanced Multi-Channel Notification System (by @laihenyi @JessyTsui)
- ✅ **Telegram Integration Completed** - Interactive buttons, real-time commands, smart personal/group chat handling
- ✅ **Multi-Channel Notifications** - Simultaneous delivery to Desktop, Telegram, Email, LINE
- ✅ **Smart Sound Alerts** - Always-on audio feedback with customizable sounds
- ✅ **Intelligent Session Management** - Auto-detection, real conversation content, 24-hour tokens
- **2025-08-01**: Fix #9 #12: Add configuration to disable subagent notifications ([#10](https://github.com/JessyTsui/Claude-Code-Remote/pull/10))
- **2025-08-01**: Implement terminal-style UI for email notifications ([#8](https://github.com/JessyTsui/Claude-Code-Remote/pull/8) by [@vaclisinc](https://github.com/vaclisinc)) - **2025-08-01**: Implement terminal-style UI for email notifications ([#8](https://github.com/JessyTsui/Claude-Code-Remote/pull/8) by [@vaclisinc](https://github.com/vaclisinc))
- **2025-08-01**: Fix working directory issue - enable claude-remote to run from any directory ([#7](https://github.com/JessyTsui/Claude-Code-Remote/pull/7) by [@vaclisinc](https://github.com/vaclisinc)) - **2025-08-01**: Fix working directory issue - enable claude-remote to run from any directory ([#7](https://github.com/JessyTsui/Claude-Code-Remote/pull/7) by [@vaclisinc](https://github.com/vaclisinc))
### July 2025 ### July 2025
- **2025-07-31**: Fix self-reply loop issue when using same email for send/receive ([#4](https://github.com/JessyTsui/Claude-Code-Remote/pull/4) by [@vaclisinc](https://github.com/vaclisinc)) - **2025-07-31**: Fix self-reply loop issue when using same email for send/receive ([#4](https://github.com/JessyTsui/Claude-Code-Remote/pull/4) by [@vaclisinc](https://github.com/vaclisinc))
- **2025-07-28**: Remove hardcoded values and implement environment-based configuration ([#2](https://github.com/JessyTsui/Claude-Code-Remote/pull/2) by [@kevinsslin](https://github.com/kevinsslin)) - **2025-07-28**: Remove hardcoded values and implement environment-based configuration ([#2](https://github.com/JessyTsui/Claude-Code-Remote/pull/2) by [@kevinsslin](https://github.com/kevinsslin))
@ -37,28 +59,33 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
## 📋 TODO List ## 📋 TODO List
### Notification Channels ### Notification Channels
- [ ] **Discord & Telegram**: Bot integration for messaging platforms - ~~**📱 Telegram Integration**~~**COMPLETED** - Bot integration with interactive buttons and real-time commands
- [ ] **Slack Workflow**: Native Slack app with slash commands - **💬 Discord Integration** - Bot integration for messaging platforms
- **⚡ Slack Workflow** - Native Slack app with slash commands
### Developer Tools ### Developer Tools
- [ ] **AI Tools**: Support for Gemini CLI, Cursor, and other AI tools - **🤖 AI Tools Support** - Integration with Gemini CLI, Cursor, and other AI development tools
- [ ] **Git Automation**: Auto-commit, PR creation, branch management - **🔀 Git Automation** - Auto-commit functionality, PR creation, branch management
### Usage Analytics ### Usage Analytics
- [ ] **Cost Tracking**: Token usage and estimated costs - **💰 Cost Tracking** - Token usage monitoring and estimated costs
- [ ] **Performance Metrics**: Execution time and resource usage - **⚡ Performance Metrics** - Execution time tracking and resource usage analysis
- [ ] **Scheduled Reports**: Daily/weekly usage summaries via email - **📧 Scheduled Reports** - Daily/weekly usage summaries delivered via email
### Native Apps ### Native Apps
- [ ] **Mobile Apps**: iOS and Android applications - **📱 Mobile Apps** - iOS and Android applications for remote Claude control
- [ ] **Desktop Apps**: macOS and Windows native clients - **🖥️ Desktop Apps** - macOS and Windows native clients with system integration
## 🚀 Quick Start
## 🚀 Setup Guide ### 1. Prerequisites
Follow these steps to get Claude Code Remote running: **System Requirements:**
- Node.js >= 14.0.0
- **tmux** (required for command injection)
- Active tmux session with Claude Code running
### Step 1: Clone and Install Dependencies ### 2. Install
```bash ```bash
git clone https://github.com/JessyTsui/Claude-Code-Remote.git git clone https://github.com/JessyTsui/Claude-Code-Remote.git
@ -66,62 +93,78 @@ cd Claude-Code-Remote
npm install npm install
``` ```
### Step 2: Configure Email Settings ### 3. Choose Your Platform
#### Option A: Configure Email (Recommended for Beginners)
```bash ```bash
# Copy the example configuration # Copy example config
cp .env.example .env cp .env.example .env
# Open .env in your editor # Edit with your email credentials
nano .env # or use vim, code, etc. nano .env
``` ```
Edit the `.env` file with your email credentials: **Required email settings:**
```env ```env
# Email account for sending notifications EMAIL_ENABLED=true
SMTP_USER=your-email@gmail.com SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password # Gmail: use App Password, not regular password SMTP_PASS=your-app-password
# Email account for receiving replies (can be same as SMTP)
IMAP_USER=your-email@gmail.com IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password IMAP_PASS=your-app-password
# Where to send notifications
EMAIL_TO=your-notification-email@gmail.com EMAIL_TO=your-notification-email@gmail.com
# Who can send commands (security whitelist)
ALLOWED_SENDERS=your-notification-email@gmail.com ALLOWED_SENDERS=your-notification-email@gmail.com
SESSION_MAP_PATH=/your/path/to/Claude-Code-Remote/src/data/session-map.json
# Path to session data (use absolute path)
SESSION_MAP_PATH=/your/absolute/path/to/Claude-Code-Remote/src/data/session-map.json
``` ```
📌 **Gmail users**: Create an [App Password](https://myaccount.google.com/security) instead of using your regular password. 📌 **Gmail users**: Use [App Passwords](https://myaccount.google.com/security), not your regular password.
> Note: You may need to enable two-step verification in your google account first before create app password.
### Step 3: Set Up Claude Code Hooks #### Option B: Configure Telegram ✅ **NEW**
Open Claude's settings file:
**Quick Setup:**
```bash ```bash
# Create the directory if it doesn't exist chmod +x setup-telegram.sh
mkdir -p ~/.claude ./setup-telegram.sh
# Edit settings.json
nano ~/.claude/settings.json
``` ```
Add this configuration (replace `/your/absolute/path/` with your actual path): **Manual Setup:**
1. Create bot via [@BotFather](https://t.me/BotFather)
2. Get your Chat ID from bot API
3. Configure webhook URL (use ngrok for local testing)
```json **Required Telegram settings:**
```env
TELEGRAM_ENABLED=true
TELEGRAM_BOT_TOKEN=your-bot-token-here
TELEGRAM_CHAT_ID=your-chat-id-here
TELEGRAM_WEBHOOK_URL=https://your-ngrok-url.app
SESSION_MAP_PATH=/your/path/to/Claude-Code-Remote/src/data/session-map.json
```
#### Option C: Configure LINE
**Required LINE settings:**
```env
LINE_ENABLED=true
LINE_CHANNEL_ACCESS_TOKEN=your-token
LINE_CHANNEL_SECRET=your-secret
LINE_USER_ID=your-user-id
```
### 4. Configure Claude Code Hooks
Create hooks configuration file:
**Method 1: Global Configuration (Recommended)**
```bash
# Add to ~/.claude/settings.json
{ {
"hooks": { "hooks": {
"Stop": [{ "Stop": [{
"matcher": "*", "matcher": "*",
"hooks": [{ "hooks": [{
"type": "command", "type": "command",
"command": "node /your/absolute/path/to/Claude-Code-Remote/claude-remote.js notify --type completed", "command": "node /your/path/to/Claude-Code-Remote/claude-hook-notify.js completed",
"timeout": 5 "timeout": 5
}] }]
}], }],
@ -129,7 +172,7 @@ Add this configuration (replace `/your/absolute/path/` with your actual path):
"matcher": "*", "matcher": "*",
"hooks": [{ "hooks": [{
"type": "command", "type": "command",
"command": "node /your/absolute/path/to/Claude-Code-Remote/claude-remote.js notify --type waiting", "command": "node /your/path/to/Claude-Code-Remote/claude-hook-notify.js waiting",
"timeout": 5 "timeout": 5
}] }]
}] }]
@ -137,111 +180,93 @@ Add this configuration (replace `/your/absolute/path/` with your actual path):
} }
``` ```
**Method 2: Project-Specific Configuration**
```bash
# Set environment variable
export CLAUDE_HOOKS_CONFIG=/your/path/to/Claude-Code-Remote/claude-hooks.json
```
> **Note**: Subagent notifications are disabled by default. To enable them, set `enableSubagentNotifications: true` in your config. See [Subagent Notifications Guide](./docs/SUBAGENT_NOTIFICATIONS.md) for details. > **Note**: Subagent notifications are disabled by default. To enable them, set `enableSubagentNotifications: true` in your config. See [Subagent Notifications Guide](./docs/SUBAGENT_NOTIFICATIONS.md) for details.
### Step 4: Test Your Setup ### 5. Start Services
#### For All Platforms (Recommended)
```bash ```bash
# Test email configuration # Automatically starts all enabled platforms
node claude-remote.js test npm run webhooks
# or
node start-all-webhooks.js
``` ```
You should receive a test email. If not, check your email settings. #### For Individual Platforms
### Step 5: Start Claude Code Remote **For Email:**
**Terminal 1 - Start email monitoring:**
```bash ```bash
npm run relay:pty npm run daemon:start
# or
node claude-remote.js daemon start
``` ```
Keep this running. You should see: **For Telegram:**
```
🚀 Claude Code Remote is running!
📧 Monitoring emails...
```
**Terminal 2 - Start Claude in tmux:**
```bash ```bash
# Create a new tmux session npm run telegram
tmux new-session -s my-project # or
node start-telegram-webhook.js
# Inside tmux, start Claude
claude
``` ```
### Step 6: You're Ready! **For LINE:**
```bash
1. Use Claude normally in the tmux session npm run line
2. When Claude completes a task, you'll receive an email # or
3. Reply to the email with new commands node start-line-webhook.js
4. Your commands will execute automatically in Claude
### Verify Everything Works
In Claude, type:
```
What is 2+2?
``` ```
Wait for Claude to respond, then check your email. You should receive a notification! ### 6. Test Your Setup
## 📖 How to Use **Quick Test:**
```bash
### Email Notifications # Test all notification channels
When Claude completes a task, you'll receive an email notification: node claude-hook-notify.js completed
# Should receive notifications via all enabled platforms
```
Subject: Claude Code Remote Task Complete [#ABC123]
Claude completed: "analyze the code structure"
[Claude's full response here...]
Reply to this email to send new commands.
``` ```
### Sending Commands via Email Reply **Full Test:**
1. Start Claude in tmux session with hooks enabled
2. Run any command in Claude
3. Check for notifications (email/Telegram/LINE)
4. Reply with new command to test two-way control
1. **Direct Reply**: Simply reply to the notification email ## 🎮 How It Works
2. **Write Command**: Type your command in the email body:
```
Please refactor the main function and add error handling
```
3. **Send**: Your command will automatically execute in Claude!
### Advanced Email Features 1. **Use Claude normally** in tmux session
2. **Get notifications** when Claude completes tasks via:
- 🔊 **Sound alert** (Desktop)
- 📧 **Email notification with execution trace** (if enabled)
- 📱 **Telegram message with buttons** (if enabled)
- 💬 **LINE message** (if enabled)
3. **Reply with commands** using any platform
4. **Commands execute automatically** in Claude
**Multi-line Commands** ### Platform Command Formats
**Email:**
``` ```
First analyze the current code structure. Simply reply to notification email with your command
Then create a comprehensive test suite. No special formatting required
Finally, update the documentation.
``` ```
**Complex Instructions** **Telegram:** ✅ **NEW**
``` ```
Refactor the authentication module with these requirements: Click smart button to get format:
- Use JWT tokens instead of sessions 📝 Personal Chat: /cmd TOKEN123 your command here
- Add rate limiting 👥 Group Chat: @bot_name /cmd TOKEN123 your command here
- Implement refresh token logic
- Update all related tests
``` ```
### Email Reply Workflow **LINE:**
```
1. **Receive Notification** → You get an email when Claude completes a task Reply to notification with: Your command here
2. **Reply with Command** → Send your next instruction via email reply (Token automatically extracted from conversation context)
3. **Automatic Execution** → The system extracts your command and injects it into Claude ```
4. **Get Results** → Receive another email when the new task completes
### Supported Email Clients
Works with any email client that supports standard reply functionality:
- ✅ Gmail (Web/Mobile)
- ✅ Apple Mail
- ✅ Outlook
- ✅ Any SMTP-compatible email client
### Advanced Configuration ### Advanced Configuration
@ -284,51 +309,110 @@ Works with any email client that supports standard reply functionality:
This is useful if you find the execution trace too verbose or if your email client has issues with scrollable content. This is useful if you find the execution trace too verbose or if your email client has issues with scrollable content.
## 💡 Common Use Cases ## 💡 Use Cases
- **Remote Development**: Start coding at the office, continue from home via email - **Remote Code Reviews**: Start reviews at office, continue from home via any platform
- **Long Tasks**: Let Claude work while you're in meetings, check results via email - **Long-running Tasks**: Monitor progress and guide next steps remotely
- **Team Collaboration**: Share Claude sessions by forwarding notification emails - **Multi-location Development**: Control Claude from anywhere without VPN
- **Team Collaboration**: Share Telegram groups for team notifications
- **Mobile Development**: Send commands from phone via Telegram
## 🔧 Useful Commands ## 🔧 Commands
### Testing & Diagnostics
```bash ```bash
# Test email setup # Test all notification channels
node claude-remote.js test node claude-hook-notify.js completed
# Check system status # Test specific platforms
node test-telegram-notification.js
node test-real-notification.js
node test-injection.js
# System diagnostics
node claude-remote.js diagnose
node claude-remote.js status node claude-remote.js status
node claude-remote.js test
```
# View tmux sessions ### Service Management
tmux list-sessions ```bash
tmux attach -t my-project # Start all enabled platforms
npm run webhooks
# Stop email monitoring # Individual services
# Press Ctrl+C in the terminal running npm run relay:pty npm run telegram # Telegram webhook
npm run line # LINE webhook
npm run daemon:start # Email daemon
# Stop services
npm run daemon:stop # Stop email daemon
``` ```
## 🔍 Troubleshooting ## 🔍 Troubleshooting
### Common Issues
**Not receiving notifications from Claude?**
1. Check hooks configuration in tmux session:
```bash
echo $CLAUDE_HOOKS_CONFIG
```
2. Verify Claude is running with hooks enabled
3. Test notification manually:
```bash
node claude-hook-notify.js completed
```
**Telegram bot not responding?** ✅ **NEW**
```bash
# Test bot connectivity
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": $TELEGRAM_CHAT_ID, \"text\": \"Test\"}"
# Check webhook status
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo"
```
**Commands not executing in Claude?**
```bash
# Check tmux session exists
tmux list-sessions
# Verify injection mode
grep INJECTION_MODE .env # Should be 'tmux'
# Test injection
node test-injection.js
```
**Not receiving emails?** **Not receiving emails?**
- Run `node claude-remote.js test` to test email setup - Run `node claude-remote.js test` to test email setup
- Check spam folder - Check spam folder
- Verify SMTP settings in `.env` - Verify SMTP settings in `.env`
- For Gmail: ensure you're using App Password - For Gmail: ensure you're using App Password
**Commands not executing?** ### Debug Mode
- Ensure tmux session is running: `tmux list-sessions` ```bash
- Check sender email matches `ALLOWED_SENDERS` in `.env` # Enable detailed logging
- Verify Claude is running inside tmux LOG_LEVEL=debug npm run webhooks
DEBUG=true node claude-hook-notify.js completed
**Need help?** ```
- Check [Issues](https://github.com/JessyTsui/Claude-Code-Remote/issues)
- Follow [@Jiaxi_Cui](https://x.com/Jiaxi_Cui) for updates
## 🛡️ Security ## 🛡️ Security
- ✅ **Sender Whitelist**: Only authorized emails can send commands ### Multi-Platform Authentication
- ✅ **Session Isolation**: Each token controls only its specific session - ✅ **Email**: Sender whitelist via `ALLOWED_SENDERS` environment variable
- ✅ **Auto Expiration**: Sessions timeout automatically - ✅ **Telegram**: Bot token and chat ID verification
- ✅ **LINE**: Channel secret and access token validation
- ✅ **Session Tokens**: 8-character alphanumeric tokens for command verification
### Session Security
- ✅ **Session Isolation**: Each token controls only its specific tmux session
- ✅ **Auto Expiration**: Sessions timeout automatically after 24 hours
- ✅ **Token-based Commands**: All platforms require valid session tokens
- ✅ **Minimal Data Storage**: Session files contain only necessary information
## 🤝 Contributing ## 🤝 Contributing
@ -352,4 +436,4 @@ MIT License - Feel free to use and modify!
**Star this repo** if it helps you code more efficiently! **Star this repo** if it helps you code more efficiently!
> 💡 **Tip**: Share your remote coding setup on Twitter and tag [@Jiaxi_Cui](https://x.com/Jiaxi_Cui) - we love seeing how developers use Claude Code Remote! > 💡 **Tip**: Enable multiple notification channels for redundancy - never miss a Claude completion again!

BIN
assets/telegram_demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

View File

@ -1,288 +0,0 @@
#!/usr/bin/env node
/**
* Claude-Code-Remote Unattended Remote Control Setup Assistant
*/
const { exec, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
class RemoteControlSetup {
constructor(sessionName = null) {
this.sessionName = sessionName || 'claude-code-remote';
this.claudeCodeRemoteHome = this.findClaudeCodeRemoteHome();
}
findClaudeCodeRemoteHome() {
// If CLAUDE_CODE_REMOTE_HOME environment variable is set, use it
if (process.env.CLAUDE_CODE_REMOTE_HOME) {
return process.env.CLAUDE_CODE_REMOTE_HOME;
}
// If running from the Claude-Code-Remote directory, use current directory
if (fs.existsSync(path.join(__dirname, 'package.json'))) {
const packagePath = path.join(__dirname, 'package.json');
try {
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
if (packageJson.name && packageJson.name.toLowerCase().includes('claude-code-remote')) {
return __dirname;
}
} catch (e) {
// Continue searching
}
}
// Search for Claude-Code-Remote in common locations
const commonPaths = [
path.join(process.env.HOME, 'dev', 'Claude-Code-Remote'),
path.join(process.env.HOME, 'Projects', 'Claude-Code-Remote'),
path.join(process.env.HOME, 'claude-code-remote'),
__dirname // fallback to current script directory
];
for (const searchPath of commonPaths) {
if (fs.existsSync(searchPath) && fs.existsSync(path.join(searchPath, 'package.json'))) {
try {
const packageJson = JSON.parse(fs.readFileSync(path.join(searchPath, 'package.json'), 'utf8'));
if (packageJson.name && packageJson.name.toLowerCase().includes('claude-code-remote')) {
return searchPath;
}
} catch (e) {
// Continue searching
}
}
}
// If not found, use current directory as fallback
return __dirname;
}
async setup() {
console.log('🚀 Claude-Code-Remote Unattended Remote Control Setup\n');
console.log('🎯 Goal: Remote access via mobile phone → Home computer Claude Code automatically executes commands\n');
try {
// 1. Check tmux
await this.checkAndInstallTmux();
// 2. Check Claude CLI
await this.checkClaudeCLI();
// 3. Setup Claude tmux session
await this.setupClaudeSession();
// 4. Session creation complete
console.log('\n4⃣ Session creation complete');
// 5. Provide usage guide
this.showUsageGuide();
} catch (error) {
console.error('❌ Error occurred during setup:', error.message);
}
}
async checkAndInstallTmux() {
console.log('1⃣ Checking tmux installation status...');
return new Promise((resolve) => {
exec('which tmux', (error, stdout) => {
if (error) {
console.log('❌ tmux not installed');
console.log('📦 Installing tmux...');
exec('brew install tmux', (installError, installStdout, installStderr) => {
if (installError) {
console.log('❌ tmux installation failed, please install manually:');
console.log(' brew install tmux');
console.log(' or download from https://github.com/tmux/tmux');
} else {
console.log('✅ tmux installation successful');
}
resolve();
});
} else {
console.log(`✅ tmux already installed: ${stdout.trim()}`);
resolve();
}
});
});
}
async checkClaudeCLI() {
console.log('\n2⃣ Checking Claude CLI status...');
return new Promise((resolve) => {
exec('which claude', (error, stdout) => {
if (error) {
console.log('❌ Claude CLI not found');
console.log('📦 Please install Claude CLI:');
console.log(' npm install -g @anthropic-ai/claude-code');
} else {
console.log(`✅ Claude CLI installed: ${stdout.trim()}`);
// Check version
exec('claude --version', (versionError, versionStdout) => {
if (!versionError) {
console.log(`📋 Version: ${versionStdout.trim()}`);
}
});
}
resolve();
});
});
}
async setupClaudeSession() {
console.log('\n3⃣ Setting up Claude tmux session...');
return new Promise((resolve) => {
// Check if session already exists
exec(`tmux has-session -t ${this.sessionName} 2>/dev/null`, (checkError) => {
if (!checkError) {
console.log('⚠️ Claude tmux session already exists');
console.log('🔄 Recreating session? (will kill existing session)');
// For simplicity, recreate directly
this.killAndCreateSession(resolve);
} else {
this.createNewSession(resolve);
}
});
});
}
killAndCreateSession(resolve) {
exec(`tmux kill-session -t ${this.sessionName} 2>/dev/null`, () => {
setTimeout(() => {
this.createNewSession(resolve);
}, 1000);
});
}
createNewSession(resolve) {
// Use current working directory as working directory for Claude session
const workingDir = process.cwd();
const command = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" clauderun`;
console.log(`🚀 Creating Claude tmux session: ${this.sessionName}`);
console.log(`📁 Working directory: ${workingDir}`);
console.log(`💡 Using convenience command: clauderun (equivalent to claude --dangerously-skip-permissions)`);
exec(command, (error, stdout, stderr) => {
if (error) {
console.log(`❌ Session creation failed: ${error.message}`);
if (stderr) {
console.log(`Error details: ${stderr}`);
}
// If clauderun fails, try using full path command
console.log('🔄 Trying full path command...');
const claudePath = process.env.CLAUDE_CLI_PATH || 'claude';
const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" ${claudePath} --dangerously-skip-permissions`;
exec(fallbackCommand, (fallbackError) => {
if (fallbackError) {
console.log(`❌ Full path command also failed: ${fallbackError.message}`);
} else {
console.log('✅ Claude tmux session created successfully (using full path)');
console.log(`📺 View session: tmux attach -t ${this.sessionName}`);
console.log(`🔚 Exit session: Ctrl+B, D (won't close Claude)`);
}
resolve();
});
} else {
console.log('✅ Claude tmux session created successfully');
console.log(`📺 View session: tmux attach -t ${this.sessionName}`);
console.log(`🔚 Exit session: Ctrl+B, D (won't close Claude)`);
resolve();
}
});
}
async testRemoteInjection() {
console.log('\n💡 Session is ready, you can start using it');
console.log('📋 Claude Code is waiting for your instructions');
console.log('🔧 To test injection functionality, please use separate test script');
return Promise.resolve();
}
showUsageGuide() {
console.log('\n🎉 Setup complete! Unattended remote control is ready\n');
console.log('🎯 New feature: clauderun convenience command');
console.log(' You can now use clauderun instead of claude --dangerously-skip-permissions');
console.log(' Clearer Claude Code startup method\n');
console.log('📋 Usage workflow:');
console.log('1. 🏠 Start email monitoring at home: npm run relay:pty');
console.log('2. 🚪 When going out, Claude continues running in tmux');
console.log('3. 📱 Receive Claude-Code-Remote email notifications on mobile');
console.log('4. 💬 Reply to email with commands on mobile');
console.log('5. 🤖 Claude at home automatically receives and executes commands');
console.log('6. 🔄 Repeat above process, completely unattended\n');
console.log('🔧 Management commands:');
console.log(` View Claude session: tmux attach -t ${this.sessionName}`);
console.log(` Exit session (without closing): Ctrl+B, D`);
console.log(` Kill session: tmux kill-session -t ${this.sessionName}`);
console.log(` View all sessions: tmux list-sessions\n`);
console.log('🎛️ Multi-session support:');
console.log(' Create custom session: node claude-control.js --session my-project');
console.log(' Create multiple sessions: node claude-control.js --session frontend');
console.log(' node claude-control.js --session backend');
console.log(' Email replies will automatically route to corresponding session\n');
console.log('📱 Email testing:');
console.log(' Token will include session information, automatically routing to correct tmux session');
console.log(` Recipient email: ${process.env.EMAIL_TO}`);
console.log(' Reply with command: echo "Remote control test"\n');
console.log('🚨 Important reminders:');
console.log('- Claude session runs continuously in tmux, won\'t be interrupted by network disconnection/reconnection');
console.log('- Email monitoring service needs to remain running');
console.log('- Home computer needs to stay powered on with network connection');
console.log('- Mobile can send email commands from anywhere');
console.log('- Supports running multiple Claude sessions for different projects simultaneously\n');
console.log('✅ Now you can achieve true unattended remote control! 🎯');
}
// Quick session restart method
async quickRestart() {
console.log('🔄 Quick restart Claude session...');
return new Promise((resolve) => {
this.killAndCreateSession(() => {
console.log('✅ Claude session restarted');
resolve();
});
});
}
}
// Command line parameter processing
if (require.main === module) {
const args = process.argv.slice(2);
// Parse session name parameter
let sessionName = null;
const sessionIndex = args.indexOf('--session');
if (sessionIndex !== -1 && args[sessionIndex + 1]) {
sessionName = args[sessionIndex + 1];
}
const setup = new RemoteControlSetup(sessionName);
if (sessionName) {
console.log(`🎛️ Using custom session name: ${sessionName}`);
}
if (args.includes('--restart')) {
setup.quickRestart();
} else {
setup.setup();
}
}
module.exports = RemoteControlSetup;

168
claude-hook-notify.js Executable file
View File

@ -0,0 +1,168 @@
#!/usr/bin/env node
/**
* Claude Hook Notification Script
* Called by Claude Code hooks to send Telegram notifications
*/
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
// Load environment variables from the project directory
const projectDir = path.dirname(__filename);
const envPath = path.join(projectDir, '.env');
console.log('🔍 Hook script started from:', process.cwd());
console.log('📁 Script location:', __filename);
console.log('🔧 Looking for .env at:', envPath);
if (fs.existsSync(envPath)) {
console.log('✅ .env file found, loading...');
dotenv.config({ path: envPath });
} else {
console.error('❌ .env file not found at:', envPath);
console.log('📂 Available files in script directory:');
try {
const files = fs.readdirSync(projectDir);
console.log(files.join(', '));
} catch (error) {
console.error('Cannot read directory:', error.message);
}
process.exit(1);
}
const TelegramChannel = require('./src/channels/telegram/telegram');
const DesktopChannel = require('./src/channels/local/desktop');
const EmailChannel = require('./src/channels/email/smtp');
async function sendHookNotification() {
try {
console.log('🔔 Claude Hook: Sending notifications...');
// Get notification type from command line argument
const notificationType = process.argv[2] || 'completed';
const channels = [];
const results = [];
// Configure Desktop channel (always enabled for sound)
const desktopChannel = new DesktopChannel({
completedSound: 'Glass',
waitingSound: 'Tink'
});
channels.push({ name: 'Desktop', channel: desktopChannel });
// Configure Telegram channel if enabled
if (process.env.TELEGRAM_ENABLED === 'true' && process.env.TELEGRAM_BOT_TOKEN) {
const telegramConfig = {
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID,
groupId: process.env.TELEGRAM_GROUP_ID
};
if (telegramConfig.botToken && (telegramConfig.chatId || telegramConfig.groupId)) {
const telegramChannel = new TelegramChannel(telegramConfig);
channels.push({ name: 'Telegram', channel: telegramChannel });
}
}
// Configure Email channel if enabled
if (process.env.EMAIL_ENABLED === 'true' && process.env.SMTP_USER) {
const emailConfig = {
smtp: {
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
},
from: process.env.EMAIL_FROM,
fromName: process.env.EMAIL_FROM_NAME,
to: process.env.EMAIL_TO
};
if (emailConfig.smtp.host && emailConfig.smtp.auth.user && emailConfig.to) {
const emailChannel = new EmailChannel(emailConfig);
channels.push({ name: 'Email', channel: emailChannel });
}
}
// Get current working directory and tmux session
const currentDir = process.cwd();
const projectName = path.basename(currentDir);
// Try to get current tmux session
let tmuxSession = process.env.TMUX_SESSION || 'claude-real';
try {
const { execSync } = require('child_process');
const sessionOutput = execSync('tmux display-message -p "#S"', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim();
if (sessionOutput) {
tmuxSession = sessionOutput;
}
} catch (error) {
// Not in tmux or tmux not available, use default
}
// Create notification
const notification = {
type: notificationType,
title: `Claude ${notificationType === 'completed' ? 'Task Completed' : 'Waiting for Input'}`,
message: `Claude has ${notificationType === 'completed' ? 'completed a task' : 'is waiting for input'}`,
project: projectName
// Don't set metadata here - let TelegramChannel extract real conversation content
};
console.log(`📱 Sending ${notificationType} notification for project: ${projectName}`);
console.log(`🖥️ Tmux session: ${tmuxSession}`);
// Send notifications to all configured channels
for (const { name, channel } of channels) {
try {
console.log(`📤 Sending to ${name}...`);
const result = await channel.send(notification);
results.push({ name, success: result });
if (result) {
console.log(`${name} notification sent successfully!`);
} else {
console.log(`❌ Failed to send ${name} notification`);
}
} catch (error) {
console.error(`${name} notification error:`, error.message);
results.push({ name, success: false, error: error.message });
}
}
// Report overall results
const successful = results.filter(r => r.success).length;
const total = results.length;
if (successful > 0) {
console.log(`\n✅ Successfully sent notifications via ${successful}/${total} channels`);
if (results.some(r => r.name === 'Telegram' && r.success)) {
console.log('📋 You can now send new commands via Telegram');
}
} else {
console.log('\n❌ All notification channels failed');
process.exit(1);
}
} catch (error) {
console.error('❌ Hook notification error:', error.message);
process.exit(1);
}
}
// Show usage if no arguments
if (process.argv.length < 2) {
console.log('Usage: node claude-hook-notify.js [completed|waiting]');
process.exit(1);
}
sendHookNotification();

28
claude-hooks.json Normal file
View File

@ -0,0 +1,28 @@
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node /Users/jessytsui/dev/Claude-Code-Remote/claude-hook-notify.js completed",
"timeout": 5
}
]
}
],
"SubagentStop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node /Users/jessytsui/dev/Claude-Code-Remote/claude-hook-notify.js waiting",
"timeout": 5
}
]
}
]
}
}

View File

@ -19,10 +19,12 @@
}, },
"telegram": { "telegram": {
"type": "chat", "type": "chat",
"enabled": false, "enabled": true,
"config": { "config": {
"token": "", "botToken": "",
"chatId": "" "chatId": "",
"groupId": "",
"whitelist": []
} }
}, },
"whatsapp": { "whatsapp": {
@ -40,5 +42,16 @@
"webhook": "", "webhook": "",
"secret": "" "secret": ""
} }
},
"line": {
"type": "chat",
"enabled": true,
"config": {
"channelAccessToken": "",
"channelSecret": "",
"userId": "",
"groupId": "",
"whitelist": []
}
} }
} }

120
fix-telegram.sh Executable file
View File

@ -0,0 +1,120 @@
#!/bin/bash
# Telegram修复脚本 - 自动重启ngrok和更新webhook
# Fix Telegram Script - Auto restart ngrok and update webhook
set -e
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$PROJECT_DIR/.env"
echo "🔧 Telegram Remote Control 修复脚本"
echo "📁 项目目录: $PROJECT_DIR"
# 检查.env文件
if [ ! -f "$ENV_FILE" ]; then
echo "❌ .env文件不存在: $ENV_FILE"
exit 1
fi
# 加载环境变量
source "$ENV_FILE"
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
echo "❌ TELEGRAM_BOT_TOKEN未设置"
exit 1
fi
# 停止旧的ngrok进程
echo "🔄 停止旧的ngrok进程..."
pkill -f "ngrok http" || true
sleep 2
# 启动新的ngrok隧道
echo "🚀 启动ngrok隧道..."
nohup ngrok http 3001 > /dev/null 2>&1 &
sleep 5
# 获取新的ngrok URL
echo "🔍 获取新的ngrok URL..."
NEW_URL=""
for i in {1..10}; do
NEW_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url' 2>/dev/null || echo "")
if [ -n "$NEW_URL" ] && [ "$NEW_URL" != "null" ]; then
break
fi
echo "等待ngrok启动... ($i/10)"
sleep 2
done
if [ -z "$NEW_URL" ] || [ "$NEW_URL" = "null" ]; then
echo "❌ 无法获取ngrok URL"
exit 1
fi
echo "✅ 新的ngrok URL: $NEW_URL"
# 更新.env文件
echo "📝 更新.env文件..."
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' "s|TELEGRAM_WEBHOOK_URL=.*|TELEGRAM_WEBHOOK_URL=$NEW_URL|" "$ENV_FILE"
else
# Linux
sed -i "s|TELEGRAM_WEBHOOK_URL=.*|TELEGRAM_WEBHOOK_URL=$NEW_URL|" "$ENV_FILE"
fi
# 设置新的webhook
echo "🔗 设置Telegram webhook..."
WEBHOOK_URL="$NEW_URL/webhook/telegram"
RESPONSE=$(curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
-H "Content-Type: application/json" \
-d "{\"url\": \"$WEBHOOK_URL\", \"allowed_updates\": [\"message\", \"callback_query\"]}")
if echo "$RESPONSE" | grep -q '"ok":true'; then
echo "✅ Webhook设置成功: $WEBHOOK_URL"
else
echo "❌ Webhook设置失败: $RESPONSE"
exit 1
fi
# 验证webhook状态
echo "🔍 验证webhook状态..."
WEBHOOK_INFO=$(curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo")
echo "📊 Webhook信息: $WEBHOOK_INFO"
# 测试健康检查
echo "🏥 测试健康检查..."
HEALTH_RESPONSE=$(curl -s "$NEW_URL/health" || echo "failed")
if echo "$HEALTH_RESPONSE" | grep -q '"status":"ok"'; then
echo "✅ 健康检查通过"
else
echo "⚠️ 健康检查失败请确保webhook服务正在运行"
echo "运行: node start-telegram-webhook.js"
fi
echo ""
echo "🎉 修复完成!"
echo "📱 新的webhook URL: $WEBHOOK_URL"
echo "🧪 发送测试消息..."
# 发送测试消息
# 优先发送到群组,如果没有群组则发送到个人聊天
CHAT_TARGET="$TELEGRAM_GROUP_ID"
if [ -z "$CHAT_TARGET" ]; then
CHAT_TARGET="$TELEGRAM_CHAT_ID"
fi
if [ -n "$CHAT_TARGET" ]; then
curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": $CHAT_TARGET, \"text\": \"🎉 Telegram Remote Control已修复并重新配置\\n\\n新的webhook URL: $WEBHOOK_URL\\n\\n现在你可以接收Claude通知了。\"}" > /dev/null
echo "✅ 测试消息已发送到Telegram (Chat ID: $CHAT_TARGET)"
else
echo "⚠️ 未配置Telegram Chat ID或Group ID"
fi
echo ""
echo "🔥 下一步:"
echo "1⃣ 确保webhook服务正在运行: node start-telegram-webhook.js"
echo "2⃣ 在tmux中设置Claude hooks: export CLAUDE_HOOKS_CONFIG=$PROJECT_DIR/claude-hooks.json"
echo "3⃣ 启动Claude: claude"

View File

@ -1,155 +0,0 @@
#!/usr/bin/env node
/**
* Global Installation Script for Claude-Code-Remote claude-control
* Makes claude-control.js accessible from any directory
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const SCRIPT_NAME = 'claude-control';
const SOURCE_PATH = path.join(__dirname, 'claude-control.js');
const TARGET_DIR = '/usr/local/bin';
const TARGET_PATH = path.join(TARGET_DIR, SCRIPT_NAME);
function checkRequirements() {
// Check if claude-control.js exists
if (!fs.existsSync(SOURCE_PATH)) {
console.error('❌ Error: claude-control.js not found in current directory');
process.exit(1);
}
// Check if /usr/local/bin is writable
try {
fs.accessSync(TARGET_DIR, fs.constants.W_OK);
} catch (error) {
console.error('❌ Error: No write permission to /usr/local/bin');
console.log('💡 Try running with sudo:');
console.log(' sudo node install-global.js');
process.exit(1);
}
}
function createGlobalScript() {
const scriptContent = `#!/usr/bin/env node
/**
* Global Claude Control Wrapper
* Executes claude-control.js from its original location
*/
const path = require('path');
const { spawn } = require('child_process');
// Claude-Code-Remote installation directory
const CLAUDE_CODE_REMOTE_DIR = '${__dirname}';
const CLAUDE_CONTROL_PATH = path.join(CLAUDE_CODE_REMOTE_DIR, 'claude-control.js');
// Get command line arguments (excluding node and script name)
const args = process.argv.slice(2);
// Change to Claude-Code-Remote directory before execution
process.chdir(CLAUDE_CODE_REMOTE_DIR);
// Execute claude-control.js with original arguments
const child = spawn('node', [CLAUDE_CONTROL_PATH, ...args], {
stdio: 'inherit',
env: { ...process.env, CLAUDE_CODE_REMOTE_HOME: CLAUDE_CODE_REMOTE_DIR }
});
child.on('error', (error) => {
console.error('Error executing claude-control:', error.message);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
});
`;
return scriptContent;
}
function install() {
console.log('🚀 Installing claude-control globally...\n');
try {
// Create the global script
const scriptContent = createGlobalScript();
fs.writeFileSync(TARGET_PATH, scriptContent);
// Make it executable
fs.chmodSync(TARGET_PATH, 0o755);
console.log('✅ Installation completed successfully!');
console.log(`📁 Installed to: ${TARGET_PATH}`);
console.log('\n🎉 Usage:');
console.log(' claude-control --session myproject');
console.log(' claude-control --list');
console.log(' claude-control --kill all');
console.log('\nYou can now run claude-control from any directory!');
} catch (error) {
console.error('❌ Installation failed:', error.message);
process.exit(1);
}
}
function uninstall() {
console.log('🗑️ Uninstalling claude-control...\n');
try {
if (fs.existsSync(TARGET_PATH)) {
fs.unlinkSync(TARGET_PATH);
console.log('✅ Uninstallation completed successfully!');
console.log(`🗑️ Removed: ${TARGET_PATH}`);
} else {
console.log('⚠️ claude-control is not installed globally');
}
} catch (error) {
console.error('❌ Uninstallation failed:', error.message);
process.exit(1);
}
}
function showHelp() {
console.log('Claude-Code-Remote Claude Control - Global Installation\n');
console.log('Usage:');
console.log(' node install-global.js [install] - Install globally');
console.log(' node install-global.js uninstall - Uninstall');
console.log(' node install-global.js --help - Show this help\n');
console.log('Requirements:');
console.log(' - Write permission to /usr/local/bin (may need sudo)');
console.log(' - claude-control.js must exist in current directory');
}
function main() {
const command = process.argv[2];
if (command === '--help' || command === '-h') {
showHelp();
return;
}
if (command === 'uninstall') {
uninstall();
return;
}
// Default action is install
checkRequirements();
install();
}
// Run only if this script is executed directly
if (require.main === module) {
main();
}
module.exports = { install, uninstall };

909
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,10 @@
"daemon:stop": "node claude-remote.js daemon stop", "daemon:stop": "node claude-remote.js daemon stop",
"daemon:status": "node claude-remote.js daemon status", "daemon:status": "node claude-remote.js daemon status",
"relay:pty": "node start-relay-pty.js", "relay:pty": "node start-relay-pty.js",
"relay:start": "INJECTION_MODE=pty node src/relay/relay-pty.js" "relay:start": "INJECTION_MODE=pty node src/relay/relay-pty.js",
"telegram": "node start-telegram-webhook.js",
"line": "node start-line-webhook.js",
"webhooks": "node start-all-webhooks.js"
}, },
"keywords": [ "keywords": [
"claude-code", "claude-code",
@ -41,8 +44,10 @@
}, },
"homepage": "https://github.com/Claude-Code-Remote/Claude-Code-Remote#readme", "homepage": "https://github.com/Claude-Code-Remote/Claude-Code-Remote#readme",
"dependencies": { "dependencies": {
"axios": "^1.6.0",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"execa": "^9.6.0", "execa": "^9.6.0",
"express": "^4.18.2",
"imapflow": "^1.0.191", "imapflow": "^1.0.191",
"mailparser": "^3.7.4", "mailparser": "^3.7.4",
"node-imap": "^0.9.6", "node-imap": "^0.9.6",

View File

@ -1,70 +0,0 @@
/**
* Send test email reply to relay service
*/
const nodemailer = require('nodemailer');
require('dotenv').config();
async function sendTestReply() {
console.log('📧 Sending test email reply...\n');
// Create test SMTP transporter (using environment variables)
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
// Generate or use test token from environment
let testToken = process.env.TEST_TOKEN;
if (!testToken) {
// Try to read latest token from session map
try {
const sessionMapPath = process.env.SESSION_MAP_PATH || './src/data/session-map.json';
if (require('fs').existsSync(sessionMapPath)) {
const sessionMap = JSON.parse(require('fs').readFileSync(sessionMapPath, 'utf8'));
const tokens = Object.keys(sessionMap);
testToken = tokens[tokens.length - 1]; // Use latest token
}
} catch (error) {
console.log('Could not read session map, using generated token');
}
// Fallback: generate a test token
if (!testToken) {
testToken = Math.random().toString(36).substr(2, 8).toUpperCase();
}
}
const mailOptions = {
from: process.env.SMTP_USER,
to: process.env.SMTP_USER, // Self-send for testing
subject: `Re: [Claude-Code-Remote #${testToken}] Claude Code Task Completed - Claude-Code-Remote`,
text: 'Please explain the basic principles of quantum computing',
replyTo: process.env.EMAIL_TO || process.env.ALLOWED_SENDERS
};
try {
const info = await transporter.sendMail(mailOptions);
console.log('✅ Test email sent successfully!');
console.log(`📧 Message ID: ${info.messageId}`);
console.log(`📋 Token: ${testToken}`);
console.log(`💬 Command: ${mailOptions.text}`);
console.log('\n🔍 Now monitoring relay service logs...');
// Wait a few seconds for email processing
setTimeout(() => {
console.log('\n📋 Please check relay-debug.log file for processing logs');
}, 5000);
} catch (error) {
console.error('❌ Email sending failed:', error.message);
}
}
sendTestReply().catch(console.error);

74
setup-telegram.sh Executable file
View File

@ -0,0 +1,74 @@
#!/bin/bash
# Claude Code Remote - Telegram Quick Setup Script
# This script helps you quickly set up Telegram notifications
echo "🚀 Claude Code Remote - Telegram Setup"
echo "====================================="
# Check if .env exists
if [ ! -f ".env" ]; then
echo "📋 Creating .env from template..."
cp .env.example .env
else
echo "✅ .env file already exists"
fi
# Get project directory
PROJECT_DIR=$(pwd)
echo "📁 Project directory: $PROJECT_DIR"
# Check if claude-hooks.json exists
if [ ! -f "claude-hooks.json" ]; then
echo "📝 Creating claude-hooks.json..."
cat > claude-hooks.json << EOF
{
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "node $PROJECT_DIR/claude-hook-notify.js completed",
"timeout": 5
}]
}],
"SubagentStop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "node $PROJECT_DIR/claude-hook-notify.js waiting",
"timeout": 5
}]
}]
}
}
EOF
echo "✅ claude-hooks.json created"
else
echo "✅ claude-hooks.json already exists"
fi
# Create data directory
mkdir -p src/data
echo "✅ Data directory ready"
echo ""
echo "📋 Next Steps:"
echo "1. Edit .env and add your Telegram credentials:"
echo " - TELEGRAM_BOT_TOKEN (from @BotFather)"
echo " - TELEGRAM_CHAT_ID (your chat ID)"
echo " - TELEGRAM_WEBHOOK_URL (your ngrok URL)"
echo ""
echo "2. Start ngrok in a terminal:"
echo " ngrok http 3001"
echo ""
echo "3. Start Telegram webhook in another terminal:"
echo " node start-telegram-webhook.js"
echo ""
echo "4. Start Claude with hooks in a third terminal:"
echo " export CLAUDE_HOOKS_CONFIG=$PROJECT_DIR/claude-hooks.json"
echo " claude"
echo ""
echo "5. Test by running a task in Claude!"
echo ""
echo "For detailed instructions, see README.md"

293
smart-monitor.js Normal file
View File

@ -0,0 +1,293 @@
#!/usr/bin/env node
/**
* Smart Monitor - 智能監控器能檢測歷史回應和新回應
* 解決監控器錯過已完成回應的問題
*/
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
const { execSync } = require('child_process');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
const TelegramChannel = require('./src/channels/telegram/telegram');
class SmartMonitor {
constructor() {
this.sessionName = process.env.TMUX_SESSION || 'claude-real';
this.lastOutput = '';
this.processedResponses = new Set(); // 記錄已處理的回應
this.checkInterval = 1000; // Check every 1 second
this.isRunning = false;
this.startupTime = Date.now();
// Setup Telegram
if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID) {
const telegramConfig = {
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID
};
this.telegram = new TelegramChannel(telegramConfig);
console.log('📱 Smart Monitor configured successfully');
} else {
console.log('❌ Telegram not configured');
process.exit(1);
}
}
start() {
this.isRunning = true;
console.log(`🧠 Starting smart monitor for session: ${this.sessionName}`);
// Check for any unprocessed responses on startup
this.checkForUnprocessedResponses();
// Initial capture
this.lastOutput = this.captureOutput();
// Start monitoring
this.monitor();
}
async checkForUnprocessedResponses() {
console.log('🔍 Checking for unprocessed responses...');
const currentOutput = this.captureOutput();
const responses = this.extractAllResponses(currentOutput);
// Check if there are recent responses (within 5 minutes) that might be unprocessed
const recentResponses = responses.filter(response => {
const responseAge = Date.now() - this.startupTime;
return responseAge < 5 * 60 * 1000; // 5 minutes
});
if (recentResponses.length > 0) {
console.log(`🎯 Found ${recentResponses.length} potentially unprocessed responses`);
// Send notification for the most recent response
const latestResponse = recentResponses[recentResponses.length - 1];
await this.sendNotificationForResponse(latestResponse);
} else {
console.log('✅ No unprocessed responses found');
}
}
captureOutput() {
try {
return execSync(`tmux capture-pane -t ${this.sessionName} -p`, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
});
} catch (error) {
console.error('Error capturing tmux:', error.message);
return '';
}
}
autoApproveDialog() {
try {
console.log('🤖 Auto-approving Claude tool usage dialog...');
// Send "1" to select the first option (usually "Yes")
execSync(`tmux send-keys -t ${this.sessionName} '1'`, { encoding: 'utf8' });
setTimeout(() => {
execSync(`tmux send-keys -t ${this.sessionName} Enter`, { encoding: 'utf8' });
}, 100);
console.log('✅ Auto-approval sent successfully');
} catch (error) {
console.error('❌ Failed to auto-approve dialog:', error.message);
}
}
extractAllResponses(content) {
const lines = content.split('\n');
const responses = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Look for standard Claude responses
if (line.startsWith('⏺ ') && line.length > 2) {
const responseText = line.substring(2).trim();
// Find the corresponding user question
let userQuestion = 'Recent command';
for (let j = i - 1; j >= 0; j--) {
const prevLine = lines[j].trim();
if (prevLine.startsWith('> ') && prevLine.length > 2) {
userQuestion = prevLine.substring(2).trim();
break;
}
}
responses.push({
userQuestion,
claudeResponse: responseText,
lineIndex: i,
responseId: `${userQuestion}-${responseText}`.substring(0, 50),
type: 'standard'
});
}
// Look for interactive dialogs/tool confirmations
if (line.includes('Do you want to proceed?') ||
line.includes(' 1. Yes') ||
line.includes('Tool use') ||
(line.includes('│') && (line.includes('serena') || line.includes('MCP') || line.includes('initial_instructions')))) {
// Check if this is part of a tool use dialog
let dialogContent = '';
let userQuestion = 'Recent command';
// Look backward to find the start of the dialog and user question
for (let j = i; j >= Math.max(0, i - 50); j--) {
const prevLine = lines[j];
if (prevLine.includes('╭') || prevLine.includes('Tool use')) {
// Found start of dialog box, now collect all content
for (let k = j; k <= Math.min(lines.length - 1, i + 20); k++) {
if (lines[k].includes('╰')) {
dialogContent += lines[k] + '\n';
break; // End of dialog box
}
dialogContent += lines[k] + '\n';
}
break;
}
// Look for user question
if (prevLine.startsWith('> ') && prevLine.length > 2) {
userQuestion = prevLine.substring(2).trim();
}
}
if (dialogContent.length > 50) { // Only if we found substantial dialog
// Auto-approve the dialog instead of asking user to go to iTerm2
this.autoApproveDialog();
responses.push({
userQuestion,
claudeResponse: 'Claude requested tool permission - automatically approved. Processing...',
lineIndex: i,
responseId: `dialog-${userQuestion}-${Date.now()}`.substring(0, 50),
type: 'interactive',
fullDialog: dialogContent.substring(0, 500)
});
break; // Only send one dialog notification per check
}
}
}
return responses;
}
async monitor() {
while (this.isRunning) {
await this.sleep(this.checkInterval);
const currentOutput = this.captureOutput();
if (currentOutput !== this.lastOutput) {
console.log('📝 Output changed, checking for new responses...');
const oldResponses = this.extractAllResponses(this.lastOutput);
const newResponses = this.extractAllResponses(currentOutput);
// Find truly new responses
const newResponseIds = new Set(newResponses.map(r => r.responseId));
const oldResponseIds = new Set(oldResponses.map(r => r.responseId));
const actuallyNewResponses = newResponses.filter(response =>
!oldResponseIds.has(response.responseId) &&
!this.processedResponses.has(response.responseId)
);
if (actuallyNewResponses.length > 0) {
console.log(`🎯 Found ${actuallyNewResponses.length} new responses`);
for (const response of actuallyNewResponses) {
await this.sendNotificationForResponse(response);
this.processedResponses.add(response.responseId);
}
} else {
console.log(' No new responses detected');
}
this.lastOutput = currentOutput;
}
}
}
async sendNotificationForResponse(response) {
try {
console.log('📤 Sending notification for response:', response.claudeResponse.substring(0, 50) + '...');
const notification = {
type: 'completed',
title: 'Claude Response Ready',
message: 'Claude has responded to your command',
project: 'claude-code-line',
metadata: {
userQuestion: response.userQuestion,
claudeResponse: response.claudeResponse,
tmuxSession: this.sessionName,
workingDirectory: process.cwd(),
timestamp: new Date().toISOString(),
autoDetected: true
}
};
const result = await this.telegram.send(notification);
if (result) {
console.log('✅ Notification sent successfully');
} else {
console.log('❌ Failed to send notification');
}
} catch (error) {
console.error('❌ Notification error:', error.message);
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
stop() {
this.isRunning = false;
console.log('⏹️ Smart Monitor stopped');
}
getStatus() {
return {
isRunning: this.isRunning,
sessionName: this.sessionName,
processedCount: this.processedResponses.size,
uptime: Math.floor((Date.now() - this.startupTime) / 1000) + 's'
};
}
}
// Handle graceful shutdown
const monitor = new SmartMonitor();
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down...');
monitor.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 Shutting down...');
monitor.stop();
process.exit(0);
});
// Start monitoring
monitor.start();

198
src/channels/line/line.js Normal file
View File

@ -0,0 +1,198 @@
/**
* LINE Notification Channel
* Sends notifications via LINE Messaging API with command support
*/
const NotificationChannel = require('../base/channel');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const fs = require('fs');
const TmuxMonitor = require('../../utils/tmux-monitor');
const { execSync } = require('child_process');
class LINEChannel extends NotificationChannel {
constructor(config = {}) {
super('line', config);
this.sessionsDir = path.join(__dirname, '../../data/sessions');
this.tmuxMonitor = new TmuxMonitor();
this.lineApiUrl = 'https://api.line.me/v2/bot/message';
this._ensureDirectories();
this._validateConfig();
}
_ensureDirectories() {
if (!fs.existsSync(this.sessionsDir)) {
fs.mkdirSync(this.sessionsDir, { recursive: true });
}
}
_validateConfig() {
if (!this.config.channelAccessToken) {
this.logger.warn('LINE Channel Access Token not found');
return false;
}
if (!this.config.userId && !this.config.groupId) {
this.logger.warn('LINE User ID or Group ID must be configured');
return false;
}
return true;
}
_generateToken() {
// Generate short Token (uppercase letters + numbers, 8 digits)
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let token = '';
for (let i = 0; i < 8; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length));
}
return token;
}
_getCurrentTmuxSession() {
try {
// Try to get current tmux session
const tmuxSession = execSync('tmux display-message -p "#S"', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim();
return tmuxSession || null;
} catch (error) {
// Not in a tmux session or tmux not available
return null;
}
}
async _sendImpl(notification) {
if (!this._validateConfig()) {
throw new Error('LINE channel not properly configured');
}
// Generate session ID and Token
const sessionId = uuidv4();
const token = this._generateToken();
// Get current tmux session and conversation content
const tmuxSession = this._getCurrentTmuxSession();
if (tmuxSession && !notification.metadata) {
const conversation = this.tmuxMonitor.getRecentConversation(tmuxSession);
notification.metadata = {
userQuestion: conversation.userQuestion || notification.message,
claudeResponse: conversation.claudeResponse || notification.message,
tmuxSession: tmuxSession
};
}
// Create session record
await this._createSession(sessionId, notification, token);
// Generate LINE message
const messages = this._generateLINEMessage(notification, sessionId, token);
// Determine recipient (user or group)
const to = this.config.groupId || this.config.userId;
const requestData = {
to: to,
messages: messages
};
try {
const response = await axios.post(
`${this.lineApiUrl}/push`,
requestData,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.channelAccessToken}`
}
}
);
this.logger.info(`LINE message sent successfully, Session: ${sessionId}`);
return true;
} catch (error) {
this.logger.error('Failed to send LINE message:', error.response?.data || error.message);
// Clean up failed session
await this._removeSession(sessionId);
return false;
}
}
_generateLINEMessage(notification, sessionId, token) {
const type = notification.type;
const emoji = type === 'completed' ? '✅' : '⏳';
const status = type === 'completed' ? '已完成' : '等待輸入';
let messageText = `${emoji} Claude 任務 ${status}\n`;
messageText += `專案: ${notification.project}\n`;
messageText += `會話 Token: ${token}\n\n`;
if (notification.metadata) {
if (notification.metadata.userQuestion) {
messageText += `📝 您的問題:\n${notification.metadata.userQuestion.substring(0, 200)}`;
if (notification.metadata.userQuestion.length > 200) {
messageText += '...';
}
messageText += '\n\n';
}
if (notification.metadata.claudeResponse) {
messageText += `🤖 Claude 回應:\n${notification.metadata.claudeResponse.substring(0, 300)}`;
if (notification.metadata.claudeResponse.length > 300) {
messageText += '...';
}
messageText += '\n\n';
}
}
messageText += `💬 回覆此訊息並輸入:\n`;
messageText += `Token ${token} <您的指令>\n`;
messageText += `來發送新指令給 Claude`;
return [{
type: 'text',
text: messageText
}];
}
async _createSession(sessionId, notification, token) {
const session = {
id: sessionId,
token: token,
type: 'line',
created: new Date().toISOString(),
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // Expires after 24 hours
createdAt: Math.floor(Date.now() / 1000),
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
tmuxSession: notification.metadata?.tmuxSession || 'default',
project: notification.project,
notification: notification
};
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2));
this.logger.debug(`Session created: ${sessionId}`);
}
async _removeSession(sessionId) {
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
if (fs.existsSync(sessionFile)) {
fs.unlinkSync(sessionFile);
this.logger.debug(`Session removed: ${sessionId}`);
}
}
supportsRelay() {
return true;
}
validateConfig() {
return this._validateConfig();
}
}
module.exports = LINEChannel;

View File

@ -0,0 +1,225 @@
/**
* LINE Webhook Handler
* Handles incoming LINE messages and commands
*/
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const path = require('path');
const fs = require('fs');
const Logger = require('../../core/logger');
const ControllerInjector = require('../../utils/controller-injector');
class LINEWebhookHandler {
constructor(config = {}) {
this.config = config;
this.logger = new Logger('LINEWebhook');
this.sessionsDir = path.join(__dirname, '../../data/sessions');
this.injector = new ControllerInjector();
this.app = express();
this._setupMiddleware();
this._setupRoutes();
}
_setupMiddleware() {
// Parse raw body for signature verification
this.app.use('/webhook', express.raw({ type: 'application/json' }));
// Parse JSON for other routes
this.app.use(express.json());
}
_setupRoutes() {
// LINE webhook endpoint
this.app.post('/webhook', this._handleWebhook.bind(this));
// Health check endpoint
this.app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'line-webhook' });
});
}
_validateSignature(body, signature) {
if (!this.config.channelSecret) {
this.logger.error('Channel Secret not configured');
return false;
}
const hash = crypto
.createHmac('SHA256', this.config.channelSecret)
.update(body)
.digest('base64');
return hash === signature;
}
async _handleWebhook(req, res) {
const signature = req.headers['x-line-signature'];
// Validate signature
if (!this._validateSignature(req.body, signature)) {
this.logger.warn('Invalid signature');
return res.status(401).send('Unauthorized');
}
try {
const events = JSON.parse(req.body.toString()).events;
for (const event of events) {
if (event.type === 'message' && event.message.type === 'text') {
await this._handleTextMessage(event);
}
}
res.status(200).send('OK');
} catch (error) {
this.logger.error('Webhook handling error:', error.message);
res.status(500).send('Internal Server Error');
}
}
async _handleTextMessage(event) {
const userId = event.source.userId;
const groupId = event.source.groupId;
const messageText = event.message.text.trim();
const replyToken = event.replyToken;
// Check if user is authorized
if (!this._isAuthorized(userId, groupId)) {
this.logger.warn(`Unauthorized user/group: ${userId || groupId}`);
await this._replyMessage(replyToken, '⚠️ 您沒有權限使用此功能');
return;
}
// Parse command
const commandMatch = messageText.match(/^Token\s+([A-Z0-9]{8})\s+(.+)$/i);
if (!commandMatch) {
await this._replyMessage(replyToken,
'❌ 格式錯誤。請使用:\nToken <8位Token> <您的指令>\n\n例如:\nToken ABC12345 請幫我分析這段程式碼');
return;
}
const token = commandMatch[1].toUpperCase();
const command = commandMatch[2];
// Find session by token
const session = await this._findSessionByToken(token);
if (!session) {
await this._replyMessage(replyToken,
'❌ Token 無效或已過期。請等待新的任務通知。');
return;
}
// Check if session is expired
if (session.expiresAt < Math.floor(Date.now() / 1000)) {
await this._replyMessage(replyToken,
'❌ Token 已過期。請等待新的任務通知。');
await this._removeSession(session.id);
return;
}
try {
// Inject command into tmux session
const tmuxSession = session.tmuxSession || 'default';
await this.injector.injectCommand(command, tmuxSession);
// Send confirmation
await this._replyMessage(replyToken,
`✅ 指令已發送\n\n📝 指令: ${command}\n🖥️ 會話: ${tmuxSession}\n\n請稍候Claude 正在處理您的請求...`);
// Log command execution
this.logger.info(`Command injected - User: ${userId}, Token: ${token}, Command: ${command}`);
} catch (error) {
this.logger.error('Command injection failed:', error.message);
await this._replyMessage(replyToken,
`❌ 指令執行失敗: ${error.message}`);
}
}
_isAuthorized(userId, groupId) {
// Check whitelist
const whitelist = this.config.whitelist || [];
if (groupId && whitelist.includes(groupId)) {
return true;
}
if (userId && whitelist.includes(userId)) {
return true;
}
// If no whitelist configured, allow configured user/group
if (whitelist.length === 0) {
if (groupId && groupId === this.config.groupId) {
return true;
}
if (userId && userId === this.config.userId) {
return true;
}
}
return false;
}
async _findSessionByToken(token) {
const files = fs.readdirSync(this.sessionsDir);
for (const file of files) {
if (!file.endsWith('.json')) continue;
const sessionPath = path.join(this.sessionsDir, file);
try {
const session = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (session.token === token) {
return session;
}
} catch (error) {
this.logger.error(`Failed to read session file ${file}:`, error.message);
}
}
return null;
}
async _removeSession(sessionId) {
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
if (fs.existsSync(sessionFile)) {
fs.unlinkSync(sessionFile);
this.logger.debug(`Session removed: ${sessionId}`);
}
}
async _replyMessage(replyToken, text) {
try {
await axios.post(
'https://api.line.me/v2/bot/message/reply',
{
replyToken: replyToken,
messages: [{
type: 'text',
text: text
}]
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.config.channelAccessToken}`
}
}
);
} catch (error) {
this.logger.error('Failed to reply message:', error.response?.data || error.message);
}
}
start(port = 3000) {
this.app.listen(port, () => {
this.logger.info(`LINE webhook server started on port ${port}`);
});
}
}
module.exports = LINEWebhookHandler;

View File

@ -0,0 +1,232 @@
/**
* Telegram Notification Channel
* Sends notifications via Telegram Bot API with command support
*/
const NotificationChannel = require('../base/channel');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const fs = require('fs');
const TmuxMonitor = require('../../utils/tmux-monitor');
const { execSync } = require('child_process');
class TelegramChannel extends NotificationChannel {
constructor(config = {}) {
super('telegram', config);
this.sessionsDir = path.join(__dirname, '../../data/sessions');
this.tmuxMonitor = new TmuxMonitor();
this.apiBaseUrl = 'https://api.telegram.org';
this.botUsername = null; // Cache for bot username
this._ensureDirectories();
this._validateConfig();
}
_ensureDirectories() {
if (!fs.existsSync(this.sessionsDir)) {
fs.mkdirSync(this.sessionsDir, { recursive: true });
}
}
_validateConfig() {
if (!this.config.botToken) {
this.logger.warn('Telegram Bot Token not found');
return false;
}
if (!this.config.chatId && !this.config.groupId) {
this.logger.warn('Telegram Chat ID or Group ID must be configured');
return false;
}
return true;
}
_generateToken() {
// Generate short Token (uppercase letters + numbers, 8 digits)
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let token = '';
for (let i = 0; i < 8; i++) {
token += chars.charAt(Math.floor(Math.random() * chars.length));
}
return token;
}
_getCurrentTmuxSession() {
try {
// Try to get current tmux session
const tmuxSession = execSync('tmux display-message -p "#S"', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim();
return tmuxSession || null;
} catch (error) {
// Not in a tmux session or tmux not available
return null;
}
}
async _getBotUsername() {
if (this.botUsername) {
return this.botUsername;
}
try {
const response = await axios.get(
`${this.apiBaseUrl}/bot${this.config.botToken}/getMe`
);
if (response.data.ok && response.data.result.username) {
this.botUsername = response.data.result.username;
return this.botUsername;
}
} catch (error) {
this.logger.error('Failed to get bot username:', error.message);
}
// Fallback to configured username or default
return this.config.botUsername || 'claude_remote_bot';
}
async _sendImpl(notification) {
if (!this._validateConfig()) {
throw new Error('Telegram channel not properly configured');
}
// Generate session ID and Token
const sessionId = uuidv4();
const token = this._generateToken();
// Get current tmux session and conversation content
const tmuxSession = this._getCurrentTmuxSession();
if (tmuxSession && !notification.metadata) {
const conversation = this.tmuxMonitor.getRecentConversation(tmuxSession);
notification.metadata = {
userQuestion: conversation.userQuestion || notification.message,
claudeResponse: conversation.claudeResponse || notification.message,
tmuxSession: tmuxSession
};
}
// Create session record
await this._createSession(sessionId, notification, token);
// Generate Telegram message
const messageText = this._generateTelegramMessage(notification, sessionId, token);
// Determine recipient (chat or group)
const chatId = this.config.groupId || this.config.chatId;
const isGroupChat = !!this.config.groupId;
// Create buttons using callback_data instead of inline query
// This avoids the automatic @bot_name addition
const buttons = [
[
{
text: '📝 Personal Chat',
callback_data: `personal:${token}`
},
{
text: '👥 Group Chat',
callback_data: `group:${token}`
}
]
];
const requestData = {
chat_id: chatId,
text: messageText,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: buttons
}
};
try {
const response = await axios.post(
`${this.apiBaseUrl}/bot${this.config.botToken}/sendMessage`,
requestData
);
this.logger.info(`Telegram message sent successfully, Session: ${sessionId}`);
return true;
} catch (error) {
this.logger.error('Failed to send Telegram message:', error.response?.data || error.message);
// Clean up failed session
await this._removeSession(sessionId);
return false;
}
}
_generateTelegramMessage(notification, sessionId, token) {
const type = notification.type;
const emoji = type === 'completed' ? '✅' : '⏳';
const status = type === 'completed' ? 'Completed' : 'Waiting for Input';
let messageText = `${emoji} *Claude Task ${status}*\n`;
messageText += `*Project:* ${notification.project}\n`;
messageText += `*Session Token:* \`${token}\`\n\n`;
if (notification.metadata) {
if (notification.metadata.userQuestion) {
messageText += `📝 *Your Question:*\n${notification.metadata.userQuestion.substring(0, 200)}`;
if (notification.metadata.userQuestion.length > 200) {
messageText += '...';
}
messageText += '\n\n';
}
if (notification.metadata.claudeResponse) {
messageText += `🤖 *Claude Response:*\n${notification.metadata.claudeResponse.substring(0, 300)}`;
if (notification.metadata.claudeResponse.length > 300) {
messageText += '...';
}
messageText += '\n\n';
}
}
messageText += `💬 *To send a new command:*\n`;
messageText += `Reply with: \`/cmd ${token} <your command>\`\n`;
messageText += `Example: \`/cmd ${token} Please analyze this code\``;
return messageText;
}
async _createSession(sessionId, notification, token) {
const session = {
id: sessionId,
token: token,
type: 'telegram',
created: new Date().toISOString(),
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // Expires after 24 hours
createdAt: Math.floor(Date.now() / 1000),
expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000),
tmuxSession: notification.metadata?.tmuxSession || 'default',
project: notification.project,
notification: notification
};
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2));
this.logger.debug(`Session created: ${sessionId}`);
}
async _removeSession(sessionId) {
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
if (fs.existsSync(sessionFile)) {
fs.unlinkSync(sessionFile);
this.logger.debug(`Session removed: ${sessionId}`);
}
}
supportsRelay() {
return true;
}
validateConfig() {
return this._validateConfig();
}
}
module.exports = TelegramChannel;

View File

@ -0,0 +1,326 @@
/**
* Telegram Webhook Handler
* Handles incoming Telegram messages and commands
*/
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const path = require('path');
const fs = require('fs');
const Logger = require('../../core/logger');
const ControllerInjector = require('../../utils/controller-injector');
class TelegramWebhookHandler {
constructor(config = {}) {
this.config = config;
this.logger = new Logger('TelegramWebhook');
this.sessionsDir = path.join(__dirname, '../../data/sessions');
this.injector = new ControllerInjector();
this.app = express();
this.apiBaseUrl = 'https://api.telegram.org';
this.botUsername = null; // Cache for bot username
this._setupMiddleware();
this._setupRoutes();
}
_setupMiddleware() {
// Parse JSON for all requests
this.app.use(express.json());
}
_setupRoutes() {
// Telegram webhook endpoint
this.app.post('/webhook/telegram', this._handleWebhook.bind(this));
// Health check endpoint
this.app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'telegram-webhook' });
});
}
async _handleWebhook(req, res) {
try {
const update = req.body;
// Handle different update types
if (update.message) {
await this._handleMessage(update.message);
} else if (update.callback_query) {
await this._handleCallbackQuery(update.callback_query);
}
res.status(200).send('OK');
} catch (error) {
this.logger.error('Webhook handling error:', error.message);
res.status(500).send('Internal Server Error');
}
}
async _handleMessage(message) {
const chatId = message.chat.id;
const userId = message.from.id;
const messageText = message.text?.trim();
if (!messageText) return;
// Check if user is authorized
if (!this._isAuthorized(userId, chatId)) {
this.logger.warn(`Unauthorized user/chat: ${userId}/${chatId}`);
await this._sendMessage(chatId, '⚠️ You are not authorized to use this bot.');
return;
}
// Handle /start command
if (messageText === '/start') {
await this._sendWelcomeMessage(chatId);
return;
}
// Handle /help command
if (messageText === '/help') {
await this._sendHelpMessage(chatId);
return;
}
// Parse command
const commandMatch = messageText.match(/^\/cmd\s+([A-Z0-9]{8})\s+(.+)$/i);
if (!commandMatch) {
// Check if it's a direct command without /cmd prefix
const directMatch = messageText.match(/^([A-Z0-9]{8})\s+(.+)$/);
if (directMatch) {
await this._processCommand(chatId, directMatch[1], directMatch[2]);
} else {
await this._sendMessage(chatId,
'❌ Invalid format. Use:\n`/cmd <TOKEN> <command>`\n\nExample:\n`/cmd ABC12345 analyze this code`',
{ parse_mode: 'Markdown' });
}
return;
}
const token = commandMatch[1].toUpperCase();
const command = commandMatch[2];
await this._processCommand(chatId, token, command);
}
async _processCommand(chatId, token, command) {
// Find session by token
const session = await this._findSessionByToken(token);
if (!session) {
await this._sendMessage(chatId,
'❌ Invalid or expired token. Please wait for a new task notification.',
{ parse_mode: 'Markdown' });
return;
}
// Check if session is expired
if (session.expiresAt < Math.floor(Date.now() / 1000)) {
await this._sendMessage(chatId,
'❌ Token has expired. Please wait for a new task notification.',
{ parse_mode: 'Markdown' });
await this._removeSession(session.id);
return;
}
try {
// Inject command into tmux session
const tmuxSession = session.tmuxSession || 'default';
await this.injector.injectCommand(command, tmuxSession);
// Send confirmation
await this._sendMessage(chatId,
`✅ *Command sent successfully*\n\n📝 *Command:* ${command}\n🖥️ *Session:* ${tmuxSession}\n\nClaude is now processing your request...`,
{ parse_mode: 'Markdown' });
// Log command execution
this.logger.info(`Command injected - User: ${chatId}, Token: ${token}, Command: ${command}`);
} catch (error) {
this.logger.error('Command injection failed:', error.message);
await this._sendMessage(chatId,
`❌ *Command execution failed:* ${error.message}`,
{ parse_mode: 'Markdown' });
}
}
async _handleCallbackQuery(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const data = callbackQuery.data;
// Answer callback query to remove loading state
await this._answerCallbackQuery(callbackQuery.id);
if (data.startsWith('personal:')) {
const token = data.split(':')[1];
// Send personal chat command format
await this._sendMessage(chatId,
`📝 *Personal Chat Command Format:*\n\n\`/cmd ${token} <your command>\`\n\n*Example:*\n\`/cmd ${token} please analyze this code\`\n\n💡 *Copy and paste the format above, then add your command!*`,
{ parse_mode: 'Markdown' });
} else if (data.startsWith('group:')) {
const token = data.split(':')[1];
// Send group chat command format with @bot_name
const botUsername = await this._getBotUsername();
await this._sendMessage(chatId,
`👥 *Group Chat Command Format:*\n\n\`@${botUsername} /cmd ${token} <your command>\`\n\n*Example:*\n\`@${botUsername} /cmd ${token} please analyze this code\`\n\n💡 *Copy and paste the format above, then add your command!*`,
{ parse_mode: 'Markdown' });
} else if (data.startsWith('session:')) {
const token = data.split(':')[1];
// For backward compatibility - send help message for old callback buttons
await this._sendMessage(chatId,
`📝 *How to send a command:*\n\nType:\n\`/cmd ${token} <your command>\`\n\nExample:\n\`/cmd ${token} please analyze this code\`\n\n💡 *Tip:* New notifications have a button that auto-fills the command for you!`,
{ parse_mode: 'Markdown' });
}
}
async _sendWelcomeMessage(chatId) {
const message = `🤖 *Welcome to Claude Code Remote Bot!*\n\n` +
`I'll notify you when Claude completes tasks or needs input.\n\n` +
`When you receive a notification with a token, you can send commands back using:\n` +
`\`/cmd <TOKEN> <your command>\`\n\n` +
`Type /help for more information.`;
await this._sendMessage(chatId, message, { parse_mode: 'Markdown' });
}
async _sendHelpMessage(chatId) {
const message = `📚 *Claude Code Remote Bot Help*\n\n` +
`*Commands:*\n` +
`\`/start\` - Welcome message\n` +
`\`/help\` - Show this help\n` +
`\`/cmd <TOKEN> <command>\` - Send command to Claude\n\n` +
`*Example:*\n` +
`\`/cmd ABC12345 analyze the performance of this function\`\n\n` +
`*Tips:*\n` +
`• Tokens are case-insensitive\n` +
`• Tokens expire after 24 hours\n` +
`• You can also just type \`TOKEN command\` without /cmd`;
await this._sendMessage(chatId, message, { parse_mode: 'Markdown' });
}
_isAuthorized(userId, chatId) {
// Check whitelist
const whitelist = this.config.whitelist || [];
if (whitelist.includes(String(chatId)) || whitelist.includes(String(userId))) {
return true;
}
// If no whitelist configured, allow configured chat/user
if (whitelist.length === 0) {
const configuredChatId = this.config.chatId || this.config.groupId;
if (configuredChatId && String(chatId) === String(configuredChatId)) {
return true;
}
}
return false;
}
async _getBotUsername() {
if (this.botUsername) {
return this.botUsername;
}
try {
const response = await axios.get(
`${this.apiBaseUrl}/bot${this.config.botToken}/getMe`
);
if (response.data.ok && response.data.result.username) {
this.botUsername = response.data.result.username;
return this.botUsername;
}
} catch (error) {
this.logger.error('Failed to get bot username:', error.message);
}
// Fallback to configured username or default
return this.config.botUsername || 'claude_remote_bot';
}
async _findSessionByToken(token) {
const files = fs.readdirSync(this.sessionsDir);
for (const file of files) {
if (!file.endsWith('.json')) continue;
const sessionPath = path.join(this.sessionsDir, file);
try {
const session = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
if (session.token === token) {
return session;
}
} catch (error) {
this.logger.error(`Failed to read session file ${file}:`, error.message);
}
}
return null;
}
async _removeSession(sessionId) {
const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`);
if (fs.existsSync(sessionFile)) {
fs.unlinkSync(sessionFile);
this.logger.debug(`Session removed: ${sessionId}`);
}
}
async _sendMessage(chatId, text, options = {}) {
try {
await axios.post(
`${this.apiBaseUrl}/bot${this.config.botToken}/sendMessage`,
{
chat_id: chatId,
text: text,
...options
}
);
} catch (error) {
this.logger.error('Failed to send message:', error.response?.data || error.message);
}
}
async _answerCallbackQuery(callbackQueryId, text = '') {
try {
await axios.post(
`${this.apiBaseUrl}/bot${this.config.botToken}/answerCallbackQuery`,
{
callback_query_id: callbackQueryId,
text: text
}
);
} catch (error) {
this.logger.error('Failed to answer callback query:', error.response?.data || error.message);
}
}
async setWebhook(webhookUrl) {
try {
const response = await axios.post(
`${this.apiBaseUrl}/bot${this.config.botToken}/setWebhook`,
{
url: webhookUrl,
allowed_updates: ['message', 'callback_query']
}
);
this.logger.info('Webhook set successfully:', response.data);
return response.data;
} catch (error) {
this.logger.error('Failed to set webhook:', error.response?.data || error.message);
throw error;
}
}
start(port = 3000) {
this.app.listen(port, () => {
this.logger.info(`Telegram webhook server started on port ${port}`);
});
}
}
module.exports = TelegramWebhookHandler;

View File

@ -50,8 +50,24 @@ class Notifier {
this.registerChannel('email', email); this.registerChannel('email', email);
} }
// TODO: Load other channels based on configuration // Load LINE channel
// Discord, Telegram, etc. const LINEChannel = require('../channels/line/line');
const lineConfig = this.config.getChannel('line');
if (lineConfig && lineConfig.enabled) {
const line = new LINEChannel(lineConfig.config || {});
this.registerChannel('line', line);
}
// Load Telegram channel
const TelegramChannel = require('../channels/telegram/telegram');
const telegramConfig = this.config.getChannel('telegram');
if (telegramConfig && telegramConfig.enabled) {
const telegram = new TelegramChannel(telegramConfig.config || {});
this.registerChannel('telegram', telegram);
}
// ✅ Telegram integration completed
// TODO: Future channels - Discord, Slack, Teams, etc.
this.logger.info(`Initialized ${this.channels.size} channels`); this.logger.info(`Initialized ${this.channels.size} channels`);
} }

View File

@ -0,0 +1,46 @@
[
{
"id": 1312,
"timestamp": 1754077174623
},
{
"id": 1315,
"timestamp": 1754077174623
},
{
"id": 1310,
"timestamp": 1754077174623
},
{
"id": 1323,
"timestamp": 1754077174623
},
{
"id": 1331,
"timestamp": 1754077174623
},
{
"id": 1334,
"timestamp": 1754077174623
},
{
"id": 1342,
"timestamp": 1754077174623
},
{
"id": 1346,
"timestamp": 1754077174623
},
{
"id": 1348,
"timestamp": 1754077174623
},
{
"id": 180,
"timestamp": 1754077174623
},
{
"id": 1691,
"timestamp": 1754077174623
}
]

542
src/data/session-map.json Normal file
View File

@ -0,0 +1,542 @@
{
"7HUMGXOT": {
"type": "pty",
"createdAt": 1753601264,
"expiresAt": 1753687664,
"cwd": "/Users/jessytsui/dev/TaskPing",
"sessionId": "3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec",
"tmuxSession": "hailuo",
"description": "completed - TaskPing"
},
"5CLDW6NQ": {
"type": "pty",
"createdAt": 1753602124,
"expiresAt": 1753688524,
"cwd": "/Users/jessytsui/dev/TaskPing",
"sessionId": "c7b6750f-6246-4ed3-bca5-81201ab980ee",
"tmuxSession": "hailuo",
"description": "completed - TaskPing"
},
"ONY66DAE": {
"type": "pty",
"createdAt": 1753604311,
"expiresAt": 1753690711,
"cwd": "/Users/jessytsui/dev/TaskPing",
"sessionId": "2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621",
"tmuxSession": "hailuo",
"description": "waiting - TaskPing"
},
"QHFI9FIJ": {
"type": "pty",
"createdAt": 1753607753,
"expiresAt": 1753694153,
"cwd": "/Users/jessytsui/dev/TaskPing",
"sessionId": "a966681d-5cfd-47b9-bb1b-c7ee9655b97b",
"tmuxSession": "a-0",
"description": "waiting - TaskPing"
},
"G3QE3STQ": {
"type": "pty",
"createdAt": 1753622403,
"expiresAt": 1753708803,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e",
"tmuxSession": "claude-taskping",
"description": "completed - TaskPing-Test"
},
"Z0Q98XCC": {
"type": "pty",
"createdAt": 1753624374,
"expiresAt": 1753710774,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "b2408839-8a64-4a07-8ceb-4a7a82ea5b25",
"tmuxSession": "claude-taskping",
"description": "completed - TaskPing-Test"
},
"65S5UGHZ": {
"type": "pty",
"createdAt": 1753624496,
"expiresAt": 1753710896,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "117a1097-dd97-41ab-a276-e4820adc8da8",
"tmuxSession": "claude-taskping",
"description": "completed - Claude-Code-Remote"
},
"N9PPKGTO": {
"type": "pty",
"createdAt": 1753624605,
"expiresAt": 1753711005,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "0caed3af-35ae-4b42-9081-b1a959735bde",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"TEST12345": {
"type": "pty",
"createdAt": 1753628000,
"expiresAt": 1753714400,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "test-session-id",
"tmuxSession": "test-claude",
"description": "testing - Test Session"
},
"YTGT6F6F": {
"type": "pty",
"createdAt": 1753625040,
"expiresAt": 1753711440,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "e6e973b6-20dd-497f-a988-02482af63336",
"tmuxSession": "claude-taskping",
"description": "completed - Claude-Code-Remote"
},
"2XQP1N0P": {
"type": "pty",
"createdAt": 1753625361,
"expiresAt": 1753711761,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "0f694f4c-f8a4-476a-946a-3dc057c3bc46",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"GKPSGCBS": {
"type": "pty",
"createdAt": 1753625618,
"expiresAt": 1753712018,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "e844d2ae-9098-4528-9e05-e77904a35be3",
"tmuxSession": "claude-taskping",
"description": "completed - Claude-Code-Remote"
},
"187JDGZ0": {
"type": "pty",
"createdAt": 1753625623,
"expiresAt": 1753712023,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "633a2687-81e7-456e-9995-3321ce3f3b2b",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"NSYKTAWC": {
"type": "pty",
"createdAt": 1753625650,
"expiresAt": 1753712050,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "b8dac307-8b4b-4286-aa73-324b9b659e60",
"tmuxSession": "claude-taskping",
"description": "completed - Claude-Code-Remote"
},
"1NTEJPH7": {
"type": "pty",
"createdAt": 1753625743,
"expiresAt": 1753712143,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "09466a43-c495-4a30-ac08-eb425748a28c",
"tmuxSession": "claude-taskping",
"description": "completed - Claude-Code-Remote"
},
"5XO64F9Z": {
"type": "pty",
"createdAt": 1753625846,
"expiresAt": 1753712246,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "99132172-7a97-46f7-b282-b22054d6e599",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"D8561S3A": {
"type": "pty",
"createdAt": 1753625904,
"expiresAt": 1753712304,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "772628f1-414b-4242-bc8f-660ad53b6c23",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"GR0GED2E": {
"type": "pty",
"createdAt": 1753626215,
"expiresAt": 1753712615,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "da40ba76-7047-41e0-95f2-081db87c1b3b",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"TTRQKVM9": {
"type": "pty",
"createdAt": 1753626245,
"expiresAt": 1753712645,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "c7c5c95d-4541-47f6-b27a-35c0fd563413",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"P9UBHY8L": {
"type": "pty",
"createdAt": 1753626325,
"expiresAt": 1753712725,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "a3f2b4f9-811e-4721-914f-f025919c2530",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"JQAOXCYJ": {
"type": "pty",
"createdAt": 1753626390,
"expiresAt": 1753712790,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "f0d0635b-59f2-45eb-acfc-d649b12fd2d6",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"B7R9OR3K": {
"type": "pty",
"createdAt": 1753626445,
"expiresAt": 1753712845,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "d33e49aa-a58f-46b0-8829-dfef7f474600",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"0KGM60XO": {
"type": "pty",
"createdAt": 1753626569,
"expiresAt": 1753712969,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "02bd4449-bdcf-464e-916e-61bc62a18dd2",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"5NXM173C": {
"type": "pty",
"createdAt": 1753626834,
"expiresAt": 1753713234,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "f8f915ee-ab64-471c-b3d2-71cb84d2b5fe",
"tmuxSession": "video",
"description": "completed - Claude-Code-Remote"
},
"2R8GD6VD": {
"type": "pty",
"createdAt": 1754066705,
"expiresAt": 1754153105,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "00a65f0d-b8ef-4c7f-97d6-74ace997f133",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"01BCXOI0": {
"type": "pty",
"createdAt": 1754066792,
"expiresAt": 1754153192,
"cwd": "/Users/jessytsui/dev/doc_page",
"sessionId": "47501ac6-1ae1-4584-9339-e64ebe8f4218",
"tmuxSession": "claude-test",
"description": "completed - doc_page"
},
"SLHHY01G": {
"type": "pty",
"createdAt": 1754066895,
"expiresAt": 1754153295,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "2704564c-3c4f-4174-95fb-33e476acd44a",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"9CUAHQ60": {
"type": "pty",
"createdAt": 1754066975,
"expiresAt": 1754153375,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "ba41d320-6579-4beb-bb62-0ea3cdeacfcb",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"C8ZKMS70": {
"type": "pty",
"createdAt": 1754067144,
"expiresAt": 1754153544,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "3050f159-e58f-4a3a-b744-45ae38f2e887",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"E71FAJA3": {
"type": "pty",
"createdAt": 1754067280,
"expiresAt": 1754153680,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "784bb250-9f56-4511-836f-a38f76c486ce",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"ZQIZ0SDR": {
"type": "pty",
"createdAt": 1754067317,
"expiresAt": 1754153717,
"cwd": "/Users/jessytsui/dev/doc_page",
"sessionId": "7277653b-cae9-4be9-9124-bc68a71c7152",
"tmuxSession": "claude-test",
"description": "completed - doc_page"
},
"0IEID7K0": {
"type": "pty",
"createdAt": 1754067385,
"expiresAt": 1754153785,
"cwd": "/Users/jessytsui",
"sessionId": "73e4e49d-bd47-43f8-92dc-42e530675a0a",
"tmuxSession": "claude-test",
"description": "completed - jessytsui"
},
"2MFHRVRP": {
"type": "pty",
"createdAt": 1754067582,
"expiresAt": 1754153982,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "60ed38da-6940-425e-a9fe-491840a3e0e7",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"WQUR8ZWG": {
"type": "pty",
"createdAt": 1754067778,
"expiresAt": 1754154178,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "bea5317e-5851-4d4a-9175-b79f766bc8a0",
"tmuxSession": "claude-code-remote",
"description": "completed - Claude-Code-Remote"
},
"VGMHY9GU": {
"type": "pty",
"createdAt": 1754067874,
"expiresAt": 1754154274,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "2c4a832b-17e2-4005-af2b-f1452315269e",
"tmuxSession": "claude-code",
"description": "completed - Claude-Code-Remote"
},
"EMECQ2ZG": {
"type": "pty",
"createdAt": 1754067996,
"expiresAt": 1754154396,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "e61867fb-9199-4fd4-8396-f41eac9cd9af",
"tmuxSession": "claude-code",
"description": "completed - Claude-Code-Remote"
},
"WZKOH82S": {
"type": "pty",
"createdAt": 1754068559,
"expiresAt": 1754154959,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "ef2a8e7d-c2e0-4329-9d30-998c23ad9149",
"tmuxSession": "claude-code",
"description": "completed - Claude-Code-Remote"
},
"O3ST8AI6": {
"type": "pty",
"createdAt": 1754069309,
"expiresAt": 1754155709,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "0aec093e-f4e0-45f6-8f15-b734f56e254f",
"tmuxSession": "claude-code",
"description": "completed - Claude-Code-Remote"
},
"DUMTN3RR": {
"type": "pty",
"createdAt": 1754069671,
"expiresAt": 1754156071,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "08333291-85f8-4672-9590-685e3049d028",
"tmuxSession": "claude-code-test",
"description": "completed - Claude-Code-Remote"
},
"55MH1LWK": {
"type": "pty",
"createdAt": 1754070069,
"expiresAt": 1754156469,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "719bf1a7-c6b1-4d45-ad47-86c985625232",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"55E0J99I": {
"type": "pty",
"createdAt": 1754070135,
"expiresAt": 1754156535,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "303dc5be-af53-478f-b5cd-702987eb29b4",
"tmuxSession": "claude-code",
"description": "completed - Claude-Code-Remote"
},
"GK0JP7C4": {
"type": "pty",
"createdAt": 1754070531,
"expiresAt": 1754156931,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "86e0c519-edf6-401b-9d44-1b980d9288f4",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"5NCJVV7P": {
"type": "pty",
"createdAt": 1754070572,
"expiresAt": 1754156972,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "cd0b513c-ccbc-41aa-a044-235c16083dda",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"DSV496RA": {
"type": "pty",
"createdAt": 1754070585,
"expiresAt": 1754156985,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "e87bb1ed-f195-407d-8024-ac8e216bc632",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"G4N2KB5Y": {
"type": "pty",
"createdAt": 1754070676,
"expiresAt": 1754157076,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "0c546cad-0000-4f5c-8bff-493e6dbddfe2",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"DIHER2N2": {
"type": "pty",
"createdAt": 1754070936,
"expiresAt": 1754157336,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "cd1cb22d-2f2f-4e4e-9ad4-e1a6c68965d0",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"PVTAZK0W": {
"type": "pty",
"createdAt": 1754070976,
"expiresAt": 1754157376,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "d5281021-12e4-4ca2-bc44-533649969568",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"6WRFBJOE": {
"type": "pty",
"createdAt": 1754071040,
"expiresAt": 1754157440,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "13dbb781-3000-4a73-9cf6-551abfcb2df8",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"15LBIE97": {
"type": "pty",
"createdAt": 1754071107,
"expiresAt": 1754157507,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "b1441fcc-33d5-402b-b094-c8dc4ce36302",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"WLPTYZ86": {
"type": "pty",
"createdAt": 1754071313,
"expiresAt": 1754157713,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "c5f99272-5603-44d3-8e97-8b19bc74d54e",
"tmuxSession": "a",
"description": "completed - Claude-Code-Remote"
},
"QF58O43H": {
"type": "pty",
"createdAt": 1754071345,
"expiresAt": 1754157745,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "7d3bae82-97fb-42bb-bd77-af36246f47db",
"tmuxSession": "ab",
"description": "completed - Claude-Code-Remote"
},
"U62IMVYP": {
"type": "pty",
"createdAt": 1754071829,
"expiresAt": 1754158229,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "32190311-904e-4f21-9bac-ebe458d87936",
"tmuxSession": "claude-test",
"description": "completed - Claude-Code-Remote"
},
"L4WIBBPP": {
"type": "pty",
"createdAt": 1754074724,
"expiresAt": 1754161124,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "04bec660-6454-407c-9881-f2bc714312b0",
"tmuxSession": "123",
"description": "completed - Claude-Code-Remote"
},
"UFORANBW": {
"type": "pty",
"createdAt": 1754074755,
"expiresAt": 1754161155,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "b75d4767-e045-4530-8740-70f6515d8b13",
"tmuxSession": "123",
"description": "completed - Claude-Code-Remote"
},
"R0AG2CIS": {
"type": "pty",
"createdAt": 1754074784,
"expiresAt": 1754161184,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "74f9ef13-a494-44eb-83a1-6d9eacc488fa",
"tmuxSession": "123",
"description": "completed - Claude-Code-Remote"
},
"YPBWUW83": {
"type": "pty",
"createdAt": 1754075528,
"expiresAt": 1754161928,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "c5d901bf-cb1c-4590-b197-c960c4153af2",
"tmuxSession": "claude-hook-test",
"description": "completed - Claude-Code-Remote"
},
"8KYNDD1A": {
"type": "pty",
"createdAt": 1754075553,
"expiresAt": 1754161953,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "096b69d9-040a-4e42-a2a2-7391ba2b9e20",
"tmuxSession": "claude-hook-test",
"description": "completed - Claude-Code-Remote"
},
"EK7LG2H4": {
"type": "pty",
"createdAt": 1754075748,
"expiresAt": 1754162148,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "94c65db1-5d64-4c10-96c3-925bb69d6bf0",
"tmuxSession": "claude-hook-test",
"description": "completed - Claude-Code-Remote"
},
"V75VD2QD": {
"type": "pty",
"createdAt": 1754075775,
"expiresAt": 1754162175,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "74443d59-66d9-4f37-b965-dbca9d79f111",
"tmuxSession": "claude-hook-test",
"description": "completed - Claude-Code-Remote"
},
"FJW7PHHH": {
"type": "pty",
"createdAt": 1754076112,
"expiresAt": 1754162512,
"cwd": "/Users/jessytsui/dev/Claude-Code-Remote",
"sessionId": "10cd6e52-91a8-476a-af0a-1fe2c2929ab6",
"tmuxSession": "claude-hook-test",
"description": "completed - Claude-Code-Remote"
}
}

View File

@ -0,0 +1,110 @@
/**
* Controller Injector
* Injects commands into tmux sessions or PTY
*/
const { execSync, spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const Logger = require('../core/logger');
class ControllerInjector {
constructor(config = {}) {
this.logger = new Logger('ControllerInjector');
this.mode = config.mode || process.env.INJECTION_MODE || 'pty';
this.defaultSession = config.defaultSession || process.env.TMUX_SESSION || 'claude-code';
}
async injectCommand(command, sessionName = null) {
const session = sessionName || this.defaultSession;
if (this.mode === 'tmux') {
return this._injectTmux(command, session);
} else {
return this._injectPty(command, session);
}
}
_injectTmux(command, sessionName) {
try {
// Check if tmux session exists
try {
execSync(`tmux has-session -t ${sessionName}`, { stdio: 'ignore' });
} catch (error) {
throw new Error(`Tmux session '${sessionName}' not found`);
}
// Send command to tmux session and execute it
const escapedCommand = command.replace(/'/g, "'\\''");
// Send command first
execSync(`tmux send-keys -t ${sessionName} '${escapedCommand}'`);
// Then send Enter as separate command
execSync(`tmux send-keys -t ${sessionName} Enter`);
this.logger.info(`Command injected to tmux session '${sessionName}'`);
return true;
} catch (error) {
this.logger.error('Failed to inject command via tmux:', error.message);
throw error;
}
}
_injectPty(command, sessionName) {
try {
// Find PTY session file
const sessionMapPath = process.env.SESSION_MAP_PATH ||
path.join(__dirname, '../data/session-map.json');
if (!fs.existsSync(sessionMapPath)) {
throw new Error('Session map file not found');
}
const sessionMap = JSON.parse(fs.readFileSync(sessionMapPath, 'utf8'));
const sessionInfo = sessionMap[sessionName];
if (!sessionInfo || !sessionInfo.ptyPath) {
throw new Error(`PTY session '${sessionName}' not found`);
}
// Write command to PTY
fs.writeFileSync(sessionInfo.ptyPath, command + '\n');
this.logger.info(`Command injected to PTY session '${sessionName}'`);
return true;
} catch (error) {
this.logger.error('Failed to inject command via PTY:', error.message);
throw error;
}
}
listSessions() {
if (this.mode === 'tmux') {
try {
const output = execSync('tmux list-sessions -F "#{session_name}"', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
});
return output.trim().split('\n').filter(Boolean);
} catch (error) {
return [];
}
} else {
try {
const sessionMapPath = process.env.SESSION_MAP_PATH ||
path.join(__dirname, '../data/session-map.json');
if (!fs.existsSync(sessionMapPath)) {
return [];
}
const sessionMap = JSON.parse(fs.readFileSync(sessionMapPath, 'utf8'));
return Object.keys(sessionMap);
} catch (error) {
return [];
}
}
}
}
module.exports = ControllerInjector;

View File

@ -1,16 +1,76 @@
/** /**
* Tmux Session Monitor * Tmux Monitor - Enhanced for real-time monitoring with Telegram/LINE automation
* Captures input/output from tmux sessions for email notifications * Monitors tmux session output for Claude completion patterns
* Based on the original email automation mechanism but adapted for real-time notifications
*/ */
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const EventEmitter = require('events');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const TraceCapture = require('./trace-capture'); const TraceCapture = require('./trace-capture');
class TmuxMonitor { class TmuxMonitor extends EventEmitter {
constructor() { constructor(sessionName = null) {
super();
this.sessionName = sessionName || process.env.TMUX_SESSION || 'claude-real';
this.captureDir = path.join(__dirname, '../data/tmux-captures'); this.captureDir = path.join(__dirname, '../data/tmux-captures');
this.isMonitoring = false;
this.monitorInterval = null;
this.lastPaneContent = '';
this.outputBuffer = [];
this.maxBufferSize = 1000; // Keep last 1000 lines
this.checkInterval = 2000; // Check every 2 seconds
// Claude completion patterns (adapted for Claude Code's actual output format)
this.completionPatterns = [
// Task completion indicators
/task.*completed/i,
/successfully.*completed/i,
/completed.*successfully/i,
/implementation.*complete/i,
/changes.*made/i,
/created.*successfully/i,
/updated.*successfully/i,
/file.*created/i,
/file.*updated/i,
/finished/i,
/done/i,
/✅/,
/All set/i,
/Ready/i,
// Claude Code specific patterns
/The file.*has been updated/i,
/File created successfully/i,
/Command executed successfully/i,
/Operation completed/i,
// Look for prompt return (indicating Claude finished responding)
/╰.*╯\s*$/, // Box ending
/^\s*>\s*$/ // Empty prompt ready for input
];
// Waiting patterns (when Claude needs input)
this.waitingPatterns = [
/waiting.*for/i,
/need.*input/i,
/please.*provide/i,
/what.*would you like/i,
/how.*can I help/i,
/⏳/,
/What would you like me to/i,
/Is there anything else/i,
/Any other/i,
/Do you want/i,
/Would you like/i,
// Claude Code specific waiting patterns
/\? for shortcuts/i, // Claude Code waiting indicator
/╭.*─.*╮/, // Start of response box
/>\s*$/ // Empty prompt
];
this._ensureCaptureDir(); this._ensureCaptureDir();
this.traceCapture = new TraceCapture(); this.traceCapture = new TraceCapture();
} }
@ -21,6 +81,290 @@ class TmuxMonitor {
} }
} }
// Real-time monitoring methods (new functionality)
start() {
if (this.isMonitoring) {
console.log('⚠️ TmuxMonitor already running');
return;
}
// Verify tmux session exists
if (!this._sessionExists()) {
console.error(`❌ Tmux session '${this.sessionName}' not found`);
throw new Error(`Tmux session '${this.sessionName}' not found`);
}
this.isMonitoring = true;
this._startRealTimeMonitoring();
console.log(`🔍 Started monitoring tmux session: ${this.sessionName}`);
}
stop() {
if (!this.isMonitoring) {
return;
}
this.isMonitoring = false;
if (this.monitorInterval) {
clearInterval(this.monitorInterval);
this.monitorInterval = null;
}
console.log('⏹️ TmuxMonitor stopped');
}
_sessionExists() {
try {
const sessions = execSync('tmux list-sessions -F "#{session_name}"', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim().split('\n');
return sessions.includes(this.sessionName);
} catch (error) {
return false;
}
}
_startRealTimeMonitoring() {
// Initial capture
this._captureCurrentContent();
// Set up periodic monitoring
this.monitorInterval = setInterval(() => {
if (this.isMonitoring) {
this._checkForChanges();
}
}, this.checkInterval);
}
_captureCurrentContent() {
try {
// Capture current pane content
const content = execSync(`tmux capture-pane -t ${this.sessionName} -p`, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
});
return content;
} catch (error) {
console.error('Error capturing tmux content:', error.message);
return '';
}
}
_checkForChanges() {
const currentContent = this._captureCurrentContent();
if (currentContent !== this.lastPaneContent) {
// Get new content (lines that were added)
const newLines = this._getNewLines(this.lastPaneContent, currentContent);
if (newLines.length > 0) {
// Add to buffer
this.outputBuffer.push(...newLines);
// Trim buffer if too large
if (this.outputBuffer.length > this.maxBufferSize) {
this.outputBuffer = this.outputBuffer.slice(-this.maxBufferSize);
}
// Check for completion patterns
this._analyzeNewContent(newLines);
}
this.lastPaneContent = currentContent;
}
}
_getNewLines(oldContent, newContent) {
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
// Find lines that were added
const addedLines = [];
// Simple approach: compare line by line from the end
const oldLength = oldLines.length;
const newLength = newLines.length;
if (newLength > oldLength) {
// New lines were added
const numNewLines = newLength - oldLength;
addedLines.push(...newLines.slice(-numNewLines));
} else if (newLength === oldLength) {
// Same number of lines, check if last lines changed
for (let i = Math.max(0, newLength - 5); i < newLength; i++) {
if (i < oldLength && newLines[i] !== oldLines[i]) {
addedLines.push(newLines[i]);
}
}
}
return addedLines.filter(line => line.trim().length > 0);
}
_analyzeNewContent(newLines) {
const recentText = newLines.join('\n');
// Also check the entire recent buffer for context
const bufferText = this.outputBuffer.slice(-20).join('\n');
console.log('🔍 Analyzing new content:', newLines.slice(0, 2).map(line => line.substring(0, 50))); // Debug log
// Look for Claude response completion patterns
const hasResponseEnd = this._detectResponseCompletion(recentText, bufferText);
const hasTaskCompletion = this._detectTaskCompletion(recentText, bufferText);
if (hasTaskCompletion || hasResponseEnd) {
console.log('🎯 Task completion detected');
this._handleTaskCompletion(newLines);
}
// Don't constantly trigger waiting notifications for static content
else if (this._shouldTriggerWaitingNotification(recentText)) {
console.log('⏳ New waiting state detected');
this._handleWaitingForInput(newLines);
}
}
_detectResponseCompletion(recentText, bufferText) {
// Look for Claude response completion indicators
const completionIndicators = [
/The file.*has been updated/i,
/File created successfully/i,
/successfully/i,
/completed/i,
/✅/,
/done/i
];
// Claude Code specific pattern: ⏺ response followed by box
const hasClaudeResponse = /⏺.*/.test(bufferText) || /⏺.*/.test(recentText);
const hasBoxStart = /╭.*╮/.test(recentText);
const hasBoxEnd = /╰.*╯/.test(recentText);
// Look for the pattern: ⏺ response -> box -> empty prompt
const isCompleteResponse = hasClaudeResponse && (hasBoxStart || hasBoxEnd);
return completionIndicators.some(pattern => pattern.test(recentText)) ||
isCompleteResponse;
}
_detectTaskCompletion(recentText, bufferText) {
// Look for specific completion patterns
return this.completionPatterns.some(pattern => pattern.test(recentText));
}
_shouldTriggerWaitingNotification(recentText) {
// Only trigger waiting notification for new meaningful content
// Avoid triggering on static "? for shortcuts" that doesn't change
const meaningfulWaitingPatterns = [
/waiting.*for/i,
/need.*input/i,
/please.*provide/i,
/what.*would you like/i,
/Do you want/i,
/Would you like/i
];
return meaningfulWaitingPatterns.some(pattern => pattern.test(recentText)) &&
!recentText.includes('? for shortcuts'); // Ignore static shortcuts line
}
_handleTaskCompletion(newLines) {
const conversation = this._extractRecentConversation();
console.log('🎉 Claude task completion detected!');
this.emit('taskCompleted', {
type: 'completed',
sessionName: this.sessionName,
timestamp: new Date().toISOString(),
newOutput: newLines,
conversation: conversation,
triggerText: newLines.join('\n')
});
}
_handleWaitingForInput(newLines) {
const conversation = this._extractRecentConversation();
console.log('⏳ Claude waiting for input detected!');
this.emit('waitingForInput', {
type: 'waiting',
sessionName: this.sessionName,
timestamp: new Date().toISOString(),
newOutput: newLines,
conversation: conversation,
triggerText: newLines.join('\n')
});
}
_extractRecentConversation() {
// Extract recent conversation from buffer
const recentBuffer = this.outputBuffer.slice(-50); // Last 50 lines
const text = recentBuffer.join('\n');
// Try to identify user question and Claude response using Claude Code patterns
let userQuestion = '';
let claudeResponse = '';
// Look for Claude Code specific patterns
const lines = recentBuffer;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Look for user input (after > prompt)
if (line.startsWith('> ') && line.length > 2) {
userQuestion = line.substring(2).trim();
continue;
}
// Look for Claude response (⏺ prefix)
if (line.startsWith('⏺ ') && line.length > 2) {
claudeResponse = line.substring(2).trim();
break;
}
}
// If we didn't find the specific format, use fallback
if (!userQuestion || !claudeResponse) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip system lines
if (!line || line.startsWith('[') || line.startsWith('$') ||
line.startsWith('#') || line.includes('? for shortcuts') ||
line.match(/^[╭╰│─]+$/)) {
continue;
}
if (!userQuestion && line.length > 2) {
userQuestion = line;
} else if (userQuestion && !claudeResponse && line.length > 5 && line !== userQuestion) {
claudeResponse = line;
break;
}
}
}
return {
userQuestion: userQuestion || 'Recent command',
claudeResponse: claudeResponse || 'Task completed',
fullContext: text
};
}
// Manual trigger methods for testing
triggerCompletionTest() {
this._handleTaskCompletion(['Test completion notification']);
}
triggerWaitingTest() {
this._handleWaitingForInput(['Test waiting notification']);
}
// Original capture methods (legacy support)
/** /**
* Start capturing a tmux session * Start capturing a tmux session
* @param {string} sessionName - The tmux session name * @param {string} sessionName - The tmux session name
@ -380,6 +724,22 @@ class TmuxMonitor {
console.error('Failed to cleanup captures:', error.message); console.error('Failed to cleanup captures:', error.message);
} }
} }
// Enhanced status method
getStatus() {
return {
isMonitoring: this.isMonitoring,
sessionName: this.sessionName,
sessionExists: this._sessionExists(),
bufferSize: this.outputBuffer.length,
checkInterval: this.checkInterval,
patterns: {
completion: this.completionPatterns.length,
waiting: this.waitingPatterns.length
},
lastCheck: new Date().toISOString()
};
}
} }
module.exports = TmuxMonitor; module.exports = TmuxMonitor;

113
start-all-webhooks.js Executable file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env node
/**
* Multi-Platform Webhook Server
* Starts all enabled webhook servers (Telegram, LINE) in parallel
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
console.log('🚀 Starting Claude Code Remote Multi-Platform Webhook Server...\n');
const processes = [];
// Start Telegram webhook if enabled
if (process.env.TELEGRAM_ENABLED === 'true' && process.env.TELEGRAM_BOT_TOKEN) {
console.log('📱 Starting Telegram webhook server...');
const telegramProcess = spawn('node', ['start-telegram-webhook.js'], {
stdio: ['inherit', 'inherit', 'inherit'],
env: process.env
});
telegramProcess.on('exit', (code) => {
console.log(`📱 Telegram webhook server exited with code ${code}`);
});
processes.push({ name: 'Telegram', process: telegramProcess });
}
// Start LINE webhook if enabled
if (process.env.LINE_ENABLED === 'true' && process.env.LINE_CHANNEL_ACCESS_TOKEN) {
console.log('📱 Starting LINE webhook server...');
const lineProcess = spawn('node', ['start-line-webhook.js'], {
stdio: ['inherit', 'inherit', 'inherit'],
env: process.env
});
lineProcess.on('exit', (code) => {
console.log(`📱 LINE webhook server exited with code ${code}`);
});
processes.push({ name: 'LINE', process: lineProcess });
}
// Start Email daemon if enabled
if (process.env.EMAIL_ENABLED === 'true' && process.env.SMTP_USER) {
console.log('📧 Starting email daemon...');
const emailProcess = spawn('node', ['claude-remote.js', 'daemon', 'start'], {
stdio: ['inherit', 'inherit', 'inherit'],
env: process.env
});
emailProcess.on('exit', (code) => {
console.log(`📧 Email daemon exited with code ${code}`);
});
processes.push({ name: 'Email', process: emailProcess });
}
if (processes.length === 0) {
console.log('❌ No platforms enabled. Please configure at least one platform in .env file:');
console.log(' - Set TELEGRAM_ENABLED=true and configure TELEGRAM_BOT_TOKEN');
console.log(' - Set LINE_ENABLED=true and configure LINE_CHANNEL_ACCESS_TOKEN');
console.log(' - Set EMAIL_ENABLED=true and configure SMTP_USER');
process.exit(1);
}
console.log(`\n✅ Started ${processes.length} webhook server(s):`);
processes.forEach(p => {
console.log(` - ${p.name}`);
});
console.log('\n📋 Platform Command Formats:');
if (process.env.TELEGRAM_ENABLED === 'true') {
console.log(' Telegram: /cmd TOKEN123 <command>');
}
if (process.env.LINE_ENABLED === 'true') {
console.log(' LINE: Token TOKEN123 <command>');
}
if (process.env.EMAIL_ENABLED === 'true') {
console.log(' Email: Reply to notification emails');
}
console.log('\n🔧 To stop all services, press Ctrl+C\n');
// Handle graceful shutdown
function shutdown() {
console.log('\n🛑 Shutting down all webhook servers...');
processes.forEach(p => {
console.log(` Stopping ${p.name}...`);
p.process.kill('SIGTERM');
});
setTimeout(() => {
console.log('✅ All services stopped');
process.exit(0);
}, 2000);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Keep the main process alive
process.stdin.resume();

64
start-line-webhook.js Executable file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* LINE Webhook Server
* Starts the LINE webhook server for receiving messages
*/
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
const Logger = require('./src/core/logger');
const LINEWebhookHandler = require('./src/channels/line/webhook');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
const logger = new Logger('LINE-Webhook-Server');
// Load configuration
const config = {
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.LINE_CHANNEL_SECRET,
userId: process.env.LINE_USER_ID,
groupId: process.env.LINE_GROUP_ID,
whitelist: process.env.LINE_WHITELIST ? process.env.LINE_WHITELIST.split(',').map(id => id.trim()) : [],
port: process.env.LINE_WEBHOOK_PORT || 3000
};
// Validate configuration
if (!config.channelAccessToken || !config.channelSecret) {
logger.error('LINE_CHANNEL_ACCESS_TOKEN and LINE_CHANNEL_SECRET must be set in .env file');
process.exit(1);
}
if (!config.userId && !config.groupId) {
logger.error('Either LINE_USER_ID or LINE_GROUP_ID must be set in .env file');
process.exit(1);
}
// Create and start webhook handler
const webhookHandler = new LINEWebhookHandler(config);
logger.info('Starting LINE webhook server...');
logger.info(`Configuration:`);
logger.info(`- Port: ${config.port}`);
logger.info(`- User ID: ${config.userId || 'Not set'}`);
logger.info(`- Group ID: ${config.groupId || 'Not set'}`);
logger.info(`- Whitelist: ${config.whitelist.length > 0 ? config.whitelist.join(', ') : 'None (using configured IDs)'}`);
webhookHandler.start(config.port);
// Handle graceful shutdown
process.on('SIGINT', () => {
logger.info('Shutting down LINE webhook server...');
process.exit(0);
});
process.on('SIGTERM', () => {
logger.info('Shutting down LINE webhook server...');
process.exit(0);
});

85
start-telegram-webhook.js Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Telegram Webhook Server
* Starts the Telegram webhook server for receiving messages
*/
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
const Logger = require('./src/core/logger');
const TelegramWebhookHandler = require('./src/channels/telegram/webhook');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
const logger = new Logger('Telegram-Webhook-Server');
// Load configuration
const config = {
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID,
groupId: process.env.TELEGRAM_GROUP_ID,
whitelist: process.env.TELEGRAM_WHITELIST ? process.env.TELEGRAM_WHITELIST.split(',').map(id => id.trim()) : [],
port: process.env.TELEGRAM_WEBHOOK_PORT || 3001,
webhookUrl: process.env.TELEGRAM_WEBHOOK_URL
};
// Validate configuration
if (!config.botToken) {
logger.error('TELEGRAM_BOT_TOKEN must be set in .env file');
process.exit(1);
}
if (!config.chatId && !config.groupId) {
logger.error('Either TELEGRAM_CHAT_ID or TELEGRAM_GROUP_ID must be set in .env file');
process.exit(1);
}
// Create and start webhook handler
const webhookHandler = new TelegramWebhookHandler(config);
async function start() {
logger.info('Starting Telegram webhook server...');
logger.info(`Configuration:`);
logger.info(`- Port: ${config.port}`);
logger.info(`- Chat ID: ${config.chatId || 'Not set'}`);
logger.info(`- Group ID: ${config.groupId || 'Not set'}`);
logger.info(`- Whitelist: ${config.whitelist.length > 0 ? config.whitelist.join(', ') : 'None (using configured IDs)'}`);
// Set webhook if URL is provided
if (config.webhookUrl) {
try {
const webhookEndpoint = `${config.webhookUrl}/webhook/telegram`;
logger.info(`Setting webhook to: ${webhookEndpoint}`);
await webhookHandler.setWebhook(webhookEndpoint);
} catch (error) {
logger.error('Failed to set webhook:', error.message);
logger.info('You can manually set the webhook using:');
logger.info(`curl -X POST https://api.telegram.org/bot${config.botToken}/setWebhook -d "url=${config.webhookUrl}/webhook/telegram"`);
}
} else {
logger.warn('TELEGRAM_WEBHOOK_URL not set. Please set the webhook manually.');
logger.info('To set webhook manually, use:');
logger.info(`curl -X POST https://api.telegram.org/bot${config.botToken}/setWebhook -d "url=https://your-domain.com/webhook/telegram"`);
}
webhookHandler.start(config.port);
}
start();
// Handle graceful shutdown
process.on('SIGINT', () => {
logger.info('Shutting down Telegram webhook server...');
process.exit(0);
});
process.on('SIGTERM', () => {
logger.info('Shutting down Telegram webhook server...');
process.exit(0);
});

View File

@ -1,6 +0,0 @@
#!/usr/bin/env node
const ConfigManager = require('./src/config-manager');
const manager = new ConfigManager();
manager.interactiveMenu().catch(console.error);

120
test-complete-flow.sh Executable file
View File

@ -0,0 +1,120 @@
#!/bin/bash
# 完整的端到端测试脚本
# Complete end-to-end test script
set -e
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$PROJECT_DIR"
echo "🧪 Claude Code Remote - 完整端到端测试"
echo "======================================"
# 1. 检查服务状态
echo "📋 1. 检查服务状态"
echo -n " ngrok服务: "
if pgrep -f "ngrok http" > /dev/null; then
echo "✅ 运行中"
NGROK_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url' 2>/dev/null || echo "获取失败")
echo " ngrok URL: $NGROK_URL"
else
echo "❌ 未运行"
fi
echo -n " Telegram webhook: "
if pgrep -f "start-telegram-webhook" > /dev/null; then
echo "✅ 运行中"
else
echo "❌ 未运行"
fi
# 2. 检查配置文件
echo ""
echo "📋 2. 检查配置文件"
echo -n " ~/.claude/settings.json: "
if [ -f ~/.claude/settings.json ]; then
echo "✅ 存在"
echo " Hooks配置:"
cat ~/.claude/settings.json | jq '.hooks' 2>/dev/null || echo " 解析失败"
else
echo "❌ 不存在"
fi
echo -n " .env文件: "
if [ -f .env ]; then
echo "✅ 存在"
echo " Telegram配置:"
grep "TELEGRAM_" .env | grep -v "BOT_TOKEN" | while read line; do
echo " $line"
done
else
echo "❌ 不存在"
fi
# 3. 测试hook脚本
echo ""
echo "📋 3. 测试hook脚本执行"
echo " 运行: node claude-hook-notify.js completed"
node claude-hook-notify.js completed
# 4. 检查最新session
echo ""
echo "📋 4. 检查最新创建的session"
if [ -d "src/data/sessions" ]; then
LATEST_SESSION=$(ls -t src/data/sessions/*.json 2>/dev/null | head -1)
if [ -n "$LATEST_SESSION" ]; then
echo " 最新session: $(basename "$LATEST_SESSION")"
echo " 内容摘要:"
cat "$LATEST_SESSION" | jq -r '"\tToken: \(.token)\n\tType: \(.type)\n\tCreated: \(.created)\n\tTmux Session: \(.tmuxSession)"' 2>/dev/null || echo " 解析失败"
else
echo " ❌ 未找到session文件"
fi
else
echo " ❌ sessions目录不存在"
fi
# 5. 测试Telegram Bot连接
echo ""
echo "📋 5. 测试Telegram Bot连接"
if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
echo " 发送测试消息到个人聊天..."
RESPONSE=$(curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": $TELEGRAM_CHAT_ID, \"text\": \"🧪 端到端测试完成\\n\\n时间: $(date)\\n\\n如果你看到这条消息说明基础通信正常。\\n\\n下一步在Claude中完成一个任务看是否能收到自动通知。\"}")
if echo "$RESPONSE" | grep -q '"ok":true'; then
echo " ✅ 测试消息发送成功"
else
echo " ❌ 测试消息发送失败"
echo " 响应: $RESPONSE"
fi
else
echo " ⚠️ Telegram配置不完整"
fi
# 6. 检查tmux sessions
echo ""
echo "📋 6. 检查tmux sessions"
if command -v tmux >/dev/null 2>&1; then
echo " 当前tmux sessions:"
tmux list-sessions 2>/dev/null || echo " 无活跃session"
else
echo " ❌ tmux未安装"
fi
echo ""
echo "🏁 测试完成"
echo ""
echo "💡 下一步调试建议:"
echo "1. 确认你收到了上面的Telegram测试消息"
echo "2. 在tmux中运行Claude完成一个简单任务"
echo "3. 检查是否收到自动通知"
echo "4. 如果没有收到检查Claude输出是否有错误信息"
echo ""
echo "🔧 如果仍有问题,请运行:"
echo " tmux new-session -s claude-debug"
echo " # 在新session中:"
echo " export CLAUDE_HOOKS_CONFIG=$PROJECT_DIR/claude-hooks.json"
echo " claude"
echo " # 然后尝试一个简单任务"

34
test-injection.js Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env node
const ControllerInjector = require('./src/utils/controller-injector');
async function testInjection() {
console.log('🧪 测试命令注入功能');
console.log('===================');
const injector = new ControllerInjector();
console.log(`当前模式: ${injector.mode}`);
console.log(`默认session: ${injector.defaultSession}`);
// 测试列出sessions
console.log('\n📋 可用的sessions:');
const sessions = injector.listSessions();
sessions.forEach((session, index) => {
console.log(` ${index + 1}. ${session}`);
});
// 测试注入命令到claude-hook-test session
console.log('\n🔧 测试注入命令到 claude-hook-test session...');
const testCommand = 'echo "Command injection test successful at $(date)"';
try {
await injector.injectCommand(testCommand, 'claude-hook-test');
console.log('✅ 命令注入成功!');
console.log(`注入的命令: ${testCommand}`);
} catch (error) {
console.log('❌ 命令注入失败:', error.message);
}
}
testInjection().catch(console.error); < /dev/null

66
test-real-notification.js Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* Test Real Notification
* Creates a notification with real tmux session name
*/
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
const TelegramChannel = require('./src/channels/telegram/telegram');
async function testRealNotification() {
console.log('🧪 Creating REAL notification with real tmux session...\n');
// Configure Telegram channel
const config = {
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID
};
const telegramChannel = new TelegramChannel(config);
// Get real tmux session name from env
const realSession = process.env.TMUX_SESSION || 'claude-real';
// Create REAL notification
const notification = {
type: 'completed',
title: 'Claude Task Completed',
message: 'Real notification - Ready for command injection',
project: 'claude-code-line',
metadata: {
userQuestion: '準備進行真實測試',
claudeResponse: '已準備好接收新指令並注入到真實 Claude 會話中',
tmuxSession: realSession // 使用真實會話名稱
}
};
try {
console.log(`📱 Sending REAL notification for session: ${realSession}`);
const result = await telegramChannel.send(notification);
if (result) {
console.log('✅ REAL notification sent successfully!');
console.log(`🖥️ Commands will be injected into tmux session: ${realSession}`);
console.log('\n📋 Now you can reply with:');
console.log(' /cmd [NEW_TOKEN] <your command>');
console.log('\n🎯 Example:');
console.log(' /cmd [NEW_TOKEN] ls -la');
} else {
console.log('❌ Failed to send notification');
}
} catch (error) {
console.error('❌ Error:', error.message);
}
}
testRealNotification();

62
test-telegram-notification.js Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Test Telegram Notification
* Simulates Claude sending a notification via Telegram
*/
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
const TelegramChannel = require('./src/channels/telegram/telegram');
async function testNotification() {
console.log('🧪 Testing Telegram notification...\n');
// Configure Telegram channel
const config = {
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID
};
const telegramChannel = new TelegramChannel(config);
// Create test notification
const notification = {
type: 'completed',
title: 'Claude Task Completed',
message: 'Test notification from Claude Code Remote',
project: 'claude-code-line',
metadata: {
userQuestion: '請幫我查詢這個代碼庫https://github.com/JessyTsui/Claude-Code-Remote',
claudeResponse: '我已經查詢了這個代碼庫,這是一個 Claude Code Remote 項目,允許通過電子郵件遠程控制 Claude Code。',
tmuxSession: 'claude-test'
}
};
try {
console.log('📱 Sending test notification...');
const result = await telegramChannel.send(notification);
if (result) {
console.log('✅ Test notification sent successfully!');
console.log('📋 Now you can reply with a command in this format:');
console.log(' /cmd TOKEN123 <your new command>');
console.log('\n🎯 Example:');
console.log(' /cmd [TOKEN_FROM_MESSAGE] 請幫我分析這個專案的架構');
} else {
console.log('❌ Failed to send test notification');
}
} catch (error) {
console.error('❌ Error:', error.message);
}
}
testNotification();

122
test-telegram-setup.sh Executable file
View File

@ -0,0 +1,122 @@
#!/bin/bash
# Claude Code Remote - Telegram Setup Test Script
# This script tests all components of the Telegram setup
echo "🧪 Claude Code Remote - Telegram Setup Test"
echo "==========================================="
# Load environment variables
if [ -f ".env" ]; then
echo "✅ .env file found"
source .env
else
echo "❌ .env file not found"
exit 1
fi
# Check required environment variables
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
echo "❌ TELEGRAM_BOT_TOKEN not set in .env"
exit 1
else
echo "✅ TELEGRAM_BOT_TOKEN found"
fi
if [ -z "$TELEGRAM_CHAT_ID" ]; then
echo "❌ TELEGRAM_CHAT_ID not set in .env"
exit 1
else
echo "✅ TELEGRAM_CHAT_ID found"
fi
if [ -z "$TELEGRAM_WEBHOOK_URL" ]; then
echo "❌ TELEGRAM_WEBHOOK_URL not set in .env"
exit 1
else
echo "✅ TELEGRAM_WEBHOOK_URL found: $TELEGRAM_WEBHOOK_URL"
fi
# Test Telegram bot connection
echo ""
echo "🔧 Testing Telegram bot connection..."
response=$(curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": $TELEGRAM_CHAT_ID, \"text\": \"🧪 Setup test from Claude Code Remote\"}")
if echo "$response" | grep -q '"ok":true'; then
echo "✅ Telegram bot connection successful"
else
echo "❌ Telegram bot connection failed"
echo "Response: $response"
exit 1
fi
# Check webhook status
echo ""
echo "🔧 Checking webhook status..."
webhook_response=$(curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo")
if echo "$webhook_response" | grep -q "$TELEGRAM_WEBHOOK_URL"; then
echo "✅ Webhook is correctly set"
else
echo "⚠️ Webhook not set to expected URL"
echo "Expected: $TELEGRAM_WEBHOOK_URL"
echo "Response: $webhook_response"
fi
# Check if claude-hooks.json exists
if [ -f "claude-hooks.json" ]; then
echo "✅ claude-hooks.json found"
else
echo "❌ claude-hooks.json not found"
exit 1
fi
# Test notification script
echo ""
echo "🔧 Testing notification script..."
node claude-hook-notify.js completed
# Check if required processes are running
echo ""
echo "🔧 Checking running processes..."
if pgrep -f "ngrok" > /dev/null; then
echo "✅ ngrok is running"
else
echo "⚠️ ngrok not found - make sure to run: ngrok http 3001"
fi
if pgrep -f "start-telegram-webhook" > /dev/null; then
echo "✅ Telegram webhook service is running"
else
echo "⚠️ Telegram webhook service not running - run: node start-telegram-webhook.js"
fi
# Check tmux sessions
echo ""
echo "🔧 Checking tmux sessions..."
if command -v tmux >/dev/null 2>&1; then
if tmux list-sessions 2>/dev/null | grep -q "claude-code"; then
echo "✅ claude-code tmux session found"
else
echo "⚠️ claude-code tmux session not found - create with: tmux new-session -s claude-code"
fi
else
echo "⚠️ tmux not installed"
fi
echo ""
echo "📋 Setup Test Summary:"
echo "====================="
echo "If all items above show ✅, your setup is ready!"
echo ""
echo "Next steps:"
echo "1. Make sure ngrok is running: ngrok http 3001"
echo "2. Make sure webhook service is running: node start-telegram-webhook.js"
echo "3. Start Claude in tmux with hooks:"
echo " tmux attach -t claude-code"
echo " export CLAUDE_HOOKS_CONFIG=$(pwd)/claude-hooks.json"
echo " claude"
echo ""
echo "Test by running a task in Claude - you should get a Telegram notification!"