#!/usr/bin/env node /** * Claude-Code-Remote - Claude Code Smart Notification System * Main entry point for the CLI tool */ // Load environment variables require('dotenv').config(); const Logger = require('./src/core/logger'); const Notifier = require('./src/core/notifier'); const ConfigManager = require('./src/core/config'); class ClaudeCodeRemoteCLI { constructor() { this.logger = new Logger('CLI'); this.config = new ConfigManager(); this.notifier = new Notifier(this.config); } async init() { // Load configuration this.config.load(); // Initialize channels await this.notifier.initializeChannels(); } async run() { const args = process.argv.slice(2); const command = args[0]; try { await this.init(); switch (command) { case 'notify': await this.handleNotify(args.slice(1)); break; case 'test': await this.handleTest(args.slice(1)); break; case 'status': await this.handleStatus(args.slice(1)); break; case 'config': await this.handleConfig(args.slice(1)); break; case 'install': await this.handleInstall(args.slice(1)); break; case 'relay': await this.handleRelay(args.slice(1)); break; case 'edit-config': await this.handleEditConfig(args.slice(1)); break; case 'setup-email': await this.handleSetupEmail(args.slice(1)); break; case 'daemon': await this.handleDaemon(args.slice(1)); break; case 'commands': await this.handleCommands(args.slice(1)); break; case 'test-paste': await this.handleTestPaste(args.slice(1)); break; case 'test-simple': await this.handleTestSimple(args.slice(1)); break; case 'test-claude': await this.handleTestClaude(args.slice(1)); break; case 'setup-permissions': await this.handleSetupPermissions(args.slice(1)); break; case 'diagnose': await this.handleDiagnose(args.slice(1)); break; case '--help': case '-h': case undefined: this.showHelp(); break; default: console.error(`Unknown command: ${command}`); this.showHelp(); process.exit(1); } } catch (error) { this.logger.error('CLI error:', error.message); process.exit(1); } } async handleNotify(args) { const typeIndex = args.findIndex(arg => arg === '--type'); if (typeIndex === -1 || typeIndex + 1 >= args.length) { console.error('Usage: claude-remote notify --type '); process.exit(1); } const type = args[typeIndex + 1]; if (!['completed', 'waiting'].includes(type)) { console.error('Invalid type. Use: completed or waiting'); process.exit(1); } // Automatically capture current tmux session conversation content const metadata = await this.captureCurrentConversation(); const result = await this.notifier.notify(type, metadata); if (result.success) { this.logger.info(`${type} notification sent successfully`); process.exit(0); } else { this.logger.error(`Failed to send ${type} notification`); process.exit(1); } } async captureCurrentConversation() { try { const { execSync } = require('child_process'); const TmuxMonitor = require('./src/utils/tmux-monitor'); // Get current tmux session name let currentSession = null; try { currentSession = execSync('tmux display-message -p "#S"', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch (e) { // Not running in tmux, return empty metadata return {}; } if (!currentSession) { return {}; } // Use TmuxMonitor to capture conversation const tmuxMonitor = new TmuxMonitor(); const conversation = tmuxMonitor.getRecentConversation(currentSession); return { userQuestion: conversation.userQuestion, claudeResponse: conversation.claudeResponse, tmuxSession: currentSession }; } catch (error) { this.logger.debug('Failed to capture conversation:', error.message); return {}; } } async handleTest(args) { console.log('Testing notification channels...\n'); const results = await this.notifier.test(); for (const [channel, result] of Object.entries(results)) { const status = result.success ? '✅ PASS' : '❌ FAIL'; console.log(`${channel}: ${status}`); if (result.error) { console.log(` Error: ${result.error}`); } } const passCount = Object.values(results).filter(r => r.success).length; const totalCount = Object.keys(results).length; console.log(`\nTest completed: ${passCount}/${totalCount} channels passed`); if (passCount === 0) { process.exit(1); } } async handleStatus(args) { const status = this.notifier.getStatus(); console.log('Claude-Code-Remote Status\n'); console.log('Configuration:'); console.log(` Enabled: ${status.enabled ? 'Yes' : 'No'}`); console.log(` Language: ${status.config.language}`); console.log(` Sounds: ${status.config.sound.completed} / ${status.config.sound.waiting}`); console.log('\nChannels:'); // Display all available channels, including disabled ones const allChannels = this.config._channels || {}; const activeChannels = status.channels || {}; // Merge all channel information const channelNames = new Set([ ...Object.keys(allChannels), ...Object.keys(activeChannels) ]); for (const name of channelNames) { const channelConfig = allChannels[name] || {}; const channelStatus = activeChannels[name]; let enabled, configured, relay; if (channelStatus) { // Active channel, use actual status enabled = channelStatus.enabled ? '✅' : '❌'; configured = channelStatus.configured ? '✅' : '❌'; relay = channelStatus.supportsRelay ? '✅' : '❌'; } else { // Inactive channel, use configuration status enabled = channelConfig.enabled ? '✅' : '❌'; configured = this._isChannelConfigured(name, channelConfig) ? '✅' : '❌'; relay = this._supportsRelay(name) ? '✅' : '❌'; } console.log(` ${name}:`); console.log(` Enabled: ${enabled}`); console.log(` Configured: ${configured}`); console.log(` Supports Relay: ${relay}`); } } _isChannelConfigured(name, config) { switch (name) { case 'desktop': return true; // Desktop notifications don't need special configuration case 'email': return config.config && config.config.smtp && config.config.smtp.host && config.config.smtp.auth && config.config.smtp.auth.user && config.config.to; default: return false; } } _supportsRelay(name) { switch (name) { case 'email': return true; case 'desktop': default: return false; } } async handleConfig(args) { // Launch the configuration tool const ConfigTool = require('./src/tools/config-manager'); const configTool = new ConfigTool(this.config); await configTool.run(args); } async handleInstall(args) { // Launch the installer const Installer = require('./src/tools/installer'); const installer = new Installer(this.config); await installer.run(args); } async handleRelay(args) { const subcommand = args[0]; switch (subcommand) { case 'start': await this.startRelay(args.slice(1)); break; case 'stop': await this.stopRelay(args.slice(1)); break; case 'status': await this.relayStatus(args.slice(1)); break; case 'cleanup': await this.cleanupRelay(args.slice(1)); break; default: console.error('Usage: claude-remote relay '); console.log(''); console.log('Commands:'); console.log(' start Start email command relay service'); console.log(' stop Stop email command relay service'); console.log(' status View relay service status'); console.log(' cleanup Clean up completed command history'); process.exit(1); } } async startRelay(args) { try { const CommandRelayService = require('./src/relay/command-relay'); const emailConfig = this.config.getChannel('email'); if (!emailConfig || !emailConfig.enabled) { console.error('❌ Email channel not configured or disabled'); console.log('Please run first: claude-remote config'); process.exit(1); } console.log('🚀 Starting email command relay service...'); const relayService = new CommandRelayService(emailConfig.config); // Listen for events relayService.on('started', () => { console.log('✅ Command relay service started'); console.log('📧 Listening for email replies...'); console.log('💡 You can now remotely execute Claude Code commands by replying to emails'); console.log(''); console.log('Press Ctrl+C to stop the service'); }); relayService.on('commandQueued', (command) => { console.log(`📨 Received new command: ${command.command.substring(0, 50)}...`); }); relayService.on('commandExecuted', (command) => { console.log(`✅ Command executed successfully: ${command.id}`); }); relayService.on('commandFailed', (command, error) => { console.log(`❌ Command execution failed: ${command.id} - ${error.message}`); }); // Handle graceful shutdown process.on('SIGINT', async () => { console.log('\n🛑 Stopping command relay service...'); await relayService.stop(); console.log('✅ Service stopped'); process.exit(0); }); // Start service await relayService.start(); // Keep process running process.stdin.resume(); } catch (error) { console.error('❌ Failed to start relay service:', error.message); process.exit(1); } } async stopRelay(args) { console.log('💡 Command relay service usually stopped with Ctrl+C'); console.log('If the service is still running, please find the corresponding process and terminate it manually'); } async relayStatus(args) { try { const fs = require('fs'); const path = require('path'); const stateFile = path.join(__dirname, 'src/data/relay-state.json'); console.log('📊 Command relay service status\n'); // Check email configuration const emailConfig = this.config.getChannel('email'); if (!emailConfig || !emailConfig.enabled) { console.log('❌ Email channel not configured'); return; } console.log('✅ Email configuration enabled'); console.log(`📧 SMTP: ${emailConfig.config.smtp.host}:${emailConfig.config.smtp.port}`); console.log(`📥 IMAP: ${emailConfig.config.imap.host}:${emailConfig.config.imap.port}`); console.log(`📬 Recipient: ${emailConfig.config.to}`); // Check relay status if (fs.existsSync(stateFile)) { const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); console.log(`\n📋 Command queue: ${state.commandQueue?.length || 0} commands`); if (state.commandQueue && state.commandQueue.length > 0) { console.log('\nRecent commands:'); state.commandQueue.slice(-5).forEach(cmd => { const status = cmd.status === 'completed' ? '✅' : cmd.status === 'failed' ? '❌' : cmd.status === 'executing' ? '⏳' : '⏸️'; console.log(` ${status} ${cmd.id}: ${cmd.command.substring(0, 50)}...`); }); } } else { console.log('\n📋 No command history found'); } } catch (error) { console.error('❌ Failed to get status:', error.message); } } async cleanupRelay(args) { try { const fs = require('fs'); const path = require('path'); const stateFile = path.join(__dirname, 'src/data/relay-state.json'); if (!fs.existsSync(stateFile)) { console.log('📋 No cleanup needed, no command history found'); return; } const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); const beforeCount = state.commandQueue?.length || 0; // Clean up completed commands (keep those within 24 hours) const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); state.commandQueue = (state.commandQueue || []).filter(cmd => cmd.status !== 'completed' || new Date(cmd.completedAt || cmd.queuedAt) > cutoff ); const afterCount = state.commandQueue.length; const removedCount = beforeCount - afterCount; fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); console.log(`🧹 Cleanup completed: removed ${removedCount} completed commands`); console.log(`📋 ${afterCount} commands remaining in queue`); } catch (error) { console.error('❌ Cleanup failed:', error.message); } } async handleEditConfig(args) { const { spawn } = require('child_process'); const path = require('path'); const configType = args[0]; if (!configType) { console.log('Available configuration files:'); console.log(' user - User personal configuration (config/user.json)'); console.log(' channels - Notification channel configuration (config/channels.json)'); console.log(' default - Default configuration template (config/default.json)'); console.log(''); console.log('Usage: claude-remote edit-config '); console.log('Example: claude-remote edit-config channels'); return; } const configFiles = { 'user': path.join(__dirname, 'config/user.json'), 'channels': path.join(__dirname, 'config/channels.json'), 'default': path.join(__dirname, 'config/default.json') }; const configFile = configFiles[configType]; if (!configFile) { console.error('❌ Invalid configuration type:', configType); console.log('Available types: user, channels, default'); return; } // Check if file exists const fs = require('fs'); if (!fs.existsSync(configFile)) { console.error('❌ Configuration file does not exist:', configFile); return; } console.log(`📝 Opening configuration file: ${configFile}`); console.log('💡 Save and close the editor after editing to take effect'); console.log(''); // Determine the editor to use const editor = process.env.EDITOR || process.env.VISUAL || this._getDefaultEditor(); try { const editorProcess = spawn(editor, [configFile], { stdio: 'inherit' }); editorProcess.on('close', (code) => { if (code === 0) { console.log('✅ Configuration file saved'); console.log('💡 Run "claude-remote status" to view updated configuration'); } else { console.log('❌ Editor exited abnormally'); } }); editorProcess.on('error', (error) => { console.error('❌ Unable to start editor:', error.message); console.log(''); console.log('💡 You can manually edit the configuration file:'); console.log(` ${configFile}`); }); } catch (error) { console.error('❌ Failed to start editor:', error.message); console.log(''); console.log('💡 You can manually edit the configuration file:'); console.log(` ${configFile}`); } } _getDefaultEditor() { // Determine default editor based on platform if (process.platform === 'win32') { return 'notepad'; } else if (process.platform === 'darwin') { return 'nano'; // Use nano on macOS as most users have it } else { return 'nano'; // Linux default to nano } } async handleSetupEmail(args) { const readline = require('readline'); const fs = require('fs'); const path = require('path'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const question = (prompt) => { return new Promise((resolve) => { rl.question(prompt, resolve); }); }; try { console.log('🚀 Claude-Code-Remote Email Quick Setup Wizard\n'); // Select email provider console.log('Please select your email provider:'); console.log('1. Gmail'); console.log('2. QQ Email'); console.log('3. 163 Email'); console.log('4. Outlook/Hotmail'); console.log('5. Custom'); const providerChoice = await question('\nPlease select (1-5): '); let smtpHost, smtpPort, imapHost, imapPort, secure; switch (providerChoice) { case '1': smtpHost = 'smtp.gmail.com'; smtpPort = 587; imapHost = 'imap.gmail.com'; imapPort = 993; secure = false; console.log('\n📧 Gmail Configuration'); console.log('💡 Need to enable two-factor authentication and generate app password first'); break; case '2': smtpHost = 'smtp.qq.com'; smtpPort = 587; imapHost = 'imap.qq.com'; imapPort = 993; secure = false; console.log('\n📧 QQ Email Configuration'); break; case '3': smtpHost = 'smtp.163.com'; smtpPort = 587; imapHost = 'imap.163.com'; imapPort = 993; secure = false; console.log('\n📧 163 Email Configuration'); break; case '4': smtpHost = 'smtp.live.com'; smtpPort = 587; imapHost = 'imap-mail.outlook.com'; imapPort = 993; secure = false; console.log('\n📧 Outlook Configuration'); break; case '5': console.log('\n📧 Custom Configuration'); smtpHost = await question('SMTP Host: '); smtpPort = parseInt(await question('SMTP Port (default 587): ') || '587'); imapHost = await question('IMAP Host: '); imapPort = parseInt(await question('IMAP Port (default 993): ') || '993'); const secureInput = await question('Use SSL/TLS? (y/n): '); secure = secureInput.toLowerCase() === 'y'; break; default: console.log('❌ Invalid selection'); rl.close(); return; } // Get email account information console.log('\n📝 Please enter email account information:'); const email = await question('Email address: '); const password = await question('Password/App password: '); // Build configuration const emailConfig = { type: "email", enabled: true, config: { smtp: { host: smtpHost, port: smtpPort, secure: secure, auth: { user: email, pass: password } }, imap: { host: imapHost, port: imapPort, secure: true, auth: { user: email, pass: password } }, from: `Claude-Code-Remote <${email}>`, to: email, template: { checkInterval: 30 } } }; // Read existing configuration const channelsFile = path.join(__dirname, 'config/channels.json'); let channels = {}; if (fs.existsSync(channelsFile)) { channels = JSON.parse(fs.readFileSync(channelsFile, 'utf8')); } // Update email configuration channels.email = emailConfig; // Save configuration fs.writeFileSync(channelsFile, JSON.stringify(channels, null, 2)); console.log('\n✅ Email configuration saved!'); console.log('\n🧪 You can now test email functionality:'); console.log(' claude-remote test'); console.log('\n🚀 Start command relay service:'); console.log(' claude-remote relay start'); // Ask if user wants to test immediately const testNow = await question('\nTest email sending now? (y/n): '); if (testNow.toLowerCase() === 'y') { rl.close(); // Reload configuration and test await this.init(); await this.handleTest([]); } else { rl.close(); } } catch (error) { console.error('❌ Configuration failed:', error.message); rl.close(); } } async handleDaemon(args) { const ClaudeCodeRemoteDaemon = require('./src/daemon/taskping-daemon'); const daemon = new ClaudeCodeRemoteDaemon(); const command = args[0]; switch (command) { case 'start': await daemon.start(); break; case 'stop': await daemon.stop(); break; case 'restart': await daemon.restart(); break; case 'status': daemon.showStatus(); break; default: console.log('Usage: claude-remote daemon '); console.log(''); console.log('Commands:'); console.log(' start Start background daemon process'); console.log(' stop Stop background daemon process'); console.log(' restart Restart background daemon process'); console.log(' status View daemon process status'); break; } } async handleCommands(args) { const ClaudeCommandBridge = require('./src/relay/claude-command-bridge'); const bridge = new ClaudeCommandBridge(); const command = args[0]; switch (command) { case 'list': const pending = bridge.getPendingCommands(); console.log(`📋 Pending commands: ${pending.length}\n`); if (pending.length > 0) { pending.forEach((cmd, index) => { console.log(`${index + 1}. ${cmd.id}`); console.log(` Command: ${cmd.command}`); console.log(` Time: ${cmd.timestamp}`); console.log(` Session: ${cmd.sessionId}`); console.log(''); }); } break; case 'status': const status = bridge.getStatus(); console.log('📊 Command bridge status\n'); console.log(`Pending commands: ${status.pendingCommands}`); console.log(`Processed commands: ${status.processedCommands}`); console.log(`Commands directory: ${status.commandsDir}`); console.log(`Response directory: ${status.responseDir}`); if (status.recentCommands.length > 0) { console.log('\nRecent commands:'); status.recentCommands.forEach(cmd => { console.log(` • ${cmd.command} (${cmd.timestamp})`); }); } break; case 'cleanup': bridge.cleanup(); console.log('🧹 Old command files cleaned up'); break; case 'clear': const pending2 = bridge.getPendingCommands(); for (const cmd of pending2) { bridge.markCommandProcessed(cmd.id, 'cancelled', 'Manually cancelled'); } console.log(`🗑️ Cleared ${pending2.length} pending commands`); break; default: console.log('Usage: claude-remote commands '); console.log(''); console.log('Commands:'); console.log(' list Show pending email commands'); console.log(' status Show command bridge status'); console.log(' cleanup Clean up old command files'); console.log(' clear Clear all pending commands'); break; } } async handleTestPaste(args) { const ClipboardAutomation = require('./src/automation/clipboard-automation'); const automation = new ClipboardAutomation(); const testCommand = args.join(' ') || 'echo "Testing email reply auto-paste functionality"'; console.log('🧪 Testing auto-paste functionality'); console.log(`📝 Test command: ${testCommand}`); console.log('\n⚠️ Please ensure Claude Code or Terminal window is open and active'); console.log('⏳ Command will be sent automatically in 3 seconds...\n'); // Countdown for (let i = 3; i > 0; i--) { process.stdout.write(`${i}... `); await new Promise(resolve => setTimeout(resolve, 1000)); } console.log('\n'); try { const success = await automation.sendCommand(testCommand); if (success) { console.log('✅ Command has been auto-pasted!'); console.log('💡 If you don\'t see the effect, please check app permissions and window status'); } else { console.log('❌ Auto-paste failed'); console.log('💡 Please ensure automation permissions are granted and target app is open'); } } catch (error) { console.error('❌ Test failed:', error.message); } } async handleSetupPermissions(args) { const PermissionSetup = require('./setup-permissions'); const setup = new PermissionSetup(); await setup.checkAndSetup(); } async handleTestSimple(args) { const SimpleAutomation = require('./src/automation/simple-automation'); const automation = new SimpleAutomation(); const testCommand = args.join(' ') || 'echo "Testing simple automation functionality"'; console.log('🧪 Testing simple automation functionality'); console.log(`📝 Test command: ${testCommand}`); console.log('\nThis test will:'); console.log('1. 📋 Copy command to clipboard'); console.log('2. 📄 Save command to file'); console.log('3. 🔔 Send notification (including dialog box)'); console.log('4. 🤖 Attempt auto-paste (if permissions granted)'); console.log('\n⏳ Starting test...\n'); try { const success = await automation.sendCommand(testCommand, 'test-session'); if (success) { console.log('✅ Test successful!'); console.log('\n📋 Next steps:'); console.log('1. Check if you received notification'); console.log('2. Check if command was copied to clipboard'); console.log('3. If you see dialog box, you can choose to open command file'); console.log('4. Manually paste to Claude Code (if auto-paste didn\'t work)'); const status = automation.getStatus(); console.log(`\n📄 Command file: ${status.commandFile}`); if (status.commandFileExists) { console.log('💡 You can run "open -t ' + status.commandFile + '" to view command file'); } } else { console.log('❌ Test failed'); } } catch (error) { console.error('❌ Error occurred during test:', error.message); } } async handleTestClaude(args) { const ClaudeAutomation = require('./src/automation/claude-automation'); const automation = new ClaudeAutomation(); const testCommand = args.join(' ') || 'echo "This is an automated test command from email reply"'; console.log('🤖 Testing Claude Code specialized automation'); console.log(`📝 Test command: ${testCommand}`); console.log('\n⚠️ Please ensure:'); console.log(' 1. Claude Code application is open'); console.log(' 2. Or Terminal/iTerm2 etc. terminal applications are open'); console.log(' 3. Necessary accessibility permissions have been granted'); console.log('\n⏳ Full automation test will start in 5 seconds...\n'); // Countdown for (let i = 5; i > 0; i--) { process.stdout.write(`${i}... `); await new Promise(resolve => setTimeout(resolve, 1000)); } console.log('\n🚀 Starting automation...\n'); try { // Check permissions const hasPermission = await automation.requestPermissions(); if (!hasPermission) { console.log('⚠️ Permission check failed, but will still attempt execution...'); } // Execute full automation const success = await automation.sendCommand(testCommand, 'test-session'); if (success) { console.log('✅ Full automation test successful!'); console.log('💡 Command should have been automatically input to Claude Code and started execution'); console.log('🔍 Please check Claude Code window to see if command was received'); } else { console.log('❌ Automation test failed'); console.log('💡 Possible reasons:'); console.log(' • Claude Code or terminal application not found'); console.log(' • Insufficient permissions'); console.log(' • Application not responding'); console.log('\n🔧 Suggestions:'); console.log(' 1. Run "claude-remote setup-permissions" to check permissions'); console.log(' 2. Ensure Claude Code is running in foreground'); console.log(' 3. Try manually clicking input box in Claude Code first'); } } catch (error) { console.error('❌ Error occurred during test:', error.message); } } async handleDiagnose(args) { const AutomationDiagnostic = require('./diagnose-automation'); const diagnostic = new AutomationDiagnostic(); await diagnostic.runDiagnostic(); } showHelp() { console.log(` Claude-Code-Remote - Claude Code Smart Notification System Usage: claude-remote [options] Commands: notify --type Send a notification (completed|waiting) test Test all notification channels status Show system status config Launch configuration manager setup-email Quick email setup wizard edit-config Edit configuration files directly install Install and configure Claude Code hooks relay Manage email command relay service daemon Manage background daemon service commands Manage email commands and bridge test-paste [command] Test automatic paste functionality test-simple [command] Test simple automation (recommended) test-claude [command] Test Claude Code full automation setup-permissions Setup macOS permissions for automation diagnose Diagnose automation issues Options: -h, --help Show this help message Relay Subcommands: relay start Start email command relay service relay stop Stop email command relay service relay status Show relay service status relay cleanup Clean up completed command history Daemon Subcommands: daemon start Start background daemon service daemon stop Stop background daemon service daemon restart Restart background daemon service daemon status Show daemon service status Commands Subcommands: commands list Show pending email commands commands status Show command bridge status commands cleanup Clean up old command files commands clear Clear all pending commands Examples: claude-remote notify --type completed claude-remote test claude-remote setup-email # Quick email setup (recommended) claude-remote edit-config channels # Edit configuration files directly claude-remote config # Interactive configuration claude-remote install claude-remote daemon start # Start background service (recommended) claude-remote daemon status # View service status claude-remote test-claude # Test full automation (recommended) claude-remote commands list # View pending email commands claude-remote relay start # Run in foreground (need to keep window open) For more information, visit: https://github.com/Claude-Code-Remote/Claude-Code-Remote `); } } // Run CLI if this file is executed directly if (require.main === module) { const cli = new ClaudeCodeRemoteCLI(); cli.run().catch(error => { console.error('Fatal error:', error.message); process.exit(1); }); } module.exports = ClaudeCodeRemoteCLI;