Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Claude Code Remote Email Configuration
# Claude Code Remote Email Configuration Example
# Copy this file to .env and configure with your actual values

# ===== SMTP 发送邮件配置 =====
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_PORT=465
SMTP_SECURE=true
[email protected]
SMTP_PASS=your-app-password

# 发件人信息
# 发件人信息 (可选,默认使用 SMTP_USER)
[email protected]
EMAIL_FROM_NAME=Claude Code Remote 通知系统

Expand All @@ -20,19 +21,19 @@ IMAP_PASS=your-app-password

# ===== 邮件路由配置 =====
# 接收通知的邮箱地址
EMAIL_TO=your-notification-[email protected]
[email protected]

# 允许发送命令的邮箱地址(安全白名单)
ALLOWED_SENDERS=your-notification-[email protected]
[email protected]

# ===== 系统配置 =====
# 会话映射文件路径 (请替换为你的实际路径)
SESSION_MAP_PATH=/Users/your-username/path/to/Claude-Code-Remote/src/data/session-map.json
# 会话映射文件路径
SESSION_MAP_PATH=/path/to/your/project/src/data/session-map.json

# 运行模式:pty 或 tmux
INJECTION_MODE=pty

# Claude CLI 路径(默认使用系统PATH中的claude)
# Claude CLI 路径(可选,默认使用系统PATH中的claude)
CLAUDE_CLI_PATH=claude

# 日志级别:debug, info, warn, error
Expand All @@ -41,9 +42,55 @@ LOG_LEVEL=info
# 是否记录PTY输出(调试用)
PTY_OUTPUT_LOG=false

# ===== 超时配置 =====
# 命令执行超时时间(毫秒)
COMMAND_TIMEOUT=10000

# SMTP 连接超时时间(毫秒)
SMTP_TIMEOUT=10000

# 通知超时时间(毫秒)
NOTIFICATION_TIMEOUT=3000

# 通知显示时间(毫秒)
NOTIFICATION_DISPLAY_TIME=10000

# ===== 邮件模板配置 =====
# 邮件检查间隔(秒)
CHECK_INTERVAL=30
CHECK_INTERVAL=20

# 会话超时时间(小时)
SESSION_TIMEOUT=24
SESSION_TIMEOUT=24

# ===== 测试配置(可选)=====
# 测试邮件使用的固定令牌(可选,默认动态生成)
TEST_TOKEN=

# Gmail 应用密码(用于测试脚本,可选)
GMAIL_APP_PASSWORD=

# ===== Gmail 配置说明 =====
# 1. 启用两步验证: https://myaccount.google.com/security
# 2. 生成应用密码: https://myaccount.google.com/apppasswords
# 3. 将生成的16位密码填入 SMTP_PASS 和 IMAP_PASS
# 4. 确保 SMTP_PORT=465 和 SMTP_SECURE=true (推荐SSL连接)

# ===== 其他邮件服务商配置示例 =====
# QQ邮箱:
# SMTP_HOST=smtp.qq.com
# SMTP_PORT=587 或 465
# IMAP_HOST=imap.qq.com
# IMAP_PORT=993

# 163邮箱:
# SMTP_HOST=smtp.163.com
# SMTP_PORT=587 或 465
# IMAP_HOST=imap.163.com
# IMAP_PORT=993

