2025-07-27 15:27:24 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-28 02:21:38 +08:00
|
|
|
|
* Claude-Code-Remote Unattended Remote Control Setup Assistant
|
2025-07-27 15:27:24 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const { exec, spawn } = require('child_process');
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
|
|
class RemoteControlSetup {
|
|
|
|
|
|
constructor(sessionName = null) {
|
2025-07-28 02:21:38 +08:00
|
|
|
|
this.sessionName = sessionName || 'claude-code-remote';
|
|
|
|
|
|
this.claudeCodeRemoteHome = this.findClaudeCodeRemoteHome();
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 02:21:38 +08:00
|
|
|
|
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;
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 02:21:38 +08:00
|
|
|
|
// If running from the Claude-Code-Remote directory, use current directory
|
2025-07-27 15:27:24 +08:00
|
|
|
|
if (fs.existsSync(path.join(__dirname, 'package.json'))) {
|
|
|
|
|
|
const packagePath = path.join(__dirname, 'package.json');
|
|
|
|
|
|
try {
|
|
|
|
|
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
2025-07-28 02:21:38 +08:00
|
|
|
|
if (packageJson.name && packageJson.name.toLowerCase().includes('claude-code-remote')) {
|
2025-07-27 15:27:24 +08:00
|
|
|
|
return __dirname;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// Continue searching
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 02:21:38 +08:00
|
|
|
|
// Search for Claude-Code-Remote in common locations
|
2025-07-27 15:27:24 +08:00
|
|
|
|
const commonPaths = [
|
2025-07-28 02:21:38 +08:00
|
|
|
|
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'),
|
2025-07-27 15:27:24 +08:00
|
|
|
|
__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'));
|
2025-07-28 02:21:38 +08:00
|
|
|
|
if (packageJson.name && packageJson.name.toLowerCase().includes('claude-code-remote')) {
|
2025-07-27 15:27:24 +08:00
|
|
|
|
return searchPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// Continue searching
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If not found, use current directory as fallback
|
|
|
|
|
|
return __dirname;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async setup() {
|
2025-07-28 02:21:38 +08:00
|
|
|
|
console.log('🚀 Claude-Code-Remote Unattended Remote Control Setup\n');
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('🎯 Goal: Remote access via mobile phone → Home computer Claude Code automatically executes commands\n');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// 1. Check tmux
|
2025-07-27 15:27:24 +08:00
|
|
|
|
await this.checkAndInstallTmux();
|
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// 2. Check Claude CLI
|
2025-07-27 15:27:24 +08:00
|
|
|
|
await this.checkClaudeCLI();
|
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// 3. Setup Claude tmux session
|
2025-07-27 15:27:24 +08:00
|
|
|
|
await this.setupClaudeSession();
|
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// 4. Session creation complete
|
|
|
|
|
|
console.log('\n4️⃣ Session creation complete');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// 5. Provide usage guide
|
2025-07-27 15:27:24 +08:00
|
|
|
|
this.showUsageGuide();
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.error('❌ Error occurred during setup:', error.message);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async checkAndInstallTmux() {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('1️⃣ Checking tmux installation status...');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
exec('which tmux', (error, stdout) => {
|
|
|
|
|
|
if (error) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('❌ tmux not installed');
|
|
|
|
|
|
console.log('📦 Installing tmux...');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
|
|
|
|
|
exec('brew install tmux', (installError, installStdout, installStderr) => {
|
|
|
|
|
|
if (installError) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('❌ tmux installation failed, please install manually:');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
console.log(' brew install tmux');
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(' or download from https://github.com/tmux/tmux');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
} else {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('✅ tmux installation successful');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(`✅ tmux already installed: ${stdout.trim()}`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async checkClaudeCLI() {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('\n2️⃣ Checking Claude CLI status...');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
exec('which claude', (error, stdout) => {
|
|
|
|
|
|
if (error) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('❌ Claude CLI not found');
|
|
|
|
|
|
console.log('📦 Please install Claude CLI:');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
console.log(' npm install -g @anthropic-ai/claude-code');
|
|
|
|
|
|
} else {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(`✅ Claude CLI installed: ${stdout.trim()}`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// Check version
|
2025-07-27 15:27:24 +08:00
|
|
|
|
exec('claude --version', (versionError, versionStdout) => {
|
|
|
|
|
|
if (!versionError) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(`📋 Version: ${versionStdout.trim()}`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async setupClaudeSession() {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('\n3️⃣ Setting up Claude tmux session...');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// Check if session already exists
|
2025-07-27 15:27:24 +08:00
|
|
|
|
exec(`tmux has-session -t ${this.sessionName} 2>/dev/null`, (checkError) => {
|
|
|
|
|
|
if (!checkError) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('⚠️ Claude tmux session already exists');
|
|
|
|
|
|
console.log('🔄 Recreating session? (will kill existing session)');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// For simplicity, recreate directly
|
2025-07-27 15:27:24 +08:00
|
|
|
|
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) {
|
2025-07-28 02:21:38 +08:00
|
|
|
|
// Use Claude-Code-Remote home directory as working directory
|
|
|
|
|
|
const workingDir = this.claudeCodeRemoteHome;
|
2025-07-27 15:27:24 +08:00
|
|
|
|
const command = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" clauderun`;
|
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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)`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
|
|
|
|
|
exec(command, (error, stdout, stderr) => {
|
|
|
|
|
|
if (error) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(`❌ Session creation failed: ${error.message}`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
if (stderr) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(`Error details: ${stderr}`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// If clauderun fails, try using full path command
|
|
|
|
|
|
console.log('🔄 Trying full path command...');
|
2025-07-28 14:44:37 +08:00
|
|
|
|
const claudePath = process.env.CLAUDE_CLI_PATH || 'claude';
|
|
|
|
|
|
const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" ${claudePath} --dangerously-skip-permissions`;
|
2025-07-27 15:27:24 +08:00
|
|
|
|
exec(fallbackCommand, (fallbackError) => {
|
|
|
|
|
|
if (fallbackError) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(`❌ Full path command also failed: ${fallbackError.message}`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
} else {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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)`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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)`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async testRemoteInjection() {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showUsageGuide() {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('\n🎉 Setup complete! Unattended remote control is ready\n');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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');
|
2025-07-28 02:21:38 +08:00
|
|
|
|
console.log('3. 📱 Receive Claude-Code-Remote email notifications on mobile');
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
console.log(' node claude-control.js --session backend');
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(' Email replies will automatically route to corresponding session\n');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('📱 Email testing:');
|
|
|
|
|
|
console.log(' Token will include session information, automatically routing to correct tmux session');
|
2025-07-28 14:44:37 +08:00
|
|
|
|
console.log(` Recipient email: ${process.env.EMAIL_TO}`);
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(' Reply with command: echo "Remote control test"\n');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
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');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('✅ Now you can achieve true unattended remote control! 🎯');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// Quick session restart method
|
2025-07-27 15:27:24 +08:00
|
|
|
|
async quickRestart() {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('🔄 Quick restart Claude session...');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
this.killAndCreateSession(() => {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log('✅ Claude session restarted');
|
2025-07-27 15:27:24 +08:00
|
|
|
|
resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// Command line parameter processing
|
2025-07-27 15:27:24 +08:00
|
|
|
|
if (require.main === module) {
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
|
2025-07-27 17:17:15 +08:00
|
|
|
|
// Parse session name parameter
|
2025-07-27 15:27:24 +08:00
|
|
|
|
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) {
|
2025-07-27 17:17:15 +08:00
|
|
|
|
console.log(`🎛️ Using custom session name: ${sessionName}`);
|
2025-07-27 15:27:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (args.includes('--restart')) {
|
|
|
|
|
|
setup.quickRestart();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setup.setup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = RemoteControlSetup;
|