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
# ===== SMTP 发送邮件配置 =====
# ===== 選擇通知方式Email、LINE 或 Telegram =====
# 可以同時啟用多個通知方式
EMAIL_ENABLED=false
LINE_ENABLED=false
TELEGRAM_ENABLED=true
# ===== Email 配置 (如果使用 Email) =====
# SMTP 发送邮件配置
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_SECURE=true
@ -12,20 +19,56 @@ SMTP_PASS=your-app-password
EMAIL_FROM=your-email@gmail.com
EMAIL_FROM_NAME=Claude Code Remote 通知系统
# ===== IMAP 接收邮件配置 =====
# IMAP 接收邮件配置
IMAP_HOST=imap.gmail.com
IMAP_PORT=993
IMAP_SECURE=true
IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password
# ===== 邮件路由配置 =====
# 邮件路由配置
# 接收通知的邮箱地址
EMAIL_TO=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

3
.gitignore vendored
View File

@ -48,10 +48,9 @@ build/
# Temporary files
tmp/
temp/
# Data files (session-specific, should not be committed)
src/data/
!src/data/.gitkeep
# Claude configuration (user-specific)
CLAUDE.md
CLAUDE.md

388
README.md
View File

@ -1,6 +1,12 @@
# 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">
@ -18,18 +24,34 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
## ✨ Features
- **📧 Email Notifications**: Get notified when Claude completes tasks ![](./assets/email_demo.png)
- **🔄 Email Control**: Reply to emails to send new commands to Claude
- **📱 Remote Access**: Control Claude from anywhere with just email
- **🔒 Secure**: Whitelist-based sender verification
- **📧 Multiple Messaging Platforms**:
- Email notifications with full execution trace and reply-to-send commands
- Telegram Bot with interactive buttons and slash commands ✅ **NEW**
- 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
- **⚡ 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
### 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**: 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
- **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))
@ -37,28 +59,33 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
## 📋 TODO List
### Notification Channels
- [ ] **Discord & Telegram**: Bot integration for messaging platforms
- [ ] **Slack Workflow**: Native Slack app with slash commands
- ~~**📱 Telegram Integration**~~**COMPLETED** - Bot integration with interactive buttons and real-time commands
- **💬 Discord Integration** - Bot integration for messaging platforms
- **⚡ Slack Workflow** - Native Slack app with slash commands
### Developer Tools
- [ ] **AI Tools**: Support for Gemini CLI, Cursor, and other AI tools
- [ ] **Git Automation**: Auto-commit, PR creation, branch management
- **🤖 AI Tools Support** - Integration with Gemini CLI, Cursor, and other AI development tools
- **🔀 Git Automation** - Auto-commit functionality, PR creation, branch management
### Usage Analytics
- [ ] **Cost Tracking**: Token usage and estimated costs
- [ ] **Performance Metrics**: Execution time and resource usage
- [ ] **Scheduled Reports**: Daily/weekly usage summaries via email
- **💰 Cost Tracking** - Token usage monitoring and estimated costs
- **⚡ Performance Metrics** - Execution time tracking and resource usage analysis
- **📧 Scheduled Reports** - Daily/weekly usage summaries delivered via email
### Native Apps
- [ ] **Mobile Apps**: iOS and Android applications
- [ ] **Desktop Apps**: macOS and Windows native clients
- **📱 Mobile Apps** - iOS and Android applications for remote Claude control
- **🖥️ 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
git clone https://github.com/JessyTsui/Claude-Code-Remote.git
@ -66,62 +93,78 @@ cd Claude-Code-Remote
npm install
```
### Step 2: Configure Email Settings
### 3. Choose Your Platform
#### Option A: Configure Email (Recommended for Beginners)
```bash
# Copy the example configuration
# Copy example config
cp .env.example .env
# Open .env in your editor
nano .env # or use vim, code, etc.
# Edit with your email credentials
nano .env
```
Edit the `.env` file with your email credentials:
**Required email settings:**
```env
# Email account for sending notifications
EMAIL_ENABLED=true
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password # Gmail: use App Password, not regular password
# Email account for receiving replies (can be same as SMTP)
SMTP_PASS=your-app-password
IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password
# Where to send notifications
EMAIL_TO=your-notification-email@gmail.com
# Who can send commands (security whitelist)
ALLOWED_SENDERS=your-notification-email@gmail.com
# Path to session data (use absolute path)
SESSION_MAP_PATH=/your/absolute/path/to/Claude-Code-Remote/src/data/session-map.json
SESSION_MAP_PATH=/your/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.
> Note: You may need to enable two-step verification in your google account first before create app password.
📌 **Gmail users**: Use [App Passwords](https://myaccount.google.com/security), not your regular password.
### Step 3: Set Up Claude Code Hooks
Open Claude's settings file:
#### Option B: Configure Telegram ✅ **NEW**
**Quick Setup:**
```bash
# Create the directory if it doesn't exist
mkdir -p ~/.claude
# Edit settings.json
nano ~/.claude/settings.json
chmod +x setup-telegram.sh
./setup-telegram.sh
```
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": {
"Stop": [{
"matcher": "*",
"hooks": [{
"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
}]
}],
@ -129,7 +172,7 @@ Add this configuration (replace `/your/absolute/path/` with your actual path):
"matcher": "*",
"hooks": [{
"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
}]
}]
@ -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.
### Step 4: Test Your Setup
### 5. Start Services
#### For All Platforms (Recommended)
```bash
# Test email configuration
node claude-remote.js test
# Automatically starts all enabled platforms
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
**Terminal 1 - Start email monitoring:**
**For Email:**
```bash
npm run relay:pty
npm run daemon:start
# or
node claude-remote.js daemon start
```
Keep this running. You should see:
```
🚀 Claude Code Remote is running!
📧 Monitoring emails...
```
**Terminal 2 - Start Claude in tmux:**
**For Telegram:**
```bash
# Create a new tmux session
tmux new-session -s my-project
# Inside tmux, start Claude
claude
npm run telegram
# or
node start-telegram-webhook.js
```
### Step 6: You're Ready!
1. Use Claude normally in the tmux session
2. When Claude completes a task, you'll receive an email
3. Reply to the email with new commands
4. Your commands will execute automatically in Claude
### Verify Everything Works
In Claude, type:
```
What is 2+2?
**For LINE:**
```bash
npm run line
# or
node start-line-webhook.js
```
Wait for Claude to respond, then check your email. You should receive a notification!
### 6. Test Your Setup
## 📖 How to Use
### Email Notifications
When Claude completes a task, you'll receive an email notification:
```
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.
**Quick Test:**
```bash
# Test all notification channels
node claude-hook-notify.js completed
# Should receive notifications via all enabled platforms
```
### 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
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!
## 🎮 How It Works
### 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.
Then create a comprehensive test suite.
Finally, update the documentation.
Simply reply to notification email with your command
No special formatting required
```
**Complex Instructions**
**Telegram:** ✅ **NEW**
```
Refactor the authentication module with these requirements:
- Use JWT tokens instead of sessions
- Add rate limiting
- Implement refresh token logic
- Update all related tests
Click smart button to get format:
📝 Personal Chat: /cmd TOKEN123 your command here
👥 Group Chat: @bot_name /cmd TOKEN123 your command here
```
### Email Reply Workflow
1. **Receive Notification** → You get an email when Claude completes a task
2. **Reply with Command** → Send your next instruction via email reply
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
**LINE:**
```
Reply to notification with: Your command here
(Token automatically extracted from conversation context)
```
### 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.
## 💡 Common Use Cases
## 💡 Use Cases
- **Remote Development**: Start coding at the office, continue from home via email
- **Long Tasks**: Let Claude work while you're in meetings, check results via email
- **Team Collaboration**: Share Claude sessions by forwarding notification emails
- **Remote Code Reviews**: Start reviews at office, continue from home via any platform
- **Long-running Tasks**: Monitor progress and guide next steps remotely
- **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
# Test email setup
node claude-remote.js test
# Test all notification channels
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 test
```
# View tmux sessions
tmux list-sessions
tmux attach -t my-project
### Service Management
```bash
# Start all enabled platforms
npm run webhooks
# Stop email monitoring
# Press Ctrl+C in the terminal running npm run relay:pty
# Individual services
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
### 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?**
- Run `node claude-remote.js test` to test email setup
- Check spam folder
- Verify SMTP settings in `.env`
- For Gmail: ensure you're using App Password
**Commands not executing?**
- Ensure tmux session is running: `tmux list-sessions`
- Check sender email matches `ALLOWED_SENDERS` in `.env`
- Verify Claude is running inside tmux
**Need help?**
- Check [Issues](https://github.com/JessyTsui/Claude-Code-Remote/issues)
- Follow [@Jiaxi_Cui](https://x.com/Jiaxi_Cui) for updates
### Debug Mode
```bash
# Enable detailed logging
LOG_LEVEL=debug npm run webhooks
DEBUG=true node claude-hook-notify.js completed
```
## 🛡️ Security
- ✅ **Sender Whitelist**: Only authorized emails can send commands
- ✅ **Session Isolation**: Each token controls only its specific session
- ✅ **Auto Expiration**: Sessions timeout automatically
### Multi-Platform Authentication
- ✅ **Email**: Sender whitelist via `ALLOWED_SENDERS` environment variable
- ✅ **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
@ -352,4 +436,4 @@ MIT License - Feel free to use and modify!
**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": {
"type": "chat",
"enabled": false,
"enabled": true,
"config": {
"token": "",
"chatId": ""
"botToken": "",
"chatId": "",
"groupId": "",
"whitelist": []
}
},
"whatsapp": {
@ -40,5 +42,16 @@
"webhook": "",
"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:status": "node claude-remote.js daemon status",
"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": [
"claude-code",
@ -41,8 +44,10 @@
},
"homepage": "https://github.com/Claude-Code-Remote/Claude-Code-Remote#readme",
"dependencies": {
"axios": "^1.6.0",
"dotenv": "^17.2.1",
"execa": "^9.6.0",
"express": "^4.18.2",
"imapflow": "^1.0.191",
"mailparser": "^3.7.4",
"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);
}
// TODO: Load other channels based on configuration
// Discord, Telegram, etc.
// Load LINE channel
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`);
}

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
* Captures input/output from tmux sessions for email notifications
* Tmux Monitor - Enhanced for real-time monitoring with Telegram/LINE automation
* 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 EventEmitter = require('events');
const fs = require('fs');
const path = require('path');
const TraceCapture = require('./trace-capture');
class TmuxMonitor {
constructor() {
class TmuxMonitor extends EventEmitter {
constructor(sessionName = null) {
super();
this.sessionName = sessionName || process.env.TMUX_SESSION || 'claude-real';
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.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
* @param {string} sessionName - The tmux session name
@ -380,6 +724,22 @@ class TmuxMonitor {
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;

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!"