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:
parent
61dd2e30c3
commit
b14f95d821
51
.env.example
51
.env.example
|
|
@ -1,7 +1,14 @@
|
||||||
# Claude Code Remote Email Configuration Example
|
# Claude Code Remote Configuration
|
||||||
# Copy this file to .env and configure with your actual values
|
# Copy this file to .env and configure with your actual values
|
||||||
|
|
||||||
# ===== SMTP 发送邮件配置 =====
|
# ===== 選擇通知方式:Email、LINE 或 Telegram =====
|
||||||
|
# 可以同時啟用多個通知方式
|
||||||
|
EMAIL_ENABLED=false
|
||||||
|
LINE_ENABLED=false
|
||||||
|
TELEGRAM_ENABLED=true
|
||||||
|
|
||||||
|
# ===== Email 配置 (如果使用 Email) =====
|
||||||
|
# SMTP 发送邮件配置
|
||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_HOST=smtp.gmail.com
|
||||||
SMTP_PORT=465
|
SMTP_PORT=465
|
||||||
SMTP_SECURE=true
|
SMTP_SECURE=true
|
||||||
|
|
@ -12,20 +19,56 @@ SMTP_PASS=your-app-password
|
||||||
EMAIL_FROM=your-email@gmail.com
|
EMAIL_FROM=your-email@gmail.com
|
||||||
EMAIL_FROM_NAME=Claude Code Remote 通知系统
|
EMAIL_FROM_NAME=Claude Code Remote 通知系统
|
||||||
|
|
||||||
# ===== IMAP 接收邮件配置 =====
|
# IMAP 接收邮件配置
|
||||||
IMAP_HOST=imap.gmail.com
|
IMAP_HOST=imap.gmail.com
|
||||||
IMAP_PORT=993
|
IMAP_PORT=993
|
||||||
IMAP_SECURE=true
|
IMAP_SECURE=true
|
||||||
IMAP_USER=your-email@gmail.com
|
IMAP_USER=your-email@gmail.com
|
||||||
IMAP_PASS=your-app-password
|
IMAP_PASS=your-app-password
|
||||||
|
|
||||||
# ===== 邮件路由配置 =====
|
# 邮件路由配置
|
||||||
# 接收通知的邮箱地址
|
# 接收通知的邮箱地址
|
||||||
EMAIL_TO=your-email@gmail.com
|
EMAIL_TO=your-email@gmail.com
|
||||||
|
|
||||||
# 允许发送命令的邮箱地址(安全白名单)
|
# 允许发送命令的邮箱地址(安全白名单)
|
||||||
ALLOWED_SENDERS=your-email@gmail.com
|
ALLOWED_SENDERS=your-email@gmail.com
|
||||||
|
|
||||||
|
# ===== LINE 配置 (如果使用 LINE) =====
|
||||||
|
# 從 LINE Developers Console 獲取: https://developers.line.biz/
|
||||||
|
LINE_CHANNEL_ACCESS_TOKEN=your-line-channel-access-token
|
||||||
|
LINE_CHANNEL_SECRET=your-line-channel-secret
|
||||||
|
|
||||||
|
# LINE 接收者配置(設定一個或兩個)
|
||||||
|
# LINE_USER_ID=your-line-user-id
|
||||||
|
# LINE_GROUP_ID=your-line-group-id
|
||||||
|
|
||||||
|
# LINE 白名單(逗號分隔的使用者/群組 ID)
|
||||||
|
# 如果不設定,只有配置的 USER_ID/GROUP_ID 可以使用
|
||||||
|
# LINE_WHITELIST=U1234567890abcdef,C1234567890abcdef
|
||||||
|
|
||||||
|
# LINE webhook 埠號(預設:3000)
|
||||||
|
# LINE_WEBHOOK_PORT=3000
|
||||||
|
|
||||||
|
# ===== Telegram 配置 (如果使用 Telegram) =====
|
||||||
|
# 從 @BotFather 獲取 Bot Token
|
||||||
|
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||||
|
|
||||||
|
# Telegram 接收者配置(設定一個或兩個)
|
||||||
|
# 個人聊天 ID
|
||||||
|
# TELEGRAM_CHAT_ID=123456789
|
||||||
|
# 群組 ID(通常是負數)
|
||||||
|
# TELEGRAM_GROUP_ID=-1001234567890
|
||||||
|
|
||||||
|
# Telegram 白名單(逗號分隔的 Chat ID)
|
||||||
|
# 如果不設定,只有配置的 CHAT_ID/GROUP_ID 可以使用
|
||||||
|
# TELEGRAM_WHITELIST=123456789,-1001234567890
|
||||||
|
|
||||||
|
# Telegram webhook 埠號(預設:3001)
|
||||||
|
# TELEGRAM_WEBHOOK_PORT=3001
|
||||||
|
|
||||||
|
# Telegram webhook URL(您的公開 HTTPS URL)
|
||||||
|
# TELEGRAM_WEBHOOK_URL=https://your-domain.com
|
||||||
|
|
||||||
# ===== 系统配置 =====
|
# ===== 系统配置 =====
|
||||||
# 会话映射文件路径
|
# 会话映射文件路径
|
||||||
SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json
|
SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ build/
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
# Data files (session-specific, should not be committed)
|
# Data files (session-specific, should not be committed)
|
||||||
src/data/
|
src/data/
|
||||||
!src/data/.gitkeep
|
!src/data/.gitkeep
|
||||||
|
|
|
||||||
388
README.md
388
README.md
|
|
@ -1,6 +1,12 @@
|
||||||
# Claude Code Remote
|
# Claude Code Remote
|
||||||
|
|
||||||
Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks locally, receive notifications when Claude completes them, and send new commands by simply replying to emails.
|
Control [Claude Code](https://claude.ai/code) remotely via multiple messaging platforms. Start tasks locally, receive notifications when Claude completes them, and send new commands by simply replying to messages.
|
||||||
|
|
||||||
|
**Supported Platforms:**
|
||||||
|
- 📧 **Email** - Traditional SMTP/IMAP integration with execution trace
|
||||||
|
- 📱 **Telegram** - Interactive bot with smart buttons ✅ **NEW**
|
||||||
|
- 💬 **LINE** - Rich messaging with token-based commands
|
||||||
|
- 🖥️ **Desktop** - Sound alerts and system notifications
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
|
@ -18,18 +24,34 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **📧 Email Notifications**: Get notified when Claude completes tasks 
|
- **📧 Multiple Messaging Platforms**:
|
||||||
- **🔄 Email Control**: Reply to emails to send new commands to Claude
|
- Email notifications with full execution trace and reply-to-send commands
|
||||||
- **📱 Remote Access**: Control Claude from anywhere with just email
|
- Telegram Bot with interactive buttons and slash commands ✅ **NEW**
|
||||||
- **🔒 Secure**: Whitelist-based sender verification
|
- LINE messaging with token-based commands
|
||||||
|
- Desktop notifications with sound alerts
|
||||||
|
- **🔄 Two-way Control**: Reply to messages or emails to send new commands
|
||||||
|
- **📱 Remote Access**: Control Claude from anywhere
|
||||||
|
- **🔒 Secure**: ID-based whitelist verification for all platforms
|
||||||
|
- **👥 Group Support**: Use in LINE groups or Telegram groups for team collaboration
|
||||||
|
- **🤖 Smart Commands**: Intuitive command formats for each platform
|
||||||
- **📋 Multi-line Support**: Send complex commands with formatting
|
- **📋 Multi-line Support**: Send complex commands with formatting
|
||||||
|
- **⚡ Smart Monitoring**: Intelligent detection of Claude responses with historical tracking
|
||||||
|
- **🔄 tmux Integration**: Seamless command injection into active tmux sessions
|
||||||
|
- **📊 Execution Trace**: Full terminal output capture in email notifications
|
||||||
|
|
||||||
## 📅 Changelog
|
## 📅 Changelog
|
||||||
|
|
||||||
### August 2025
|
### August 2025
|
||||||
|
- **2025-08-02**: Add full execution trace to email notifications ([#14](https://github.com/JessyTsui/Claude-Code-Remote/pull/14))
|
||||||
|
- **2025-08-01**: Enhanced Multi-Channel Notification System (by @laihenyi @JessyTsui)
|
||||||
|
- ✅ **Telegram Integration Completed** - Interactive buttons, real-time commands, smart personal/group chat handling
|
||||||
|
- ✅ **Multi-Channel Notifications** - Simultaneous delivery to Desktop, Telegram, Email, LINE
|
||||||
|
- ✅ **Smart Sound Alerts** - Always-on audio feedback with customizable sounds
|
||||||
|
- ✅ **Intelligent Session Management** - Auto-detection, real conversation content, 24-hour tokens
|
||||||
|
- **2025-08-01**: Fix #9 #12: Add configuration to disable subagent notifications ([#10](https://github.com/JessyTsui/Claude-Code-Remote/pull/10))
|
||||||
- **2025-08-01**: Implement terminal-style UI for email notifications ([#8](https://github.com/JessyTsui/Claude-Code-Remote/pull/8) by [@vaclisinc](https://github.com/vaclisinc))
|
- **2025-08-01**: Implement terminal-style UI for email notifications ([#8](https://github.com/JessyTsui/Claude-Code-Remote/pull/8) by [@vaclisinc](https://github.com/vaclisinc))
|
||||||
- **2025-08-01**: Fix working directory issue - enable claude-remote to run from any directory ([#7](https://github.com/JessyTsui/Claude-Code-Remote/pull/7) by [@vaclisinc](https://github.com/vaclisinc))
|
- **2025-08-01**: Fix working directory issue - enable claude-remote to run from any directory ([#7](https://github.com/JessyTsui/Claude-Code-Remote/pull/7) by [@vaclisinc](https://github.com/vaclisinc))
|
||||||
|
|
||||||
### July 2025
|
### July 2025
|
||||||
- **2025-07-31**: Fix self-reply loop issue when using same email for send/receive ([#4](https://github.com/JessyTsui/Claude-Code-Remote/pull/4) by [@vaclisinc](https://github.com/vaclisinc))
|
- **2025-07-31**: Fix self-reply loop issue when using same email for send/receive ([#4](https://github.com/JessyTsui/Claude-Code-Remote/pull/4) by [@vaclisinc](https://github.com/vaclisinc))
|
||||||
- **2025-07-28**: Remove hardcoded values and implement environment-based configuration ([#2](https://github.com/JessyTsui/Claude-Code-Remote/pull/2) by [@kevinsslin](https://github.com/kevinsslin))
|
- **2025-07-28**: Remove hardcoded values and implement environment-based configuration ([#2](https://github.com/JessyTsui/Claude-Code-Remote/pull/2) by [@kevinsslin](https://github.com/kevinsslin))
|
||||||
|
|
@ -37,28 +59,33 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
|
||||||
## 📋 TODO List
|
## 📋 TODO List
|
||||||
|
|
||||||
### Notification Channels
|
### Notification Channels
|
||||||
- [ ] **Discord & Telegram**: Bot integration for messaging platforms
|
- ~~**📱 Telegram Integration**~~ ✅ **COMPLETED** - Bot integration with interactive buttons and real-time commands
|
||||||
- [ ] **Slack Workflow**: Native Slack app with slash commands
|
- **💬 Discord Integration** - Bot integration for messaging platforms
|
||||||
|
- **⚡ Slack Workflow** - Native Slack app with slash commands
|
||||||
|
|
||||||
### Developer Tools
|
### Developer Tools
|
||||||
- [ ] **AI Tools**: Support for Gemini CLI, Cursor, and other AI tools
|
- **🤖 AI Tools Support** - Integration with Gemini CLI, Cursor, and other AI development tools
|
||||||
- [ ] **Git Automation**: Auto-commit, PR creation, branch management
|
- **🔀 Git Automation** - Auto-commit functionality, PR creation, branch management
|
||||||
|
|
||||||
### Usage Analytics
|
### Usage Analytics
|
||||||
- [ ] **Cost Tracking**: Token usage and estimated costs
|
- **💰 Cost Tracking** - Token usage monitoring and estimated costs
|
||||||
- [ ] **Performance Metrics**: Execution time and resource usage
|
- **⚡ Performance Metrics** - Execution time tracking and resource usage analysis
|
||||||
- [ ] **Scheduled Reports**: Daily/weekly usage summaries via email
|
- **📧 Scheduled Reports** - Daily/weekly usage summaries delivered via email
|
||||||
|
|
||||||
### Native Apps
|
### Native Apps
|
||||||
- [ ] **Mobile Apps**: iOS and Android applications
|
- **📱 Mobile Apps** - iOS and Android applications for remote Claude control
|
||||||
- [ ] **Desktop Apps**: macOS and Windows native clients
|
- **🖥️ Desktop Apps** - macOS and Windows native clients with system integration
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
## 🚀 Setup Guide
|
### 1. Prerequisites
|
||||||
|
|
||||||
Follow these steps to get Claude Code Remote running:
|
**System Requirements:**
|
||||||
|
- Node.js >= 14.0.0
|
||||||
|
- **tmux** (required for command injection)
|
||||||
|
- Active tmux session with Claude Code running
|
||||||
|
|
||||||
### Step 1: Clone and Install Dependencies
|
### 2. Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/JessyTsui/Claude-Code-Remote.git
|
git clone https://github.com/JessyTsui/Claude-Code-Remote.git
|
||||||
|
|
@ -66,62 +93,78 @@ cd Claude-Code-Remote
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Configure Email Settings
|
### 3. Choose Your Platform
|
||||||
|
|
||||||
|
#### Option A: Configure Email (Recommended for Beginners)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Copy the example configuration
|
# Copy example config
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# Open .env in your editor
|
# Edit with your email credentials
|
||||||
nano .env # or use vim, code, etc.
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit the `.env` file with your email credentials:
|
**Required email settings:**
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Email account for sending notifications
|
EMAIL_ENABLED=true
|
||||||
SMTP_USER=your-email@gmail.com
|
SMTP_USER=your-email@gmail.com
|
||||||
SMTP_PASS=your-app-password # Gmail: use App Password, not regular password
|
SMTP_PASS=your-app-password
|
||||||
|
|
||||||
# Email account for receiving replies (can be same as SMTP)
|
|
||||||
IMAP_USER=your-email@gmail.com
|
IMAP_USER=your-email@gmail.com
|
||||||
IMAP_PASS=your-app-password
|
IMAP_PASS=your-app-password
|
||||||
|
|
||||||
# Where to send notifications
|
|
||||||
EMAIL_TO=your-notification-email@gmail.com
|
EMAIL_TO=your-notification-email@gmail.com
|
||||||
|
|
||||||
# Who can send commands (security whitelist)
|
|
||||||
ALLOWED_SENDERS=your-notification-email@gmail.com
|
ALLOWED_SENDERS=your-notification-email@gmail.com
|
||||||
|
SESSION_MAP_PATH=/your/path/to/Claude-Code-Remote/src/data/session-map.json
|
||||||
# Path to session data (use absolute path)
|
|
||||||
SESSION_MAP_PATH=/your/absolute/path/to/Claude-Code-Remote/src/data/session-map.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
📌 **Gmail users**: Create an [App Password](https://myaccount.google.com/security) instead of using your regular password.
|
📌 **Gmail users**: Use [App Passwords](https://myaccount.google.com/security), not your regular password.
|
||||||
> Note: You may need to enable two-step verification in your google account first before create app password.
|
|
||||||
|
|
||||||
### Step 3: Set Up Claude Code Hooks
|
#### Option B: Configure Telegram ✅ **NEW**
|
||||||
|
|
||||||
Open Claude's settings file:
|
|
||||||
|
|
||||||
|
**Quick Setup:**
|
||||||
```bash
|
```bash
|
||||||
# Create the directory if it doesn't exist
|
chmod +x setup-telegram.sh
|
||||||
mkdir -p ~/.claude
|
./setup-telegram.sh
|
||||||
|
|
||||||
# Edit settings.json
|
|
||||||
nano ~/.claude/settings.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Add this configuration (replace `/your/absolute/path/` with your actual path):
|
**Manual Setup:**
|
||||||
|
1. Create bot via [@BotFather](https://t.me/BotFather)
|
||||||
|
2. Get your Chat ID from bot API
|
||||||
|
3. Configure webhook URL (use ngrok for local testing)
|
||||||
|
|
||||||
```json
|
**Required Telegram settings:**
|
||||||
|
```env
|
||||||
|
TELEGRAM_ENABLED=true
|
||||||
|
TELEGRAM_BOT_TOKEN=your-bot-token-here
|
||||||
|
TELEGRAM_CHAT_ID=your-chat-id-here
|
||||||
|
TELEGRAM_WEBHOOK_URL=https://your-ngrok-url.app
|
||||||
|
SESSION_MAP_PATH=/your/path/to/Claude-Code-Remote/src/data/session-map.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option C: Configure LINE
|
||||||
|
|
||||||
|
**Required LINE settings:**
|
||||||
|
```env
|
||||||
|
LINE_ENABLED=true
|
||||||
|
LINE_CHANNEL_ACCESS_TOKEN=your-token
|
||||||
|
LINE_CHANNEL_SECRET=your-secret
|
||||||
|
LINE_USER_ID=your-user-id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configure Claude Code Hooks
|
||||||
|
|
||||||
|
Create hooks configuration file:
|
||||||
|
|
||||||
|
**Method 1: Global Configuration (Recommended)**
|
||||||
|
```bash
|
||||||
|
# Add to ~/.claude/settings.json
|
||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"Stop": [{
|
"Stop": [{
|
||||||
"matcher": "*",
|
"matcher": "*",
|
||||||
"hooks": [{
|
"hooks": [{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node /your/absolute/path/to/Claude-Code-Remote/claude-remote.js notify --type completed",
|
"command": "node /your/path/to/Claude-Code-Remote/claude-hook-notify.js completed",
|
||||||
"timeout": 5
|
"timeout": 5
|
||||||
}]
|
}]
|
||||||
}],
|
}],
|
||||||
|
|
@ -129,7 +172,7 @@ Add this configuration (replace `/your/absolute/path/` with your actual path):
|
||||||
"matcher": "*",
|
"matcher": "*",
|
||||||
"hooks": [{
|
"hooks": [{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node /your/absolute/path/to/Claude-Code-Remote/claude-remote.js notify --type waiting",
|
"command": "node /your/path/to/Claude-Code-Remote/claude-hook-notify.js waiting",
|
||||||
"timeout": 5
|
"timeout": 5
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
|
|
@ -137,111 +180,93 @@ Add this configuration (replace `/your/absolute/path/` with your actual path):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Method 2: Project-Specific Configuration**
|
||||||
|
```bash
|
||||||
|
# Set environment variable
|
||||||
|
export CLAUDE_HOOKS_CONFIG=/your/path/to/Claude-Code-Remote/claude-hooks.json
|
||||||
|
```
|
||||||
|
|
||||||
> **Note**: Subagent notifications are disabled by default. To enable them, set `enableSubagentNotifications: true` in your config. See [Subagent Notifications Guide](./docs/SUBAGENT_NOTIFICATIONS.md) for details.
|
> **Note**: Subagent notifications are disabled by default. To enable them, set `enableSubagentNotifications: true` in your config. See [Subagent Notifications Guide](./docs/SUBAGENT_NOTIFICATIONS.md) for details.
|
||||||
|
|
||||||
### Step 4: Test Your Setup
|
### 5. Start Services
|
||||||
|
|
||||||
|
#### For All Platforms (Recommended)
|
||||||
```bash
|
```bash
|
||||||
# Test email configuration
|
# Automatically starts all enabled platforms
|
||||||
node claude-remote.js test
|
npm run webhooks
|
||||||
|
# or
|
||||||
|
node start-all-webhooks.js
|
||||||
```
|
```
|
||||||
|
|
||||||
You should receive a test email. If not, check your email settings.
|
#### For Individual Platforms
|
||||||
|
|
||||||
### Step 5: Start Claude Code Remote
|
**For Email:**
|
||||||
|
|
||||||
**Terminal 1 - Start email monitoring:**
|
|
||||||
```bash
|
```bash
|
||||||
npm run relay:pty
|
npm run daemon:start
|
||||||
|
# or
|
||||||
|
node claude-remote.js daemon start
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep this running. You should see:
|
**For Telegram:**
|
||||||
```
|
|
||||||
🚀 Claude Code Remote is running!
|
|
||||||
📧 Monitoring emails...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Terminal 2 - Start Claude in tmux:**
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new tmux session
|
npm run telegram
|
||||||
tmux new-session -s my-project
|
# or
|
||||||
|
node start-telegram-webhook.js
|
||||||
# Inside tmux, start Claude
|
|
||||||
claude
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: You're Ready!
|
**For LINE:**
|
||||||
|
```bash
|
||||||
1. Use Claude normally in the tmux session
|
npm run line
|
||||||
2. When Claude completes a task, you'll receive an email
|
# or
|
||||||
3. Reply to the email with new commands
|
node start-line-webhook.js
|
||||||
4. Your commands will execute automatically in Claude
|
|
||||||
|
|
||||||
### Verify Everything Works
|
|
||||||
|
|
||||||
In Claude, type:
|
|
||||||
```
|
|
||||||
What is 2+2?
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Wait for Claude to respond, then check your email. You should receive a notification!
|
### 6. Test Your Setup
|
||||||
|
|
||||||
## 📖 How to Use
|
**Quick Test:**
|
||||||
|
```bash
|
||||||
### Email Notifications
|
# Test all notification channels
|
||||||
When Claude completes a task, you'll receive an email notification:
|
node claude-hook-notify.js completed
|
||||||
|
# Should receive notifications via all enabled platforms
|
||||||
```
|
|
||||||
Subject: Claude Code Remote Task Complete [#ABC123]
|
|
||||||
|
|
||||||
Claude completed: "analyze the code structure"
|
|
||||||
|
|
||||||
[Claude's full response here...]
|
|
||||||
|
|
||||||
Reply to this email to send new commands.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sending Commands via Email Reply
|
**Full Test:**
|
||||||
|
1. Start Claude in tmux session with hooks enabled
|
||||||
|
2. Run any command in Claude
|
||||||
|
3. Check for notifications (email/Telegram/LINE)
|
||||||
|
4. Reply with new command to test two-way control
|
||||||
|
|
||||||
1. **Direct Reply**: Simply reply to the notification email
|
## 🎮 How It Works
|
||||||
2. **Write Command**: Type your command in the email body:
|
|
||||||
```
|
|
||||||
Please refactor the main function and add error handling
|
|
||||||
```
|
|
||||||
3. **Send**: Your command will automatically execute in Claude!
|
|
||||||
|
|
||||||
### Advanced Email Features
|
1. **Use Claude normally** in tmux session
|
||||||
|
2. **Get notifications** when Claude completes tasks via:
|
||||||
|
- 🔊 **Sound alert** (Desktop)
|
||||||
|
- 📧 **Email notification with execution trace** (if enabled)
|
||||||
|
- 📱 **Telegram message with buttons** (if enabled)
|
||||||
|
- 💬 **LINE message** (if enabled)
|
||||||
|
3. **Reply with commands** using any platform
|
||||||
|
4. **Commands execute automatically** in Claude
|
||||||
|
|
||||||
**Multi-line Commands**
|
### Platform Command Formats
|
||||||
|
|
||||||
|
**Email:**
|
||||||
```
|
```
|
||||||
First analyze the current code structure.
|
Simply reply to notification email with your command
|
||||||
Then create a comprehensive test suite.
|
No special formatting required
|
||||||
Finally, update the documentation.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Complex Instructions**
|
**Telegram:** ✅ **NEW**
|
||||||
```
|
```
|
||||||
Refactor the authentication module with these requirements:
|
Click smart button to get format:
|
||||||
- Use JWT tokens instead of sessions
|
📝 Personal Chat: /cmd TOKEN123 your command here
|
||||||
- Add rate limiting
|
👥 Group Chat: @bot_name /cmd TOKEN123 your command here
|
||||||
- Implement refresh token logic
|
|
||||||
- Update all related tests
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Email Reply Workflow
|
**LINE:**
|
||||||
|
```
|
||||||
1. **Receive Notification** → You get an email when Claude completes a task
|
Reply to notification with: Your command here
|
||||||
2. **Reply with Command** → Send your next instruction via email reply
|
(Token automatically extracted from conversation context)
|
||||||
3. **Automatic Execution** → The system extracts your command and injects it into Claude
|
```
|
||||||
4. **Get Results** → Receive another email when the new task completes
|
|
||||||
|
|
||||||
### Supported Email Clients
|
|
||||||
|
|
||||||
Works with any email client that supports standard reply functionality:
|
|
||||||
- ✅ Gmail (Web/Mobile)
|
|
||||||
- ✅ Apple Mail
|
|
||||||
- ✅ Outlook
|
|
||||||
- ✅ Any SMTP-compatible email client
|
|
||||||
|
|
||||||
### Advanced Configuration
|
### Advanced Configuration
|
||||||
|
|
||||||
|
|
@ -284,51 +309,110 @@ Works with any email client that supports standard reply functionality:
|
||||||
|
|
||||||
This is useful if you find the execution trace too verbose or if your email client has issues with scrollable content.
|
This is useful if you find the execution trace too verbose or if your email client has issues with scrollable content.
|
||||||
|
|
||||||
## 💡 Common Use Cases
|
## 💡 Use Cases
|
||||||
|
|
||||||
- **Remote Development**: Start coding at the office, continue from home via email
|
- **Remote Code Reviews**: Start reviews at office, continue from home via any platform
|
||||||
- **Long Tasks**: Let Claude work while you're in meetings, check results via email
|
- **Long-running Tasks**: Monitor progress and guide next steps remotely
|
||||||
- **Team Collaboration**: Share Claude sessions by forwarding notification emails
|
- **Multi-location Development**: Control Claude from anywhere without VPN
|
||||||
|
- **Team Collaboration**: Share Telegram groups for team notifications
|
||||||
|
- **Mobile Development**: Send commands from phone via Telegram
|
||||||
|
|
||||||
## 🔧 Useful Commands
|
## 🔧 Commands
|
||||||
|
|
||||||
|
### Testing & Diagnostics
|
||||||
```bash
|
```bash
|
||||||
# Test email setup
|
# Test all notification channels
|
||||||
node claude-remote.js test
|
node claude-hook-notify.js completed
|
||||||
|
|
||||||
# Check system status
|
# Test specific platforms
|
||||||
|
node test-telegram-notification.js
|
||||||
|
node test-real-notification.js
|
||||||
|
node test-injection.js
|
||||||
|
|
||||||
|
# System diagnostics
|
||||||
|
node claude-remote.js diagnose
|
||||||
node claude-remote.js status
|
node claude-remote.js status
|
||||||
|
node claude-remote.js test
|
||||||
|
```
|
||||||
|
|
||||||
# View tmux sessions
|
### Service Management
|
||||||
tmux list-sessions
|
```bash
|
||||||
tmux attach -t my-project
|
# Start all enabled platforms
|
||||||
|
npm run webhooks
|
||||||
|
|
||||||
# Stop email monitoring
|
# Individual services
|
||||||
# Press Ctrl+C in the terminal running npm run relay:pty
|
npm run telegram # Telegram webhook
|
||||||
|
npm run line # LINE webhook
|
||||||
|
npm run daemon:start # Email daemon
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
npm run daemon:stop # Stop email daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔍 Troubleshooting
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Not receiving notifications from Claude?**
|
||||||
|
1. Check hooks configuration in tmux session:
|
||||||
|
```bash
|
||||||
|
echo $CLAUDE_HOOKS_CONFIG
|
||||||
|
```
|
||||||
|
2. Verify Claude is running with hooks enabled
|
||||||
|
3. Test notification manually:
|
||||||
|
```bash
|
||||||
|
node claude-hook-notify.js completed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Telegram bot not responding?** ✅ **NEW**
|
||||||
|
```bash
|
||||||
|
# Test bot connectivity
|
||||||
|
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"chat_id\": $TELEGRAM_CHAT_ID, \"text\": \"Test\"}"
|
||||||
|
|
||||||
|
# Check webhook status
|
||||||
|
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commands not executing in Claude?**
|
||||||
|
```bash
|
||||||
|
# Check tmux session exists
|
||||||
|
tmux list-sessions
|
||||||
|
|
||||||
|
# Verify injection mode
|
||||||
|
grep INJECTION_MODE .env # Should be 'tmux'
|
||||||
|
|
||||||
|
# Test injection
|
||||||
|
node test-injection.js
|
||||||
|
```
|
||||||
|
|
||||||
**Not receiving emails?**
|
**Not receiving emails?**
|
||||||
- Run `node claude-remote.js test` to test email setup
|
- Run `node claude-remote.js test` to test email setup
|
||||||
- Check spam folder
|
- Check spam folder
|
||||||
- Verify SMTP settings in `.env`
|
- Verify SMTP settings in `.env`
|
||||||
- For Gmail: ensure you're using App Password
|
- For Gmail: ensure you're using App Password
|
||||||
|
|
||||||
**Commands not executing?**
|
### Debug Mode
|
||||||
- Ensure tmux session is running: `tmux list-sessions`
|
```bash
|
||||||
- Check sender email matches `ALLOWED_SENDERS` in `.env`
|
# Enable detailed logging
|
||||||
- Verify Claude is running inside tmux
|
LOG_LEVEL=debug npm run webhooks
|
||||||
|
DEBUG=true node claude-hook-notify.js completed
|
||||||
**Need help?**
|
```
|
||||||
- Check [Issues](https://github.com/JessyTsui/Claude-Code-Remote/issues)
|
|
||||||
- Follow [@Jiaxi_Cui](https://x.com/Jiaxi_Cui) for updates
|
|
||||||
|
|
||||||
## 🛡️ Security
|
## 🛡️ Security
|
||||||
|
|
||||||
- ✅ **Sender Whitelist**: Only authorized emails can send commands
|
### Multi-Platform Authentication
|
||||||
- ✅ **Session Isolation**: Each token controls only its specific session
|
- ✅ **Email**: Sender whitelist via `ALLOWED_SENDERS` environment variable
|
||||||
- ✅ **Auto Expiration**: Sessions timeout automatically
|
- ✅ **Telegram**: Bot token and chat ID verification
|
||||||
|
- ✅ **LINE**: Channel secret and access token validation
|
||||||
|
- ✅ **Session Tokens**: 8-character alphanumeric tokens for command verification
|
||||||
|
|
||||||
|
### Session Security
|
||||||
|
- ✅ **Session Isolation**: Each token controls only its specific tmux session
|
||||||
|
- ✅ **Auto Expiration**: Sessions timeout automatically after 24 hours
|
||||||
|
- ✅ **Token-based Commands**: All platforms require valid session tokens
|
||||||
|
- ✅ **Minimal Data Storage**: Session files contain only necessary information
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
|
@ -352,4 +436,4 @@ MIT License - Feel free to use and modify!
|
||||||
|
|
||||||
⭐ **Star this repo** if it helps you code more efficiently!
|
⭐ **Star this repo** if it helps you code more efficiently!
|
||||||
|
|
||||||
> 💡 **Tip**: Share your remote coding setup on Twitter and tag [@Jiaxi_Cui](https://x.com/Jiaxi_Cui) - we love seeing how developers use Claude Code Remote!
|
> 💡 **Tip**: Enable multiple notification channels for redundancy - never miss a Claude completion again!
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 520 KiB |
|
|
@ -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;
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,10 +19,12 @@
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"type": "chat",
|
"type": "chat",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"config": {
|
"config": {
|
||||||
"token": "",
|
"botToken": "",
|
||||||
"chatId": ""
|
"chatId": "",
|
||||||
|
"groupId": "",
|
||||||
|
"whitelist": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"whatsapp": {
|
"whatsapp": {
|
||||||
|
|
@ -40,5 +42,16 @@
|
||||||
"webhook": "",
|
"webhook": "",
|
||||||
"secret": ""
|
"secret": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"line": {
|
||||||
|
"type": "chat",
|
||||||
|
"enabled": true,
|
||||||
|
"config": {
|
||||||
|
"channelAccessToken": "",
|
||||||
|
"channelSecret": "",
|
||||||
|
"userId": "",
|
||||||
|
"groupId": "",
|
||||||
|
"whitelist": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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 };
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,7 +9,10 @@
|
||||||
"daemon:stop": "node claude-remote.js daemon stop",
|
"daemon:stop": "node claude-remote.js daemon stop",
|
||||||
"daemon:status": "node claude-remote.js daemon status",
|
"daemon:status": "node claude-remote.js daemon status",
|
||||||
"relay:pty": "node start-relay-pty.js",
|
"relay:pty": "node start-relay-pty.js",
|
||||||
"relay:start": "INJECTION_MODE=pty node src/relay/relay-pty.js"
|
"relay:start": "INJECTION_MODE=pty node src/relay/relay-pty.js",
|
||||||
|
"telegram": "node start-telegram-webhook.js",
|
||||||
|
"line": "node start-line-webhook.js",
|
||||||
|
"webhooks": "node start-all-webhooks.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|
@ -41,8 +44,10 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Claude-Code-Remote/Claude-Code-Remote#readme",
|
"homepage": "https://github.com/Claude-Code-Remote/Claude-Code-Remote#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
|
"express": "^4.18.2",
|
||||||
"imapflow": "^1.0.191",
|
"imapflow": "^1.0.191",
|
||||||
"mailparser": "^3.7.4",
|
"mailparser": "^3.7.4",
|
||||||
"node-imap": "^0.9.6",
|
"node-imap": "^0.9.6",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -50,8 +50,24 @@ class Notifier {
|
||||||
this.registerChannel('email', email);
|
this.registerChannel('email', email);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Load other channels based on configuration
|
// Load LINE channel
|
||||||
// Discord, Telegram, etc.
|
const LINEChannel = require('../channels/line/line');
|
||||||
|
const lineConfig = this.config.getChannel('line');
|
||||||
|
if (lineConfig && lineConfig.enabled) {
|
||||||
|
const line = new LINEChannel(lineConfig.config || {});
|
||||||
|
this.registerChannel('line', line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Telegram channel
|
||||||
|
const TelegramChannel = require('../channels/telegram/telegram');
|
||||||
|
const telegramConfig = this.config.getChannel('telegram');
|
||||||
|
if (telegramConfig && telegramConfig.enabled) {
|
||||||
|
const telegram = new TelegramChannel(telegramConfig.config || {});
|
||||||
|
this.registerChannel('telegram', telegram);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Telegram integration completed
|
||||||
|
// TODO: Future channels - Discord, Slack, Teams, etc.
|
||||||
|
|
||||||
this.logger.info(`Initialized ${this.channels.size} channels`);
|
this.logger.info(`Initialized ${this.channels.size} channels`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,16 +1,76 @@
|
||||||
/**
|
/**
|
||||||
* Tmux Session Monitor
|
* Tmux Monitor - Enhanced for real-time monitoring with Telegram/LINE automation
|
||||||
* Captures input/output from tmux sessions for email notifications
|
* Monitors tmux session output for Claude completion patterns
|
||||||
|
* Based on the original email automation mechanism but adapted for real-time notifications
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
const EventEmitter = require('events');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const TraceCapture = require('./trace-capture');
|
const TraceCapture = require('./trace-capture');
|
||||||
|
|
||||||
class TmuxMonitor {
|
class TmuxMonitor extends EventEmitter {
|
||||||
constructor() {
|
constructor(sessionName = null) {
|
||||||
|
super();
|
||||||
|
this.sessionName = sessionName || process.env.TMUX_SESSION || 'claude-real';
|
||||||
this.captureDir = path.join(__dirname, '../data/tmux-captures');
|
this.captureDir = path.join(__dirname, '../data/tmux-captures');
|
||||||
|
this.isMonitoring = false;
|
||||||
|
this.monitorInterval = null;
|
||||||
|
this.lastPaneContent = '';
|
||||||
|
this.outputBuffer = [];
|
||||||
|
this.maxBufferSize = 1000; // Keep last 1000 lines
|
||||||
|
this.checkInterval = 2000; // Check every 2 seconds
|
||||||
|
|
||||||
|
// Claude completion patterns (adapted for Claude Code's actual output format)
|
||||||
|
this.completionPatterns = [
|
||||||
|
// Task completion indicators
|
||||||
|
/task.*completed/i,
|
||||||
|
/successfully.*completed/i,
|
||||||
|
/completed.*successfully/i,
|
||||||
|
/implementation.*complete/i,
|
||||||
|
/changes.*made/i,
|
||||||
|
/created.*successfully/i,
|
||||||
|
/updated.*successfully/i,
|
||||||
|
/file.*created/i,
|
||||||
|
/file.*updated/i,
|
||||||
|
/finished/i,
|
||||||
|
/done/i,
|
||||||
|
/✅/,
|
||||||
|
/All set/i,
|
||||||
|
/Ready/i,
|
||||||
|
|
||||||
|
// Claude Code specific patterns
|
||||||
|
/The file.*has been updated/i,
|
||||||
|
/File created successfully/i,
|
||||||
|
/Command executed successfully/i,
|
||||||
|
/Operation completed/i,
|
||||||
|
|
||||||
|
// Look for prompt return (indicating Claude finished responding)
|
||||||
|
/╰.*╯\s*$/, // Box ending
|
||||||
|
/^\s*>\s*$/ // Empty prompt ready for input
|
||||||
|
];
|
||||||
|
|
||||||
|
// Waiting patterns (when Claude needs input)
|
||||||
|
this.waitingPatterns = [
|
||||||
|
/waiting.*for/i,
|
||||||
|
/need.*input/i,
|
||||||
|
/please.*provide/i,
|
||||||
|
/what.*would you like/i,
|
||||||
|
/how.*can I help/i,
|
||||||
|
/⏳/,
|
||||||
|
/What would you like me to/i,
|
||||||
|
/Is there anything else/i,
|
||||||
|
/Any other/i,
|
||||||
|
/Do you want/i,
|
||||||
|
/Would you like/i,
|
||||||
|
|
||||||
|
// Claude Code specific waiting patterns
|
||||||
|
/\? for shortcuts/i, // Claude Code waiting indicator
|
||||||
|
/╭.*─.*╮/, // Start of response box
|
||||||
|
/>\s*$/ // Empty prompt
|
||||||
|
];
|
||||||
|
|
||||||
this._ensureCaptureDir();
|
this._ensureCaptureDir();
|
||||||
this.traceCapture = new TraceCapture();
|
this.traceCapture = new TraceCapture();
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +81,290 @@ class TmuxMonitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Real-time monitoring methods (new functionality)
|
||||||
|
start() {
|
||||||
|
if (this.isMonitoring) {
|
||||||
|
console.log('⚠️ TmuxMonitor already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tmux session exists
|
||||||
|
if (!this._sessionExists()) {
|
||||||
|
console.error(`❌ Tmux session '${this.sessionName}' not found`);
|
||||||
|
throw new Error(`Tmux session '${this.sessionName}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMonitoring = true;
|
||||||
|
this._startRealTimeMonitoring();
|
||||||
|
console.log(`🔍 Started monitoring tmux session: ${this.sessionName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (!this.isMonitoring) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMonitoring = false;
|
||||||
|
if (this.monitorInterval) {
|
||||||
|
clearInterval(this.monitorInterval);
|
||||||
|
this.monitorInterval = null;
|
||||||
|
}
|
||||||
|
console.log('⏹️ TmuxMonitor stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
_sessionExists() {
|
||||||
|
try {
|
||||||
|
const sessions = execSync('tmux list-sessions -F "#{session_name}"', {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore']
|
||||||
|
}).trim().split('\n');
|
||||||
|
|
||||||
|
return sessions.includes(this.sessionName);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startRealTimeMonitoring() {
|
||||||
|
// Initial capture
|
||||||
|
this._captureCurrentContent();
|
||||||
|
|
||||||
|
// Set up periodic monitoring
|
||||||
|
this.monitorInterval = setInterval(() => {
|
||||||
|
if (this.isMonitoring) {
|
||||||
|
this._checkForChanges();
|
||||||
|
}
|
||||||
|
}, this.checkInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
_captureCurrentContent() {
|
||||||
|
try {
|
||||||
|
// Capture current pane content
|
||||||
|
const content = execSync(`tmux capture-pane -t ${this.sessionName} -p`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore']
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error capturing tmux content:', error.message);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkForChanges() {
|
||||||
|
const currentContent = this._captureCurrentContent();
|
||||||
|
|
||||||
|
if (currentContent !== this.lastPaneContent) {
|
||||||
|
// Get new content (lines that were added)
|
||||||
|
const newLines = this._getNewLines(this.lastPaneContent, currentContent);
|
||||||
|
|
||||||
|
if (newLines.length > 0) {
|
||||||
|
// Add to buffer
|
||||||
|
this.outputBuffer.push(...newLines);
|
||||||
|
|
||||||
|
// Trim buffer if too large
|
||||||
|
if (this.outputBuffer.length > this.maxBufferSize) {
|
||||||
|
this.outputBuffer = this.outputBuffer.slice(-this.maxBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for completion patterns
|
||||||
|
this._analyzeNewContent(newLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastPaneContent = currentContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getNewLines(oldContent, newContent) {
|
||||||
|
const oldLines = oldContent.split('\n');
|
||||||
|
const newLines = newContent.split('\n');
|
||||||
|
|
||||||
|
// Find lines that were added
|
||||||
|
const addedLines = [];
|
||||||
|
|
||||||
|
// Simple approach: compare line by line from the end
|
||||||
|
const oldLength = oldLines.length;
|
||||||
|
const newLength = newLines.length;
|
||||||
|
|
||||||
|
if (newLength > oldLength) {
|
||||||
|
// New lines were added
|
||||||
|
const numNewLines = newLength - oldLength;
|
||||||
|
addedLines.push(...newLines.slice(-numNewLines));
|
||||||
|
} else if (newLength === oldLength) {
|
||||||
|
// Same number of lines, check if last lines changed
|
||||||
|
for (let i = Math.max(0, newLength - 5); i < newLength; i++) {
|
||||||
|
if (i < oldLength && newLines[i] !== oldLines[i]) {
|
||||||
|
addedLines.push(newLines[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addedLines.filter(line => line.trim().length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_analyzeNewContent(newLines) {
|
||||||
|
const recentText = newLines.join('\n');
|
||||||
|
|
||||||
|
// Also check the entire recent buffer for context
|
||||||
|
const bufferText = this.outputBuffer.slice(-20).join('\n');
|
||||||
|
|
||||||
|
console.log('🔍 Analyzing new content:', newLines.slice(0, 2).map(line => line.substring(0, 50))); // Debug log
|
||||||
|
|
||||||
|
// Look for Claude response completion patterns
|
||||||
|
const hasResponseEnd = this._detectResponseCompletion(recentText, bufferText);
|
||||||
|
const hasTaskCompletion = this._detectTaskCompletion(recentText, bufferText);
|
||||||
|
|
||||||
|
if (hasTaskCompletion || hasResponseEnd) {
|
||||||
|
console.log('🎯 Task completion detected');
|
||||||
|
this._handleTaskCompletion(newLines);
|
||||||
|
}
|
||||||
|
// Don't constantly trigger waiting notifications for static content
|
||||||
|
else if (this._shouldTriggerWaitingNotification(recentText)) {
|
||||||
|
console.log('⏳ New waiting state detected');
|
||||||
|
this._handleWaitingForInput(newLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_detectResponseCompletion(recentText, bufferText) {
|
||||||
|
// Look for Claude response completion indicators
|
||||||
|
const completionIndicators = [
|
||||||
|
/The file.*has been updated/i,
|
||||||
|
/File created successfully/i,
|
||||||
|
/successfully/i,
|
||||||
|
/completed/i,
|
||||||
|
/✅/,
|
||||||
|
/done/i
|
||||||
|
];
|
||||||
|
|
||||||
|
// Claude Code specific pattern: ⏺ response followed by box
|
||||||
|
const hasClaudeResponse = /⏺.*/.test(bufferText) || /⏺.*/.test(recentText);
|
||||||
|
const hasBoxStart = /╭.*╮/.test(recentText);
|
||||||
|
const hasBoxEnd = /╰.*╯/.test(recentText);
|
||||||
|
|
||||||
|
// Look for the pattern: ⏺ response -> box -> empty prompt
|
||||||
|
const isCompleteResponse = hasClaudeResponse && (hasBoxStart || hasBoxEnd);
|
||||||
|
|
||||||
|
return completionIndicators.some(pattern => pattern.test(recentText)) ||
|
||||||
|
isCompleteResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
_detectTaskCompletion(recentText, bufferText) {
|
||||||
|
// Look for specific completion patterns
|
||||||
|
return this.completionPatterns.some(pattern => pattern.test(recentText));
|
||||||
|
}
|
||||||
|
|
||||||
|
_shouldTriggerWaitingNotification(recentText) {
|
||||||
|
// Only trigger waiting notification for new meaningful content
|
||||||
|
// Avoid triggering on static "? for shortcuts" that doesn't change
|
||||||
|
const meaningfulWaitingPatterns = [
|
||||||
|
/waiting.*for/i,
|
||||||
|
/need.*input/i,
|
||||||
|
/please.*provide/i,
|
||||||
|
/what.*would you like/i,
|
||||||
|
/Do you want/i,
|
||||||
|
/Would you like/i
|
||||||
|
];
|
||||||
|
|
||||||
|
return meaningfulWaitingPatterns.some(pattern => pattern.test(recentText)) &&
|
||||||
|
!recentText.includes('? for shortcuts'); // Ignore static shortcuts line
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleTaskCompletion(newLines) {
|
||||||
|
const conversation = this._extractRecentConversation();
|
||||||
|
|
||||||
|
console.log('🎉 Claude task completion detected!');
|
||||||
|
|
||||||
|
this.emit('taskCompleted', {
|
||||||
|
type: 'completed',
|
||||||
|
sessionName: this.sessionName,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
newOutput: newLines,
|
||||||
|
conversation: conversation,
|
||||||
|
triggerText: newLines.join('\n')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleWaitingForInput(newLines) {
|
||||||
|
const conversation = this._extractRecentConversation();
|
||||||
|
|
||||||
|
console.log('⏳ Claude waiting for input detected!');
|
||||||
|
|
||||||
|
this.emit('waitingForInput', {
|
||||||
|
type: 'waiting',
|
||||||
|
sessionName: this.sessionName,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
newOutput: newLines,
|
||||||
|
conversation: conversation,
|
||||||
|
triggerText: newLines.join('\n')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_extractRecentConversation() {
|
||||||
|
// Extract recent conversation from buffer
|
||||||
|
const recentBuffer = this.outputBuffer.slice(-50); // Last 50 lines
|
||||||
|
const text = recentBuffer.join('\n');
|
||||||
|
|
||||||
|
// Try to identify user question and Claude response using Claude Code patterns
|
||||||
|
let userQuestion = '';
|
||||||
|
let claudeResponse = '';
|
||||||
|
|
||||||
|
// Look for Claude Code specific patterns
|
||||||
|
const lines = recentBuffer;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
|
||||||
|
// Look for user input (after > prompt)
|
||||||
|
if (line.startsWith('> ') && line.length > 2) {
|
||||||
|
userQuestion = line.substring(2).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for Claude response (⏺ prefix)
|
||||||
|
if (line.startsWith('⏺ ') && line.length > 2) {
|
||||||
|
claudeResponse = line.substring(2).trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find the specific format, use fallback
|
||||||
|
if (!userQuestion || !claudeResponse) {
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
|
||||||
|
// Skip system lines
|
||||||
|
if (!line || line.startsWith('[') || line.startsWith('$') ||
|
||||||
|
line.startsWith('#') || line.includes('? for shortcuts') ||
|
||||||
|
line.match(/^[╭╰│─]+$/)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userQuestion && line.length > 2) {
|
||||||
|
userQuestion = line;
|
||||||
|
} else if (userQuestion && !claudeResponse && line.length > 5 && line !== userQuestion) {
|
||||||
|
claudeResponse = line;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userQuestion: userQuestion || 'Recent command',
|
||||||
|
claudeResponse: claudeResponse || 'Task completed',
|
||||||
|
fullContext: text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual trigger methods for testing
|
||||||
|
triggerCompletionTest() {
|
||||||
|
this._handleTaskCompletion(['Test completion notification']);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerWaitingTest() {
|
||||||
|
this._handleWaitingForInput(['Test waiting notification']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original capture methods (legacy support)
|
||||||
/**
|
/**
|
||||||
* Start capturing a tmux session
|
* Start capturing a tmux session
|
||||||
* @param {string} sessionName - The tmux session name
|
* @param {string} sessionName - The tmux session name
|
||||||
|
|
@ -380,6 +724,22 @@ class TmuxMonitor {
|
||||||
console.error('Failed to cleanup captures:', error.message);
|
console.error('Failed to cleanup captures:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced status method
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
isMonitoring: this.isMonitoring,
|
||||||
|
sessionName: this.sessionName,
|
||||||
|
sessionExists: this._sessionExists(),
|
||||||
|
bufferSize: this.outputBuffer.length,
|
||||||
|
checkInterval: this.checkInterval,
|
||||||
|
patterns: {
|
||||||
|
completion: this.completionPatterns.length,
|
||||||
|
waiting: this.waitingPatterns.length
|
||||||
|
},
|
||||||
|
lastCheck: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TmuxMonitor;
|
module.exports = TmuxMonitor;
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const ConfigManager = require('./src/config-manager');
|
|
||||||
|
|
||||||
const manager = new ConfigManager();
|
|
||||||
manager.interactiveMenu().catch(console.error);
|
|
||||||
|
|
@ -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 " # 然后尝试一个简单任务"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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!"
|
||||||
Loading…
Reference in New Issue