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