diff --git a/.env.example b/.env.example
index 4abb1ab..7bfee69 100644
--- a/.env.example
+++ b/.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
-# ===== SMTP 发送邮件配置 =====
+# ===== 選擇通知方式:Email、LINE 或 Telegram =====
+# 可以同時啟用多個通知方式
+EMAIL_ENABLED=false
+LINE_ENABLED=false
+TELEGRAM_ENABLED=true
+
+# ===== Email 配置 (如果使用 Email) =====
+# SMTP 发送邮件配置
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_SECURE=true
@@ -12,20 +19,56 @@ SMTP_PASS=your-app-password
EMAIL_FROM=your-email@gmail.com
EMAIL_FROM_NAME=Claude Code Remote 通知系统
-# ===== IMAP 接收邮件配置 =====
+# IMAP 接收邮件配置
IMAP_HOST=imap.gmail.com
IMAP_PORT=993
IMAP_SECURE=true
IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password
-# ===== 邮件路由配置 =====
+# 邮件路由配置
# 接收通知的邮箱地址
EMAIL_TO=your-email@gmail.com
# 允许发送命令的邮箱地址(安全白名单)
ALLOWED_SENDERS=your-email@gmail.com
+# ===== LINE 配置 (如果使用 LINE) =====
+# 從 LINE Developers Console 獲取: https://developers.line.biz/
+LINE_CHANNEL_ACCESS_TOKEN=your-line-channel-access-token
+LINE_CHANNEL_SECRET=your-line-channel-secret
+
+# LINE 接收者配置(設定一個或兩個)
+# LINE_USER_ID=your-line-user-id
+# LINE_GROUP_ID=your-line-group-id
+
+# LINE 白名單(逗號分隔的使用者/群組 ID)
+# 如果不設定,只有配置的 USER_ID/GROUP_ID 可以使用
+# LINE_WHITELIST=U1234567890abcdef,C1234567890abcdef
+
+# LINE webhook 埠號(預設:3000)
+# LINE_WEBHOOK_PORT=3000
+
+# ===== Telegram 配置 (如果使用 Telegram) =====
+# 從 @BotFather 獲取 Bot Token
+TELEGRAM_BOT_TOKEN=your-telegram-bot-token
+
+# Telegram 接收者配置(設定一個或兩個)
+# 個人聊天 ID
+# TELEGRAM_CHAT_ID=123456789
+# 群組 ID(通常是負數)
+# TELEGRAM_GROUP_ID=-1001234567890
+
+# Telegram 白名單(逗號分隔的 Chat ID)
+# 如果不設定,只有配置的 CHAT_ID/GROUP_ID 可以使用
+# TELEGRAM_WHITELIST=123456789,-1001234567890
+
+# Telegram webhook 埠號(預設:3001)
+# TELEGRAM_WEBHOOK_PORT=3001
+
+# Telegram webhook URL(您的公開 HTTPS URL)
+# TELEGRAM_WEBHOOK_URL=https://your-domain.com
+
# ===== 系统配置 =====
# 会话映射文件路径
SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json
diff --git a/.gitignore b/.gitignore
index 6f8c7d3..4e91d29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,10 +48,9 @@ build/
# Temporary files
tmp/
temp/
-
# Data files (session-specific, should not be committed)
src/data/
!src/data/.gitkeep
# Claude configuration (user-specific)
-CLAUDE.md
\ No newline at end of file
+CLAUDE.md
diff --git a/README.md b/README.md
index 63eb6fa..915bb9e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
# Claude Code Remote
-Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks locally, receive notifications when Claude completes them, and send new commands by simply replying to emails.
+Control [Claude Code](https://claude.ai/code) remotely via multiple messaging platforms. Start tasks locally, receive notifications when Claude completes them, and send new commands by simply replying to messages.
+
+**Supported Platforms:**
+- 📧 **Email** - Traditional SMTP/IMAP integration with execution trace
+- 📱 **Telegram** - Interactive bot with smart buttons ✅ **NEW**
+- 💬 **LINE** - Rich messaging with token-based commands
+- 🖥️ **Desktop** - Sound alerts and system notifications
@@ -18,18 +24,34 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
## ✨ Features
-- **📧 Email Notifications**: Get notified when Claude completes tasks 
-- **🔄 Email Control**: Reply to emails to send new commands to Claude
-- **📱 Remote Access**: Control Claude from anywhere with just email
-- **🔒 Secure**: Whitelist-based sender verification
+- **📧 Multiple Messaging Platforms**:
+ - Email notifications with full execution trace and reply-to-send commands
+ - Telegram Bot with interactive buttons and slash commands ✅ **NEW**
+ - LINE messaging with token-based commands
+ - Desktop notifications with sound alerts
+- **🔄 Two-way Control**: Reply to messages or emails to send new commands
+- **📱 Remote Access**: Control Claude from anywhere
+- **🔒 Secure**: ID-based whitelist verification for all platforms
+- **👥 Group Support**: Use in LINE groups or Telegram groups for team collaboration
+- **🤖 Smart Commands**: Intuitive command formats for each platform
- **📋 Multi-line Support**: Send complex commands with formatting
-
+- **⚡ Smart Monitoring**: Intelligent detection of Claude responses with historical tracking
+- **🔄 tmux Integration**: Seamless command injection into active tmux sessions
+- **📊 Execution Trace**: Full terminal output capture in email notifications
## 📅 Changelog
### August 2025
+- **2025-08-02**: Add full execution trace to email notifications ([#14](https://github.com/JessyTsui/Claude-Code-Remote/pull/14))
+- **2025-08-01**: Enhanced Multi-Channel Notification System (by @laihenyi @JessyTsui)
+ - ✅ **Telegram Integration Completed** - Interactive buttons, real-time commands, smart personal/group chat handling
+ - ✅ **Multi-Channel Notifications** - Simultaneous delivery to Desktop, Telegram, Email, LINE
+ - ✅ **Smart Sound Alerts** - Always-on audio feedback with customizable sounds
+ - ✅ **Intelligent Session Management** - Auto-detection, real conversation content, 24-hour tokens
+- **2025-08-01**: Fix #9 #12: Add configuration to disable subagent notifications ([#10](https://github.com/JessyTsui/Claude-Code-Remote/pull/10))
- **2025-08-01**: Implement terminal-style UI for email notifications ([#8](https://github.com/JessyTsui/Claude-Code-Remote/pull/8) by [@vaclisinc](https://github.com/vaclisinc))
- **2025-08-01**: Fix working directory issue - enable claude-remote to run from any directory ([#7](https://github.com/JessyTsui/Claude-Code-Remote/pull/7) by [@vaclisinc](https://github.com/vaclisinc))
+
### July 2025
- **2025-07-31**: Fix self-reply loop issue when using same email for send/receive ([#4](https://github.com/JessyTsui/Claude-Code-Remote/pull/4) by [@vaclisinc](https://github.com/vaclisinc))
- **2025-07-28**: Remove hardcoded values and implement environment-based configuration ([#2](https://github.com/JessyTsui/Claude-Code-Remote/pull/2) by [@kevinsslin](https://github.com/kevinsslin))
@@ -37,28 +59,33 @@ Control [Claude Code](https://claude.ai/code) remotely via email. Start tasks lo
## 📋 TODO List
### Notification Channels
-- [ ] **Discord & Telegram**: Bot integration for messaging platforms
-- [ ] **Slack Workflow**: Native Slack app with slash commands
+- ~~**📱 Telegram Integration**~~ ✅ **COMPLETED** - Bot integration with interactive buttons and real-time commands
+- **💬 Discord Integration** - Bot integration for messaging platforms
+- **⚡ Slack Workflow** - Native Slack app with slash commands
### Developer Tools
-- [ ] **AI Tools**: Support for Gemini CLI, Cursor, and other AI tools
-- [ ] **Git Automation**: Auto-commit, PR creation, branch management
+- **🤖 AI Tools Support** - Integration with Gemini CLI, Cursor, and other AI development tools
+- **🔀 Git Automation** - Auto-commit functionality, PR creation, branch management
### Usage Analytics
-- [ ] **Cost Tracking**: Token usage and estimated costs
-- [ ] **Performance Metrics**: Execution time and resource usage
-- [ ] **Scheduled Reports**: Daily/weekly usage summaries via email
+- **💰 Cost Tracking** - Token usage monitoring and estimated costs
+- **⚡ Performance Metrics** - Execution time tracking and resource usage analysis
+- **📧 Scheduled Reports** - Daily/weekly usage summaries delivered via email
### Native Apps
-- [ ] **Mobile Apps**: iOS and Android applications
-- [ ] **Desktop Apps**: macOS and Windows native clients
+- **📱 Mobile Apps** - iOS and Android applications for remote Claude control
+- **🖥️ Desktop Apps** - macOS and Windows native clients with system integration
+## 🚀 Quick Start
-## 🚀 Setup Guide
+### 1. Prerequisites
-Follow these steps to get Claude Code Remote running:
+**System Requirements:**
+- Node.js >= 14.0.0
+- **tmux** (required for command injection)
+- Active tmux session with Claude Code running
-### Step 1: Clone and Install Dependencies
+### 2. Install
```bash
git clone https://github.com/JessyTsui/Claude-Code-Remote.git
@@ -66,62 +93,78 @@ cd Claude-Code-Remote
npm install
```
-### Step 2: Configure Email Settings
+### 3. Choose Your Platform
+
+#### Option A: Configure Email (Recommended for Beginners)
```bash
-# Copy the example configuration
+# Copy example config
cp .env.example .env
-# Open .env in your editor
-nano .env # or use vim, code, etc.
+# Edit with your email credentials
+nano .env
```
-Edit the `.env` file with your email credentials:
-
+**Required email settings:**
```env
-# Email account for sending notifications
+EMAIL_ENABLED=true
SMTP_USER=your-email@gmail.com
-SMTP_PASS=your-app-password # Gmail: use App Password, not regular password
-
-# Email account for receiving replies (can be same as SMTP)
+SMTP_PASS=your-app-password
IMAP_USER=your-email@gmail.com
IMAP_PASS=your-app-password
-
-# Where to send notifications
EMAIL_TO=your-notification-email@gmail.com
-
-# Who can send commands (security whitelist)
ALLOWED_SENDERS=your-notification-email@gmail.com
-
-# Path to session data (use absolute path)
-SESSION_MAP_PATH=/your/absolute/path/to/Claude-Code-Remote/src/data/session-map.json
+SESSION_MAP_PATH=/your/path/to/Claude-Code-Remote/src/data/session-map.json
```
-📌 **Gmail users**: Create an [App Password](https://myaccount.google.com/security) instead of using your regular password.
-> Note: You may need to enable two-step verification in your google account first before create app password.
+📌 **Gmail users**: Use [App Passwords](https://myaccount.google.com/security), not your regular password.
-### Step 3: Set Up Claude Code Hooks
-
-Open Claude's settings file:
+#### Option B: Configure Telegram ✅ **NEW**
+**Quick Setup:**
```bash
-# Create the directory if it doesn't exist
-mkdir -p ~/.claude
-
-# Edit settings.json
-nano ~/.claude/settings.json
+chmod +x setup-telegram.sh
+./setup-telegram.sh
```
-Add this configuration (replace `/your/absolute/path/` with your actual path):
+**Manual Setup:**
+1. Create bot via [@BotFather](https://t.me/BotFather)
+2. Get your Chat ID from bot API
+3. Configure webhook URL (use ngrok for local testing)
-```json
+**Required Telegram settings:**
+```env
+TELEGRAM_ENABLED=true
+TELEGRAM_BOT_TOKEN=your-bot-token-here
+TELEGRAM_CHAT_ID=your-chat-id-here
+TELEGRAM_WEBHOOK_URL=https://your-ngrok-url.app
+SESSION_MAP_PATH=/your/path/to/Claude-Code-Remote/src/data/session-map.json
+```
+
+#### Option C: Configure LINE
+
+**Required LINE settings:**
+```env
+LINE_ENABLED=true
+LINE_CHANNEL_ACCESS_TOKEN=your-token
+LINE_CHANNEL_SECRET=your-secret
+LINE_USER_ID=your-user-id
+```
+
+### 4. Configure Claude Code Hooks
+
+Create hooks configuration file:
+
+**Method 1: Global Configuration (Recommended)**
+```bash
+# Add to ~/.claude/settings.json
{
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
- "command": "node /your/absolute/path/to/Claude-Code-Remote/claude-remote.js notify --type completed",
+ "command": "node /your/path/to/Claude-Code-Remote/claude-hook-notify.js completed",
"timeout": 5
}]
}],
@@ -129,7 +172,7 @@ Add this configuration (replace `/your/absolute/path/` with your actual path):
"matcher": "*",
"hooks": [{
"type": "command",
- "command": "node /your/absolute/path/to/Claude-Code-Remote/claude-remote.js notify --type waiting",
+ "command": "node /your/path/to/Claude-Code-Remote/claude-hook-notify.js waiting",
"timeout": 5
}]
}]
@@ -137,111 +180,93 @@ Add this configuration (replace `/your/absolute/path/` with your actual path):
}
```
+**Method 2: Project-Specific Configuration**
+```bash
+# Set environment variable
+export CLAUDE_HOOKS_CONFIG=/your/path/to/Claude-Code-Remote/claude-hooks.json
+```
+
> **Note**: Subagent notifications are disabled by default. To enable them, set `enableSubagentNotifications: true` in your config. See [Subagent Notifications Guide](./docs/SUBAGENT_NOTIFICATIONS.md) for details.
-### Step 4: Test Your Setup
+### 5. Start Services
+#### For All Platforms (Recommended)
```bash
-# Test email configuration
-node claude-remote.js test
+# Automatically starts all enabled platforms
+npm run webhooks
+# or
+node start-all-webhooks.js
```
-You should receive a test email. If not, check your email settings.
+#### For Individual Platforms
-### Step 5: Start Claude Code Remote
-
-**Terminal 1 - Start email monitoring:**
+**For Email:**
```bash
-npm run relay:pty
+npm run daemon:start
+# or
+node claude-remote.js daemon start
```
-Keep this running. You should see:
-```
-🚀 Claude Code Remote is running!
-📧 Monitoring emails...
-```
-
-**Terminal 2 - Start Claude in tmux:**
+**For Telegram:**
```bash
-# Create a new tmux session
-tmux new-session -s my-project
-
-# Inside tmux, start Claude
-claude
+npm run telegram
+# or
+node start-telegram-webhook.js
```
-### Step 6: You're Ready!
-
-1. Use Claude normally in the tmux session
-2. When Claude completes a task, you'll receive an email
-3. Reply to the email with new commands
-4. Your commands will execute automatically in Claude
-
-### Verify Everything Works
-
-In Claude, type:
-```
-What is 2+2?
+**For LINE:**
+```bash
+npm run line
+# or
+node start-line-webhook.js
```
-Wait for Claude to respond, then check your email. You should receive a notification!
+### 6. Test Your Setup
-## 📖 How to Use
-
-### Email Notifications
-When Claude completes a task, you'll receive an email notification:
-
-```
-Subject: Claude Code Remote Task Complete [#ABC123]
-
-Claude completed: "analyze the code structure"
-
-[Claude's full response here...]
-
-Reply to this email to send new commands.
+**Quick Test:**
+```bash
+# Test all notification channels
+node claude-hook-notify.js completed
+# Should receive notifications via all enabled platforms
```
-### Sending Commands via Email Reply
+**Full Test:**
+1. Start Claude in tmux session with hooks enabled
+2. Run any command in Claude
+3. Check for notifications (email/Telegram/LINE)
+4. Reply with new command to test two-way control
-1. **Direct Reply**: Simply reply to the notification email
-2. **Write Command**: Type your command in the email body:
- ```
- Please refactor the main function and add error handling
- ```
-3. **Send**: Your command will automatically execute in Claude!
+## 🎮 How It Works
-### Advanced Email Features
+1. **Use Claude normally** in tmux session
+2. **Get notifications** when Claude completes tasks via:
+ - 🔊 **Sound alert** (Desktop)
+ - 📧 **Email notification with execution trace** (if enabled)
+ - 📱 **Telegram message with buttons** (if enabled)
+ - 💬 **LINE message** (if enabled)
+3. **Reply with commands** using any platform
+4. **Commands execute automatically** in Claude
-**Multi-line Commands**
+### Platform Command Formats
+
+**Email:**
```
-First analyze the current code structure.
-Then create a comprehensive test suite.
-Finally, update the documentation.
+Simply reply to notification email with your command
+No special formatting required
```
-**Complex Instructions**
+**Telegram:** ✅ **NEW**
```
-Refactor the authentication module with these requirements:
-- Use JWT tokens instead of sessions
-- Add rate limiting
-- Implement refresh token logic
-- Update all related tests
+Click smart button to get format:
+📝 Personal Chat: /cmd TOKEN123 your command here
+👥 Group Chat: @bot_name /cmd TOKEN123 your command here
```
-### Email Reply Workflow
-
-1. **Receive Notification** → You get an email when Claude completes a task
-2. **Reply with Command** → Send your next instruction via email reply
-3. **Automatic Execution** → The system extracts your command and injects it into Claude
-4. **Get Results** → Receive another email when the new task completes
-
-### Supported Email Clients
-
-Works with any email client that supports standard reply functionality:
-- ✅ Gmail (Web/Mobile)
-- ✅ Apple Mail
-- ✅ Outlook
-- ✅ Any SMTP-compatible email client
+**LINE:**
+```
+Reply to notification with: Your command here
+(Token automatically extracted from conversation context)
+```
### Advanced Configuration
@@ -284,51 +309,110 @@ Works with any email client that supports standard reply functionality:
This is useful if you find the execution trace too verbose or if your email client has issues with scrollable content.
-## 💡 Common Use Cases
+## 💡 Use Cases
-- **Remote Development**: Start coding at the office, continue from home via email
-- **Long Tasks**: Let Claude work while you're in meetings, check results via email
-- **Team Collaboration**: Share Claude sessions by forwarding notification emails
+- **Remote Code Reviews**: Start reviews at office, continue from home via any platform
+- **Long-running Tasks**: Monitor progress and guide next steps remotely
+- **Multi-location Development**: Control Claude from anywhere without VPN
+- **Team Collaboration**: Share Telegram groups for team notifications
+- **Mobile Development**: Send commands from phone via Telegram
-## 🔧 Useful Commands
+## 🔧 Commands
+### Testing & Diagnostics
```bash
-# Test email setup
-node claude-remote.js test
+# Test all notification channels
+node claude-hook-notify.js completed
-# Check system status
+# Test specific platforms
+node test-telegram-notification.js
+node test-real-notification.js
+node test-injection.js
+
+# System diagnostics
+node claude-remote.js diagnose
node claude-remote.js status
+node claude-remote.js test
+```
-# View tmux sessions
-tmux list-sessions
-tmux attach -t my-project
+### Service Management
+```bash
+# Start all enabled platforms
+npm run webhooks
-# Stop email monitoring
-# Press Ctrl+C in the terminal running npm run relay:pty
+# Individual services
+npm run telegram # Telegram webhook
+npm run line # LINE webhook
+npm run daemon:start # Email daemon
+
+# Stop services
+npm run daemon:stop # Stop email daemon
```
## 🔍 Troubleshooting
+### Common Issues
+
+**Not receiving notifications from Claude?**
+1. Check hooks configuration in tmux session:
+ ```bash
+ echo $CLAUDE_HOOKS_CONFIG
+ ```
+2. Verify Claude is running with hooks enabled
+3. Test notification manually:
+ ```bash
+ node claude-hook-notify.js completed
+ ```
+
+**Telegram bot not responding?** ✅ **NEW**
+```bash
+# Test bot connectivity
+curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
+ -H "Content-Type: application/json" \
+ -d "{\"chat_id\": $TELEGRAM_CHAT_ID, \"text\": \"Test\"}"
+
+# Check webhook status
+curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo"
+```
+
+**Commands not executing in Claude?**
+```bash
+# Check tmux session exists
+tmux list-sessions
+
+# Verify injection mode
+grep INJECTION_MODE .env # Should be 'tmux'
+
+# Test injection
+node test-injection.js
+```
+
**Not receiving emails?**
- Run `node claude-remote.js test` to test email setup
- Check spam folder
- Verify SMTP settings in `.env`
- For Gmail: ensure you're using App Password
-**Commands not executing?**
-- Ensure tmux session is running: `tmux list-sessions`
-- Check sender email matches `ALLOWED_SENDERS` in `.env`
-- Verify Claude is running inside tmux
-
-**Need help?**
-- Check [Issues](https://github.com/JessyTsui/Claude-Code-Remote/issues)
-- Follow [@Jiaxi_Cui](https://x.com/Jiaxi_Cui) for updates
+### Debug Mode
+```bash
+# Enable detailed logging
+LOG_LEVEL=debug npm run webhooks
+DEBUG=true node claude-hook-notify.js completed
+```
## 🛡️ Security
-- ✅ **Sender Whitelist**: Only authorized emails can send commands
-- ✅ **Session Isolation**: Each token controls only its specific session
-- ✅ **Auto Expiration**: Sessions timeout automatically
+### Multi-Platform Authentication
+- ✅ **Email**: Sender whitelist via `ALLOWED_SENDERS` environment variable
+- ✅ **Telegram**: Bot token and chat ID verification
+- ✅ **LINE**: Channel secret and access token validation
+- ✅ **Session Tokens**: 8-character alphanumeric tokens for command verification
+
+### Session Security
+- ✅ **Session Isolation**: Each token controls only its specific tmux session
+- ✅ **Auto Expiration**: Sessions timeout automatically after 24 hours
+- ✅ **Token-based Commands**: All platforms require valid session tokens
+- ✅ **Minimal Data Storage**: Session files contain only necessary information
## 🤝 Contributing
@@ -352,4 +436,4 @@ MIT License - Feel free to use and modify!
⭐ **Star this repo** if it helps you code more efficiently!
-> 💡 **Tip**: Share your remote coding setup on Twitter and tag [@Jiaxi_Cui](https://x.com/Jiaxi_Cui) - we love seeing how developers use Claude Code Remote!
+> 💡 **Tip**: Enable multiple notification channels for redundancy - never miss a Claude completion again!
\ No newline at end of file
diff --git a/assets/telegram_demo.png b/assets/telegram_demo.png
new file mode 100644
index 0000000..fd9d9a6
Binary files /dev/null and b/assets/telegram_demo.png differ
diff --git a/claude-control.js b/claude-control.js
deleted file mode 100644
index 5ff6aa2..0000000
--- a/claude-control.js
+++ /dev/null
@@ -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;
\ No newline at end of file
diff --git a/claude-hook-notify.js b/claude-hook-notify.js
new file mode 100755
index 0000000..f8caca3
--- /dev/null
+++ b/claude-hook-notify.js
@@ -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();
\ No newline at end of file
diff --git a/claude-hooks.json b/claude-hooks.json
new file mode 100644
index 0000000..6e9f58d
--- /dev/null
+++ b/claude-hooks.json
@@ -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
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/config/channels.json b/config/channels.json
index ad49722..e744f53 100644
--- a/config/channels.json
+++ b/config/channels.json
@@ -19,10 +19,12 @@
},
"telegram": {
"type": "chat",
- "enabled": false,
+ "enabled": true,
"config": {
- "token": "",
- "chatId": ""
+ "botToken": "",
+ "chatId": "",
+ "groupId": "",
+ "whitelist": []
}
},
"whatsapp": {
@@ -40,5 +42,16 @@
"webhook": "",
"secret": ""
}
+ },
+ "line": {
+ "type": "chat",
+ "enabled": true,
+ "config": {
+ "channelAccessToken": "",
+ "channelSecret": "",
+ "userId": "",
+ "groupId": "",
+ "whitelist": []
+ }
}
}
\ No newline at end of file
diff --git a/fix-telegram.sh b/fix-telegram.sh
new file mode 100755
index 0000000..f966967
--- /dev/null
+++ b/fix-telegram.sh
@@ -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"
\ No newline at end of file
diff --git a/install-global.js b/install-global.js
deleted file mode 100644
index f148314..0000000
--- a/install-global.js
+++ /dev/null
@@ -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 };
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 2df6247..ec0a450 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,8 +14,10 @@
"win32"
],
"dependencies": {
+ "axios": "^1.6.0",
"dotenv": "^17.2.1",
"execa": "^9.6.0",
+ "express": "^4.18.2",
"imapflow": "^1.0.191",
"mailparser": "^3.7.4",
"node-imap": "^0.9.6",
@@ -57,6 +59,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@@ -65,11 +92,144 @@
"node": ">=8.0.0"
}
},
+ "node_modules/axios": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -91,6 +251,15 @@
"node": "*"
}
},
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -99,6 +268,34 @@
"node": ">=0.10.0"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -161,6 +358,35 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/encoding-japanese": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz",
@@ -188,6 +414,66 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/execa": {
"version": "9.6.0",
"resolved": "https://registry.npmmirror.com/execa/-/execa-9.6.0.tgz",
@@ -213,6 +499,52 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/fast-copy/-/fast-copy-3.0.2.tgz",
@@ -245,6 +577,124 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz",
@@ -260,6 +710,57 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
@@ -306,6 +807,22 @@
"entities": "^4.4.0"
}
},
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/human-signals": {
"version": "8.0.1",
"resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz",
@@ -358,6 +875,15 @@
"node": ">= 12"
}
},
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -481,6 +1007,75 @@
"libqp": "2.1.1"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
@@ -489,11 +1084,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/nan": {
"version": "2.23.0",
"resolved": "https://registry.npmmirror.com/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ=="
},
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/node-imap": {
"version": "0.9.6",
"resolved": "https://registry.npmmirror.com/node-imap/-/node-imap-0.9.6.tgz",
@@ -549,6 +1159,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
@@ -557,6 +1179,18 @@
"node": ">=14.0.0"
}
},
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
@@ -588,6 +1222,15 @@
"url": "https://ko-fi.com/killymxi"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
@@ -596,6 +1239,12 @@
"node": ">=8"
}
},
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
"node_modules/peberminta": {
"version": "0.9.0",
"resolved": "https://registry.npmmirror.com/peberminta/-/peberminta-0.9.0.tgz",
@@ -690,6 +1339,25 @@
}
]
},
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz",
@@ -707,11 +1375,62 @@
"node": ">=6"
}
},
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -789,6 +1508,66 @@
"semver": "bin/semver"
}
},
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -808,6 +1587,78 @@
"node": ">=8"
}
},
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -862,6 +1713,15 @@
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -908,6 +1768,28 @@
"tlds": "bin.js"
}
},
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -924,6 +1806,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/utf7": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/utf7/-/utf7-1.0.2.tgz",
@@ -937,6 +1828,15 @@
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz",
@@ -949,6 +1849,15 @@
"uuid": "dist/esm/bin/uuid"
}
},
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 44a974e..76daabd 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,10 @@
"daemon:stop": "node claude-remote.js daemon stop",
"daemon:status": "node claude-remote.js daemon status",
"relay:pty": "node start-relay-pty.js",
- "relay:start": "INJECTION_MODE=pty node src/relay/relay-pty.js"
+ "relay:start": "INJECTION_MODE=pty node src/relay/relay-pty.js",
+ "telegram": "node start-telegram-webhook.js",
+ "line": "node start-line-webhook.js",
+ "webhooks": "node start-all-webhooks.js"
},
"keywords": [
"claude-code",
@@ -41,8 +44,10 @@
},
"homepage": "https://github.com/Claude-Code-Remote/Claude-Code-Remote#readme",
"dependencies": {
+ "axios": "^1.6.0",
"dotenv": "^17.2.1",
"execa": "^9.6.0",
+ "express": "^4.18.2",
"imapflow": "^1.0.191",
"mailparser": "^3.7.4",
"node-imap": "^0.9.6",
diff --git a/send-test-reply.js b/send-test-reply.js
deleted file mode 100644
index a31edd2..0000000
--- a/send-test-reply.js
+++ /dev/null
@@ -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);
\ No newline at end of file
diff --git a/setup-telegram.sh b/setup-telegram.sh
new file mode 100755
index 0000000..a11a70d
--- /dev/null
+++ b/setup-telegram.sh
@@ -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"
\ No newline at end of file
diff --git a/smart-monitor.js b/smart-monitor.js
new file mode 100644
index 0000000..31c5400
--- /dev/null
+++ b/smart-monitor.js
@@ -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();
\ No newline at end of file
diff --git a/src/channels/line/line.js b/src/channels/line/line.js
new file mode 100644
index 0000000..4b3d0b0
--- /dev/null
+++ b/src/channels/line/line.js
@@ -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;
\ No newline at end of file
diff --git a/src/channels/line/webhook.js b/src/channels/line/webhook.js
new file mode 100644
index 0000000..bb8cb53
--- /dev/null
+++ b/src/channels/line/webhook.js
@@ -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;
\ No newline at end of file
diff --git a/src/channels/telegram/telegram.js b/src/channels/telegram/telegram.js
new file mode 100644
index 0000000..14ad758
--- /dev/null
+++ b/src/channels/telegram/telegram.js
@@ -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} \`\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;
\ No newline at end of file
diff --git a/src/channels/telegram/webhook.js b/src/channels/telegram/webhook.js
new file mode 100644
index 0000000..ddf855c
--- /dev/null
+++ b/src/channels/telegram/webhook.js
@@ -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 `\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} \`\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} \`\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} \`\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 \`\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 \` - 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;
\ No newline at end of file
diff --git a/src/core/notifier.js b/src/core/notifier.js
index 07961ee..814eec7 100644
--- a/src/core/notifier.js
+++ b/src/core/notifier.js
@@ -50,8 +50,24 @@ class Notifier {
this.registerChannel('email', email);
}
- // TODO: Load other channels based on configuration
- // Discord, Telegram, etc.
+ // Load LINE channel
+ const LINEChannel = require('../channels/line/line');
+ const lineConfig = this.config.getChannel('line');
+ if (lineConfig && lineConfig.enabled) {
+ const line = new LINEChannel(lineConfig.config || {});
+ this.registerChannel('line', line);
+ }
+
+ // Load Telegram channel
+ const TelegramChannel = require('../channels/telegram/telegram');
+ const telegramConfig = this.config.getChannel('telegram');
+ if (telegramConfig && telegramConfig.enabled) {
+ const telegram = new TelegramChannel(telegramConfig.config || {});
+ this.registerChannel('telegram', telegram);
+ }
+
+ // ✅ Telegram integration completed
+ // TODO: Future channels - Discord, Slack, Teams, etc.
this.logger.info(`Initialized ${this.channels.size} channels`);
}
diff --git a/src/data/processed-messages.json b/src/data/processed-messages.json
new file mode 100644
index 0000000..36877ed
--- /dev/null
+++ b/src/data/processed-messages.json
@@ -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
+ }
+]
\ No newline at end of file
diff --git a/src/data/session-map.json b/src/data/session-map.json
new file mode 100644
index 0000000..2ca03b0
--- /dev/null
+++ b/src/data/session-map.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/src/utils/controller-injector.js b/src/utils/controller-injector.js
new file mode 100644
index 0000000..f49efd1
--- /dev/null
+++ b/src/utils/controller-injector.js
@@ -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;
\ No newline at end of file
diff --git a/src/utils/tmux-monitor.js b/src/utils/tmux-monitor.js
index af7b636..07f6f26 100644
--- a/src/utils/tmux-monitor.js
+++ b/src/utils/tmux-monitor.js
@@ -1,16 +1,76 @@
/**
- * Tmux Session Monitor
- * Captures input/output from tmux sessions for email notifications
+ * Tmux Monitor - Enhanced for real-time monitoring with Telegram/LINE automation
+ * Monitors tmux session output for Claude completion patterns
+ * Based on the original email automation mechanism but adapted for real-time notifications
*/
const { execSync } = require('child_process');
+const EventEmitter = require('events');
const fs = require('fs');
const path = require('path');
const TraceCapture = require('./trace-capture');
-class TmuxMonitor {
- constructor() {
+class TmuxMonitor extends EventEmitter {
+ constructor(sessionName = null) {
+ super();
+ this.sessionName = sessionName || process.env.TMUX_SESSION || 'claude-real';
this.captureDir = path.join(__dirname, '../data/tmux-captures');
+ this.isMonitoring = false;
+ this.monitorInterval = null;
+ this.lastPaneContent = '';
+ this.outputBuffer = [];
+ this.maxBufferSize = 1000; // Keep last 1000 lines
+ this.checkInterval = 2000; // Check every 2 seconds
+
+ // Claude completion patterns (adapted for Claude Code's actual output format)
+ this.completionPatterns = [
+ // Task completion indicators
+ /task.*completed/i,
+ /successfully.*completed/i,
+ /completed.*successfully/i,
+ /implementation.*complete/i,
+ /changes.*made/i,
+ /created.*successfully/i,
+ /updated.*successfully/i,
+ /file.*created/i,
+ /file.*updated/i,
+ /finished/i,
+ /done/i,
+ /✅/,
+ /All set/i,
+ /Ready/i,
+
+ // Claude Code specific patterns
+ /The file.*has been updated/i,
+ /File created successfully/i,
+ /Command executed successfully/i,
+ /Operation completed/i,
+
+ // Look for prompt return (indicating Claude finished responding)
+ /╰.*╯\s*$/, // Box ending
+ /^\s*>\s*$/ // Empty prompt ready for input
+ ];
+
+ // Waiting patterns (when Claude needs input)
+ this.waitingPatterns = [
+ /waiting.*for/i,
+ /need.*input/i,
+ /please.*provide/i,
+ /what.*would you like/i,
+ /how.*can I help/i,
+ /⏳/,
+ /What would you like me to/i,
+ /Is there anything else/i,
+ /Any other/i,
+ /Do you want/i,
+ /Would you like/i,
+
+ // Claude Code specific waiting patterns
+ /\? for shortcuts/i, // Claude Code waiting indicator
+ /╭.*─.*╮/, // Start of response box
+ />\s*$/ // Empty prompt
+ ];
+
this._ensureCaptureDir();
this.traceCapture = new TraceCapture();
}
@@ -21,6 +81,290 @@ class TmuxMonitor {
}
}
+ // Real-time monitoring methods (new functionality)
+ start() {
+ if (this.isMonitoring) {
+ console.log('⚠️ TmuxMonitor already running');
+ return;
+ }
+
+ // Verify tmux session exists
+ if (!this._sessionExists()) {
+ console.error(`❌ Tmux session '${this.sessionName}' not found`);
+ throw new Error(`Tmux session '${this.sessionName}' not found`);
+ }
+
+ this.isMonitoring = true;
+ this._startRealTimeMonitoring();
+ console.log(`🔍 Started monitoring tmux session: ${this.sessionName}`);
+ }
+
+ stop() {
+ if (!this.isMonitoring) {
+ return;
+ }
+
+ this.isMonitoring = false;
+ if (this.monitorInterval) {
+ clearInterval(this.monitorInterval);
+ this.monitorInterval = null;
+ }
+ console.log('⏹️ TmuxMonitor stopped');
+ }
+
+ _sessionExists() {
+ try {
+ const sessions = execSync('tmux list-sessions -F "#{session_name}"', {
+ encoding: 'utf8',
+ stdio: ['ignore', 'pipe', 'ignore']
+ }).trim().split('\n');
+
+ return sessions.includes(this.sessionName);
+ } catch (error) {
+ return false;
+ }
+ }
+
+ _startRealTimeMonitoring() {
+ // Initial capture
+ this._captureCurrentContent();
+
+ // Set up periodic monitoring
+ this.monitorInterval = setInterval(() => {
+ if (this.isMonitoring) {
+ this._checkForChanges();
+ }
+ }, this.checkInterval);
+ }
+
+ _captureCurrentContent() {
+ try {
+ // Capture current pane content
+ const content = execSync(`tmux capture-pane -t ${this.sessionName} -p`, {
+ encoding: 'utf8',
+ stdio: ['ignore', 'pipe', 'ignore']
+ });
+
+ return content;
+ } catch (error) {
+ console.error('Error capturing tmux content:', error.message);
+ return '';
+ }
+ }
+
+ _checkForChanges() {
+ const currentContent = this._captureCurrentContent();
+
+ if (currentContent !== this.lastPaneContent) {
+ // Get new content (lines that were added)
+ const newLines = this._getNewLines(this.lastPaneContent, currentContent);
+
+ if (newLines.length > 0) {
+ // Add to buffer
+ this.outputBuffer.push(...newLines);
+
+ // Trim buffer if too large
+ if (this.outputBuffer.length > this.maxBufferSize) {
+ this.outputBuffer = this.outputBuffer.slice(-this.maxBufferSize);
+ }
+
+ // Check for completion patterns
+ this._analyzeNewContent(newLines);
+ }
+
+ this.lastPaneContent = currentContent;
+ }
+ }
+
+ _getNewLines(oldContent, newContent) {
+ const oldLines = oldContent.split('\n');
+ const newLines = newContent.split('\n');
+
+ // Find lines that were added
+ const addedLines = [];
+
+ // Simple approach: compare line by line from the end
+ const oldLength = oldLines.length;
+ const newLength = newLines.length;
+
+ if (newLength > oldLength) {
+ // New lines were added
+ const numNewLines = newLength - oldLength;
+ addedLines.push(...newLines.slice(-numNewLines));
+ } else if (newLength === oldLength) {
+ // Same number of lines, check if last lines changed
+ for (let i = Math.max(0, newLength - 5); i < newLength; i++) {
+ if (i < oldLength && newLines[i] !== oldLines[i]) {
+ addedLines.push(newLines[i]);
+ }
+ }
+ }
+
+ return addedLines.filter(line => line.trim().length > 0);
+ }
+
+ _analyzeNewContent(newLines) {
+ const recentText = newLines.join('\n');
+
+ // Also check the entire recent buffer for context
+ const bufferText = this.outputBuffer.slice(-20).join('\n');
+
+ console.log('🔍 Analyzing new content:', newLines.slice(0, 2).map(line => line.substring(0, 50))); // Debug log
+
+ // Look for Claude response completion patterns
+ const hasResponseEnd = this._detectResponseCompletion(recentText, bufferText);
+ const hasTaskCompletion = this._detectTaskCompletion(recentText, bufferText);
+
+ if (hasTaskCompletion || hasResponseEnd) {
+ console.log('🎯 Task completion detected');
+ this._handleTaskCompletion(newLines);
+ }
+ // Don't constantly trigger waiting notifications for static content
+ else if (this._shouldTriggerWaitingNotification(recentText)) {
+ console.log('⏳ New waiting state detected');
+ this._handleWaitingForInput(newLines);
+ }
+ }
+
+ _detectResponseCompletion(recentText, bufferText) {
+ // Look for Claude response completion indicators
+ const completionIndicators = [
+ /The file.*has been updated/i,
+ /File created successfully/i,
+ /successfully/i,
+ /completed/i,
+ /✅/,
+ /done/i
+ ];
+
+ // Claude Code specific pattern: ⏺ response followed by box
+ const hasClaudeResponse = /⏺.*/.test(bufferText) || /⏺.*/.test(recentText);
+ const hasBoxStart = /╭.*╮/.test(recentText);
+ const hasBoxEnd = /╰.*╯/.test(recentText);
+
+ // Look for the pattern: ⏺ response -> box -> empty prompt
+ const isCompleteResponse = hasClaudeResponse && (hasBoxStart || hasBoxEnd);
+
+ return completionIndicators.some(pattern => pattern.test(recentText)) ||
+ isCompleteResponse;
+ }
+
+ _detectTaskCompletion(recentText, bufferText) {
+ // Look for specific completion patterns
+ return this.completionPatterns.some(pattern => pattern.test(recentText));
+ }
+
+ _shouldTriggerWaitingNotification(recentText) {
+ // Only trigger waiting notification for new meaningful content
+ // Avoid triggering on static "? for shortcuts" that doesn't change
+ const meaningfulWaitingPatterns = [
+ /waiting.*for/i,
+ /need.*input/i,
+ /please.*provide/i,
+ /what.*would you like/i,
+ /Do you want/i,
+ /Would you like/i
+ ];
+
+ return meaningfulWaitingPatterns.some(pattern => pattern.test(recentText)) &&
+ !recentText.includes('? for shortcuts'); // Ignore static shortcuts line
+ }
+
+ _handleTaskCompletion(newLines) {
+ const conversation = this._extractRecentConversation();
+
+ console.log('🎉 Claude task completion detected!');
+
+ this.emit('taskCompleted', {
+ type: 'completed',
+ sessionName: this.sessionName,
+ timestamp: new Date().toISOString(),
+ newOutput: newLines,
+ conversation: conversation,
+ triggerText: newLines.join('\n')
+ });
+ }
+
+ _handleWaitingForInput(newLines) {
+ const conversation = this._extractRecentConversation();
+
+ console.log('⏳ Claude waiting for input detected!');
+
+ this.emit('waitingForInput', {
+ type: 'waiting',
+ sessionName: this.sessionName,
+ timestamp: new Date().toISOString(),
+ newOutput: newLines,
+ conversation: conversation,
+ triggerText: newLines.join('\n')
+ });
+ }
+
+ _extractRecentConversation() {
+ // Extract recent conversation from buffer
+ const recentBuffer = this.outputBuffer.slice(-50); // Last 50 lines
+ const text = recentBuffer.join('\n');
+
+ // Try to identify user question and Claude response using Claude Code patterns
+ let userQuestion = '';
+ let claudeResponse = '';
+
+ // Look for Claude Code specific patterns
+ const lines = recentBuffer;
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+
+ // Look for user input (after > prompt)
+ if (line.startsWith('> ') && line.length > 2) {
+ userQuestion = line.substring(2).trim();
+ continue;
+ }
+
+ // Look for Claude response (⏺ prefix)
+ if (line.startsWith('⏺ ') && line.length > 2) {
+ claudeResponse = line.substring(2).trim();
+ break;
+ }
+ }
+
+ // If we didn't find the specific format, use fallback
+ if (!userQuestion || !claudeResponse) {
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+
+ // Skip system lines
+ if (!line || line.startsWith('[') || line.startsWith('$') ||
+ line.startsWith('#') || line.includes('? for shortcuts') ||
+ line.match(/^[╭╰│─]+$/)) {
+ continue;
+ }
+
+ if (!userQuestion && line.length > 2) {
+ userQuestion = line;
+ } else if (userQuestion && !claudeResponse && line.length > 5 && line !== userQuestion) {
+ claudeResponse = line;
+ break;
+ }
+ }
+ }
+
+ return {
+ userQuestion: userQuestion || 'Recent command',
+ claudeResponse: claudeResponse || 'Task completed',
+ fullContext: text
+ };
+ }
+
+ // Manual trigger methods for testing
+ triggerCompletionTest() {
+ this._handleTaskCompletion(['Test completion notification']);
+ }
+
+ triggerWaitingTest() {
+ this._handleWaitingForInput(['Test waiting notification']);
+ }
+
+ // Original capture methods (legacy support)
/**
* Start capturing a tmux session
* @param {string} sessionName - The tmux session name
@@ -380,6 +724,22 @@ class TmuxMonitor {
console.error('Failed to cleanup captures:', error.message);
}
}
+
+ // Enhanced status method
+ getStatus() {
+ return {
+ isMonitoring: this.isMonitoring,
+ sessionName: this.sessionName,
+ sessionExists: this._sessionExists(),
+ bufferSize: this.outputBuffer.length,
+ checkInterval: this.checkInterval,
+ patterns: {
+ completion: this.completionPatterns.length,
+ waiting: this.waitingPatterns.length
+ },
+ lastCheck: new Date().toISOString()
+ };
+ }
}
module.exports = TmuxMonitor;
\ No newline at end of file
diff --git a/start-all-webhooks.js b/start-all-webhooks.js
new file mode 100755
index 0000000..244f8f8
--- /dev/null
+++ b/start-all-webhooks.js
@@ -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 ');
+}
+if (process.env.LINE_ENABLED === 'true') {
+ console.log(' LINE: Token TOKEN123 ');
+}
+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();
\ No newline at end of file
diff --git a/start-line-webhook.js b/start-line-webhook.js
new file mode 100755
index 0000000..8ac6470
--- /dev/null
+++ b/start-line-webhook.js
@@ -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);
+});
\ No newline at end of file
diff --git a/start-telegram-webhook.js b/start-telegram-webhook.js
new file mode 100755
index 0000000..c4a76f3
--- /dev/null
+++ b/start-telegram-webhook.js
@@ -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);
+});
\ No newline at end of file
diff --git a/taskping-config.js b/taskping-config.js
deleted file mode 100755
index 342ec3f..0000000
--- a/taskping-config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env node
-
-const ConfigManager = require('./src/config-manager');
-
-const manager = new ConfigManager();
-manager.interactiveMenu().catch(console.error);
\ No newline at end of file
diff --git a/test-complete-flow.sh b/test-complete-flow.sh
new file mode 100755
index 0000000..055977d
--- /dev/null
+++ b/test-complete-flow.sh
@@ -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 " # 然后尝试一个简单任务"
\ No newline at end of file
diff --git a/test-injection.js b/test-injection.js
new file mode 100755
index 0000000..b54d135
--- /dev/null
+++ b/test-injection.js
@@ -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
\ No newline at end of file
diff --git a/test-real-notification.js b/test-real-notification.js
new file mode 100644
index 0000000..c5dede5
--- /dev/null
+++ b/test-real-notification.js
@@ -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] ');
+ 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();
\ No newline at end of file
diff --git a/test-telegram-notification.js b/test-telegram-notification.js
new file mode 100755
index 0000000..de1e839
--- /dev/null
+++ b/test-telegram-notification.js
@@ -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 ');
+ 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();
\ No newline at end of file
diff --git a/test-telegram-setup.sh b/test-telegram-setup.sh
new file mode 100755
index 0000000..d1422dc
--- /dev/null
+++ b/test-telegram-setup.sh
@@ -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!"
\ No newline at end of file