# Outlook:
# SMTP_HOST=smtp.live.com
# SMTP_PORT=587
# IMAP_HOST=imap-mail.outlook.com
# IMAP_PORT=993
EOF < /dev/null
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ tmp/
temp/src/data/sessions/
src/data/processed-messages.json
src/data/session-map.json
src/data/sessions/*.json
5 changes: 3 additions & 2 deletions claude-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ class RemoteControlSetup {
}
// If clauderun fails, try using full path command
console.log('🔄 Trying full path command...');
const fallbackCommand = `tmux new-session -d -s ${this.sessionName} -c "${workingDir}" /Users/jessytsui/.nvm/versions/node/v18.17.0/bin/claude --dangerously-skip-permissions`;
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}`);
Expand Down Expand Up @@ -234,7 +235,7 @@ class RemoteControlSetup {

console.log('📱 Email testing:');
console.log(' Token will include session information, automatically routing to correct tmux session');
console.log(' Recipient email: [email protected]');
console.log(` Recipient email: ${process.env.EMAIL_TO}`);
console.log(' Reply with command: echo "Remote control test"\n');

console.log('🚨 Important reminders:');
Expand Down
3 changes: 3 additions & 0 deletions claude-remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* 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');
Expand Down
27 changes: 1 addition & 26 deletions config/channels.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,7 @@
},
"email": {
"type": "email",
"enabled": true,
"config": {
"smtp": {
"host": "smtp.feishu.cn",
"port": 465,
"secure": true,
"auth": {
"user": "[email protected]",
"pass": "kKgS3tNReRTL3RQC"
}
},
"imap": {
"host": "imap.feishu.cn",
"port": 993,
"secure": true,
"auth": {
"user": "[email protected]",
"pass": "kKgS3tNReRTL3RQC"
}
},
"from": "Claude-Code-Remote Notification System <[email protected]>",
"to": "[email protected]",
"template": {
"checkInterval": 30
}
}
"enabled": true
},
"discord": {
"type": "chat",
Expand Down
39 changes: 30 additions & 9 deletions send-test-reply.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,45 @@ require('dotenv').config();
async function sendTestReply() {
console.log('📧 Sending test email reply...\n');

// Create test SMTP transporter (using Gmail)
// Create test SMTP transporter (using environment variables)
const transporter = nodemailer.createTransport({
service: 'gmail',
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: parseInt(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: '[email protected]',
pass: process.env.GMAIL_APP_PASSWORD || 'your-app-password'
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});

// Use latest token
const testToken = 'V5UPZ1UE'; // Latest token from session-map.json
// 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: '[email protected]',
to: '[email protected]',
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: '[email protected]'
replyTo: process.env.EMAIL_TO || process.env.ALLOWED_SENDERS
};

try {
Expand Down
6 changes: 3 additions & 3 deletions src/channels/email/smtp.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ class EmailChannel extends NotificationChannel {
pass: this.config.smtp.auth.pass
},
// Add timeout settings
connectionTimeout: 10000,
greetingTimeout: 10000,
socketTimeout: 10000
connectionTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000,
greetingTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000,
socketTimeout: parseInt(process.env.SMTP_TIMEOUT) || 10000
});

this.logger.debug('Email transporter initialized');
Expand Down
8 changes: 5 additions & 3 deletions src/channels/local/desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ class DesktopChannel extends NotificationChannel {
// Try terminal-notifier first
try {
const cmd = `terminal-notifier -title "${title}" -message "${message}" -sound "${sound}" -group "claude-code-remote"`;
execSync(cmd, { timeout: 3000 });
execSync(cmd, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 });
return true;
} catch (e) {
// Fallback to osascript
const script = `display notification "${message}" with title "${title}"`;
execSync(`osascript -e '${script}'`, { timeout: 3000 });
execSync(`osascript -e '${script}'`, { timeout: parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000 });

// Play sound separately
this._playSound(sound);
Expand All @@ -63,7 +63,9 @@ class DesktopChannel extends NotificationChannel {

_sendLinux(title, message, sound) {
try {
execSync(`notify-send "${title}" "${message}" -t 10000`, { timeout: 3000 });
const notificationTimeout = parseInt(process.env.NOTIFICATION_TIMEOUT) || 3000;
const displayTime = parseInt(process.env.NOTIFICATION_DISPLAY_TIME) || 10000;
execSync(`notify-send "${title}" "${message}" -t ${displayTime}`, { timeout: notificationTimeout });
this._playSound(sound);
return true;
} catch (error) {
Expand Down
30 changes: 21 additions & 9 deletions src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,31 @@ class ConfigManager {
},
email: {
type: 'email',
enabled: false,
enabled: process.env.SMTP_USER ? true : false,
config: {
smtp: {
host: '',
port: 587,
secure: false,
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 || ''
}
},
imap: {
host: process.env.IMAP_HOST || 'imap.gmail.com',
port: parseInt(process.env.IMAP_PORT) || 993,
secure: process.env.IMAP_SECURE !== 'false',
auth: {
user: '',
pass: ''
user: process.env.IMAP_USER || process.env.SMTP_USER || '',
pass: process.env.IMAP_PASS || process.env.SMTP_PASS || ''
}
},
from: '',
to: []
from: process.env.EMAIL_FROM || `${process.env.EMAIL_FROM_NAME || 'Claude Code Remote'} <${process.env.SMTP_USER}>`,
to: process.env.EMAIL_TO || '',
template: {
checkInterval: parseInt(process.env.CHECK_INTERVAL) || 30
}
}
},
discord: {
Expand Down Expand Up @@ -125,7 +137,7 @@ class ConfigManager {
try {
if (fs.existsSync(this.channelsConfigPath)) {
const fileChannels = JSON.parse(fs.readFileSync(this.channelsConfigPath, 'utf8'));
this._channels = { ...this._channels, ...fileChannels };
this._channels = this._deepMerge(this._channels, fileChannels);
}
} catch (error) {
this.logger.warn('Failed to load channels config:', error.message);
Expand Down
11 changes: 5 additions & 6 deletions src/relay/relay-pty.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,11 @@ function isAllowed(fromAddress) {
return ALLOWED_SENDERS.some(allowed => addr.includes(allowed));
}

// Extract TaskPing token from subject
// Extract Claude-Code-Remote token from subject
function extractTokenFromSubject(subject = '') {
const patterns = [
/\[TaskPing\s+#([A-Za-z0-9_-]+)\]/,
/\[TaskPing\s+([A-Za-z0-9_-]+)\]/,
/TaskPing:\s*([A-Za-z0-9_-]+)/i
/\[Claude-Code-Remote\s+#([A-Z0-9]+)\]/,
/Re:\s*\[Claude-Code-Remote\s+#([A-Z0-9]+)\]/
];

for (const pattern of patterns) {
Expand All @@ -118,7 +117,7 @@ function cleanEmailText(text = '') {
line.includes('On') && line.includes('wrote:') ||
line.includes('Session ID:') ||
line.includes('Session ID:') ||
line.includes('<[email protected]>') ||
line.includes(`<${process.env.SMTP_USER}>`) ||
line.includes('Claude-Code-Remote Notification System') ||
line.includes('on 2025') && line.includes('wrote:') ||
line.match(/^>.*/) || // Quote lines start with >
Expand Down Expand Up @@ -161,7 +160,7 @@ function cleanEmailText(text = '') {

// Skip remaining email quotes
if (trimmedLine.includes('Claude-Code-Remote Notification System') ||
trimmedLine.includes('<[email protected]>') ||
trimmedLine.includes(`<${process.env.SMTP_USER}>`) ||
trimmedLine.includes('on 2025')) {
continue;
}
Expand Down
Loading