diff --git a/.gitignore b/.gitignore index 23c36e2..6f8c7d3 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,11 @@ build/ # Temporary files tmp/ -temp/src/data/sessions/ -src/data/processed-messages.json -src/data/session-map.json -src/data/sessions/*.json +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 diff --git a/README.md b/README.md index 5e1a6a5..63eb6fa 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,47 @@ Works with any email client that supports standard reply functionality: - ✅ Outlook - ✅ Any SMTP-compatible email client +### Advanced Configuration + +**Email Notification Options** + +1. **Subagent Activities in Email** + + By default, email notifications only show the execution trace. You can optionally enable a separate subagent activities summary section: + + ```json + // In your config/config.json + { + "showSubagentActivitiesInEmail": true // Default: false + } + ``` + + When enabled, emails will include: + - **Subagent Activities Summary**: A structured list of all subagent activities + - **Full Execution Trace**: The complete terminal output + + Since the execution trace already contains all information, this feature is disabled by default to keep emails concise. + +2. **Execution Trace Display** + + You can control whether to include the execution trace in email notifications: + + ```json + // In your email channel configuration + { + "email": { + "config": { + "includeExecutionTrace": false // Default: true + } + } + } + ``` + + - When `true` (default): Shows a scrollable execution trace section in emails + - When `false`: Removes the execution trace section entirely from emails + + This is useful if you find the execution trace too verbose or if your email client has issues with scrollable content. + ## 💡 Common Use Cases - **Remote Development**: Start coding at the office, continue from home via email diff --git a/claude-remote.js b/claude-remote.js index 8741f14..6592d5d 100755 --- a/claude-remote.js +++ b/claude-remote.js @@ -155,19 +155,31 @@ class ClaudeCodeRemoteCLI { } } - // For completed notifications, include subagent activities + // For completed notifications, include subagent activities and execution trace if (type === 'completed') { - const SubagentTracker = require('./src/utils/subagent-tracker'); - const tracker = new SubagentTracker(); - const trackingKey = metadata.tmuxSession || 'default'; + const Config = require('./src/core/config'); + const config = new Config(); + config.load(); + const showSubagentActivitiesInEmail = config.get('showSubagentActivitiesInEmail', false); - // Get and format subagent activities - const subagentSummary = tracker.formatActivitiesForEmail(trackingKey); - if (subagentSummary) { - metadata.subagentActivities = subagentSummary; - this.logger.info('Including subagent activities in completion email'); + if (showSubagentActivitiesInEmail) { + const SubagentTracker = require('./src/utils/subagent-tracker'); + const tracker = new SubagentTracker(); + const trackingKey = metadata.tmuxSession || 'default'; - // Clear activities after including them + // Get and format subagent activities + const subagentSummary = tracker.formatActivitiesForEmail(trackingKey); + if (subagentSummary) { + metadata.subagentActivities = subagentSummary; + } + + // Clear activities after including them in the notification + tracker.clearActivities(trackingKey); + } else { + // Always clear activities even if not showing them + const SubagentTracker = require('./src/utils/subagent-tracker'); + const tracker = new SubagentTracker(); + const trackingKey = metadata.tmuxSession || 'default'; tracker.clearActivities(trackingKey); } } @@ -207,11 +219,13 @@ class ClaudeCodeRemoteCLI { // Use TmuxMonitor to capture conversation const tmuxMonitor = new TmuxMonitor(); const conversation = tmuxMonitor.getRecentConversation(currentSession); + const fullTrace = tmuxMonitor.getFullExecutionTrace(currentSession); return { userQuestion: conversation.userQuestion, claudeResponse: conversation.claudeResponse, - tmuxSession: currentSession + tmuxSession: currentSession, + fullExecutionTrace: fullTrace }; } catch (error) { this.logger.debug('Failed to capture conversation:', error.message); diff --git a/config/defaults/config.json b/config/defaults/config.json index 91f4e6a..fe7154d 100644 --- a/config/defaults/config.json +++ b/config/defaults/config.json @@ -6,6 +6,7 @@ }, "enabled": true, "enableSubagentNotifications": false, + "showSubagentActivitiesInEmail": false, "subagentActivityDetail": "medium", "timeout": 5, "customMessages": { diff --git a/config/test-with-subagent.json b/config/test-with-subagent.json new file mode 100644 index 0000000..9c5128f --- /dev/null +++ b/config/test-with-subagent.json @@ -0,0 +1,16 @@ +{ + "language": "zh-CN", + "sound": { + "completed": "Submarine", + "waiting": "Hero" + }, + "enabled": true, + "enableSubagentNotifications": false, + "showSubagentActivitiesInEmail": true, + "subagentActivityDetail": "medium", + "timeout": 5, + "customMessages": { + "completed": null, + "waiting": null + } +} \ No newline at end of file diff --git a/src/channels/email/smtp.js b/src/channels/email/smtp.js index f821b38..ea99e56 100644 --- a/src/channels/email/smtp.js +++ b/src/channels/email/smtp.js @@ -114,10 +114,12 @@ class EmailChannel extends NotificationChannel { const tmuxSession = this._getCurrentTmuxSession(); if (tmuxSession && !notification.metadata) { const conversation = this.tmuxMonitor.getRecentConversation(tmuxSession); + const fullTrace = this.tmuxMonitor.getFullExecutionTrace(tmuxSession); notification.metadata = { userQuestion: conversation.userQuestion || notification.message, claudeResponse: conversation.claudeResponse || notification.message, - tmuxSession: tmuxSession + tmuxSession: tmuxSession, + fullExecutionTrace: fullTrace }; } @@ -287,6 +289,36 @@ class EmailChannel extends NotificationChannel { enhancedSubject = enhancedSubject.replace('{{project}}', projectDir); } + // Check if execution trace should be included + const includeExecutionTrace = this.config.includeExecutionTrace !== false; // Default to true + + // Generate execution trace section HTML + let executionTraceSection = ''; + let executionTraceText = ''; + if (includeExecutionTrace) { + executionTraceSection = ` + +
+
+ $ tail -n 1000 execution.log +
+
+
+ [Execution Trace - Scroll to view] +
+
+
{{fullExecutionTrace}}
+
+
+
`; + + executionTraceText = ` + +====== FULL EXECUTION TRACE ====== +{{fullExecutionTrace}} +==================================`; + } + // Template variable replacement const variables = { project: projectDir, @@ -299,7 +331,11 @@ class EmailChannel extends NotificationChannel { claudeResponse: claudeResponse || notification.message, projectDir: projectDir, shortQuestion: shortQuestion || 'No specific question', - subagentActivities: notification.metadata?.subagentActivities || '' + subagentActivities: notification.metadata?.subagentActivities || '', + executionTraceSection: executionTraceSection, + executionTraceText: executionTraceText, + fullExecutionTrace: notification.metadata?.fullExecutionTrace || + 'No execution trace available. This may occur if the task completed very quickly or if tmux session logging is not enabled.' }; let subject = enhancedSubject; @@ -310,8 +346,15 @@ class EmailChannel extends NotificationChannel { Object.keys(variables).forEach(key => { const placeholder = new RegExp(`{{${key}}}`, 'g'); subject = subject.replace(placeholder, variables[key]); - // Escape HTML entities for HTML content - html = html.replace(placeholder, this._escapeHtml(variables[key])); + + // Special handling for HTML content - don't escape + if (key === 'subagentActivities' || key === 'executionTraceSection') { + html = html.replace(placeholder, variables[key]); + } else { + // Escape HTML entities for other content + html = html.replace(placeholder, this._escapeHtml(variables[key])); + } + // No escaping needed for plain text text = text.replace(placeholder, variables[key]); }); @@ -341,63 +384,51 @@ class EmailChannel extends NotificationChannel {
- -
- $ claude-code status
-
- PROJECT: {{projectDir}}
- SESSION: #{{token}}
- STATUS: ✓ Task Completed
- TIME: {{timestamp}} + +
+
+ $ cat user_request.txt
+
{{userQuestion}}
- -
- $ cat user_input.txt
-
{{userQuestion}}
-
- - -
- $ claude-code execute
-
- [INFO] Processing request...
- [INFO] Executing task... + +
+
+ $ claude-code execute
-
{{claudeResponse}}
-
- [SUCCESS] Task completed successfully ✓ +
+
+ [INFO] Processing request...
+ [INFO] Task execution started at {{timestamp}} +
+
{{claudeResponse}}
+
+ [SUCCESS] Task completed successfully ✓ +
{{subagentActivities}} - -
- $ claude-code help --continue
-
-
→ TO CONTINUE THIS SESSION:
-
+ +
+
+ $ claude-code --help continue +
+
+
TO CONTINUE THIS SESSION:
+
Reply to this email directly with your next instruction.

- Examples:
- • "Add error handling to the function"
- • "Write unit tests for this code"
- • "Optimize the performance" + Examples:
+ "Add error handling to the function"
+ "Write unit tests for this code"
+ "Optimize the performance"
- -
- $ echo $SESSION_INFO
-
- SESSION_ID={{sessionId}}
- EXPIRES_IN=24h
- SECURITY=Do not forward this email
- POWERED_BY=Claude-Code-Remote -
-
+ {{executionTraceSection}}
@@ -415,7 +446,7 @@ Status: {{type}} 🤖 Claude's Response: {{claudeResponse}} -{{subagentActivities}} +{{subagentActivities}}{{executionTraceText}} How to Continue Conversation: To continue conversation with Claude Code, please reply to this email directly and enter your instructions in the email body. @@ -553,7 +584,29 @@ Security Note: Please do not forward this email, session will automatically expi project: 'Claude-Code-Remote-Test', metadata: { test: true, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + userQuestion: 'This is a test notification', + claudeResponse: 'Email notification system is working correctly.', + fullExecutionTrace: `> claude-remote test + +🧪 Testing email notification system... + +[2025-08-01T06:29:28.893Z] [Config] [INFO] Configuration loaded successfully +[2025-08-01T06:29:28.918Z] [Notifier] [INFO] Initialized 2 channels +[2025-08-01T06:29:29.015Z] [Channel:desktop] [INFO] Notification sent successfully +[2025-08-01T06:29:32.880Z] [Channel:email] [INFO] Email sent successfully + +✅ Test completed successfully! + +This is a test trace to demonstrate how the full execution trace will appear in actual usage. +When Claude Code completes a task, this section will contain the complete terminal output including: +- User commands +- Claude's responses +- Subagent activities +- Error messages +- Debug information + +The trace provides complete transparency about what happened during task execution.` } }; diff --git a/src/channels/email/smtp.js.backup b/src/channels/email/smtp.js.backup new file mode 100644 index 0000000..cee0e73 --- /dev/null +++ b/src/channels/email/smtp.js.backup @@ -0,0 +1,640 @@ +/** + * Email Notification Channel + * Sends notifications via email with reply support + */ + +const NotificationChannel = require('../base/channel'); +const nodemailer = require('nodemailer'); +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 EmailChannel extends NotificationChannel { + constructor(config = {}) { + super('email', config); + this.transporter = null; + this.sessionsDir = path.join(__dirname, '../../data/sessions'); + this.templatesDir = path.join(__dirname, '../../assets/email-templates'); + this.sentMessagesPath = config.sentMessagesPath || path.join(__dirname, '../../data/sent-messages.json'); + this.tmuxMonitor = new TmuxMonitor(); + + this._ensureDirectories(); + this._initializeTransporter(); + } + + _escapeHtml(text) { + if (!text) return ''; + const htmlEntities = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, char => htmlEntities[char]); + } + + _ensureDirectories() { + if (!fs.existsSync(this.sessionsDir)) { + fs.mkdirSync(this.sessionsDir, { recursive: true }); + } + if (!fs.existsSync(this.templatesDir)) { + fs.mkdirSync(this.templatesDir, { recursive: 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; + } + + _initializeTransporter() { + if (!this.config.smtp) { + this.logger.warn('SMTP configuration not found'); + return; + } + + try { + this.transporter = nodemailer.createTransport({ + host: this.config.smtp.host, + port: this.config.smtp.port, + secure: this.config.smtp.secure || false, + auth: { + user: this.config.smtp.auth.user, + pass: this.config.smtp.auth.pass + }, + // Add timeout settings + 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'); + } catch (error) { + this.logger.error('Failed to initialize email transporter:', error.message); + } + } + + _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.transporter) { + throw new Error('Email transporter not initialized'); + } + + if (!this.config.to) { + throw new Error('Email recipient not 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); + const fullTrace = this.tmuxMonitor.getFullExecutionTrace(tmuxSession); + notification.metadata = { + userQuestion: conversation.userQuestion || notification.message, + claudeResponse: conversation.claudeResponse || notification.message, + tmuxSession: tmuxSession, + fullExecutionTrace: fullTrace + }; + } + + // Create session record + await this._createSession(sessionId, notification, token); + + // Generate email content + const emailContent = this._generateEmailContent(notification, sessionId, token); + + // Generate unique Message-ID + const messageId = `<${sessionId}-${Date.now()}@claude-code-remote>`; + + const mailOptions = { + from: this.config.from || this.config.smtp.auth.user, + to: this.config.to, + subject: emailContent.subject, + html: emailContent.html, + text: emailContent.text, + messageId: messageId, + // Add custom headers for reply recognition + headers: { + 'X-Claude-Code-Remote-Session-ID': sessionId, + 'X-Claude-Code-Remote-Type': notification.type + } + }; + + try { + const result = await this.transporter.sendMail(mailOptions); + this.logger.info(`Email sent successfully to ${this.config.to}, Session: ${sessionId}`); + + // Track sent message + await this._trackSentMessage(messageId, sessionId, token); + + return true; + } catch (error) { + this.logger.error('Failed to send email:', error.message); + // Clean up failed session + await this._removeSession(sessionId); + return false; + } + } + + async _createSession(sessionId, notification, token) { + const session = { + id: sessionId, + token: token, + type: 'pty', + 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), + cwd: process.cwd(), + notification: { + type: notification.type, + project: notification.project, + message: notification.message + }, + status: 'waiting', + commandCount: 0, + maxCommands: 10 + }; + + const sessionFile = path.join(this.sessionsDir, `${sessionId}.json`); + fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2)); + + // Also save in PTY mapping format + const sessionMapPath = process.env.SESSION_MAP_PATH || path.join(__dirname, '../../data/session-map.json'); + let sessionMap = {}; + if (fs.existsSync(sessionMapPath)) { + try { + sessionMap = JSON.parse(fs.readFileSync(sessionMapPath, 'utf8')); + } catch (e) { + sessionMap = {}; + } + } + + // Use passed tmux session name or detect current session + let tmuxSession = notification.metadata?.tmuxSession || this._getCurrentTmuxSession() || 'claude-code-remote'; + + sessionMap[token] = { + type: 'pty', + createdAt: Math.floor(Date.now() / 1000), + expiresAt: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000), + cwd: process.cwd(), + sessionId: sessionId, + tmuxSession: tmuxSession, + description: `${notification.type} - ${notification.project}` + }; + + // Ensure directory exists + const mapDir = path.dirname(sessionMapPath); + if (!fs.existsSync(mapDir)) { + fs.mkdirSync(mapDir, { recursive: true }); + } + + fs.writeFileSync(sessionMapPath, JSON.stringify(sessionMap, null, 2)); + + this.logger.debug(`Session created: ${sessionId}, Token: ${token}`); + } + + 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 _trackSentMessage(messageId, sessionId, token) { + let sentMessages = { messages: [] }; + + // Read existing data if file exists + if (fs.existsSync(this.sentMessagesPath)) { + try { + sentMessages = JSON.parse(fs.readFileSync(this.sentMessagesPath, 'utf8')); + } catch (e) { + this.logger.warn('Failed to read sent-messages.json, creating new one'); + } + } + + // Add new message + sentMessages.messages.push({ + messageId: messageId, + sessionId: sessionId, + token: token, + type: 'notification', + sentAt: new Date().toISOString() + }); + + // Ensure directory exists + const dir = path.dirname(this.sentMessagesPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Write updated data + fs.writeFileSync(this.sentMessagesPath, JSON.stringify(sentMessages, null, 2)); + this.logger.debug(`Tracked sent message: ${messageId}`); + } + + _generateEmailContent(notification, sessionId, token) { + const template = this._getTemplate(notification.type); + const timestamp = new Date().toLocaleString('zh-CN'); + + // Get project directory name (last level directory) + const projectDir = path.basename(process.cwd()); + + // Extract user question (from notification.metadata if available) + let userQuestion = ''; + let claudeResponse = ''; + + if (notification.metadata) { + userQuestion = notification.metadata.userQuestion || ''; + claudeResponse = notification.metadata.claudeResponse || ''; + } + + // Limit user question length for title + const maxQuestionLength = 30; + const shortQuestion = userQuestion.length > maxQuestionLength ? + userQuestion.substring(0, maxQuestionLength) + '...' : userQuestion; + + // Generate more distinctive title + let enhancedSubject = template.subject; + if (shortQuestion) { + enhancedSubject = enhancedSubject.replace('{{project}}', `${projectDir} | ${shortQuestion}`); + } else { + enhancedSubject = enhancedSubject.replace('{{project}}', projectDir); + } + + // Template variable replacement + const variables = { + project: projectDir, + message: notification.message, + timestamp: timestamp, + sessionId: sessionId, + token: token, + type: notification.type === 'completed' ? 'Task completed' : 'Waiting for input', + userQuestion: userQuestion || 'No specified task', + claudeResponse: claudeResponse || notification.message, + projectDir: projectDir, + shortQuestion: shortQuestion || 'No specific question', + subagentActivities: notification.metadata?.subagentActivities || '', + fullExecutionTrace: notification.metadata?.fullExecutionTrace || + 'No execution trace available. This may occur if the task completed very quickly or if tmux session logging is not enabled.' + }; + + let subject = enhancedSubject; + let html = template.html; + let text = template.text; + + // Replace template variables + Object.keys(variables).forEach(key => { + const placeholder = new RegExp(`{{${key}}}`, 'g'); + subject = subject.replace(placeholder, variables[key]); + // Escape HTML entities for HTML content + html = html.replace(placeholder, this._escapeHtml(variables[key])); + // No escaping needed for plain text + text = text.replace(placeholder, variables[key]); + }); + + return { subject, html, text }; + } + + _getTemplate(type) { + // Default templates + const templates = { + completed: { + subject: '[Claude-Code-Remote #{{token}}] Claude Code Task Completed - {{project}}', + html: ` +
+
+ +
+ + + + + + + +
claude-code-remote@{{project}} - Task Completed
+
+ + +
+ +
+
+ $ cat user_request.txt +
+
{{userQuestion}}
+
+ + +
+
+ $ claude-code execute +
+<<<<<<< HEAD +
+
+ [INFO] Processing request...
+ [INFO] Task execution started at {{timestamp}} +
+
{{claudeResponse}}
+
+ [SUCCESS] Task completed successfully ✓ +======= +
{{claudeResponse}}
+
+ [SUCCESS] Task completed successfully ✓ +
+
+ + {{subagentActivities}} + + +
+ $ claude-code help --continue
+
+
→ TO CONTINUE THIS SESSION:
+
+ Reply to this email directly with your next instruction.

+ Examples:
+ • "Add error handling to the function"
+ • "Write unit tests for this code"
+ • "Optimize the performance" +>>>>>>> upstream/master +
+
+
+ + {{subagentActivities}} + + +
+
+ $ claude-code --help continue +
+
+
TO CONTINUE THIS SESSION:
+
+ Reply to this email directly with your next instruction.

+ Examples:
+ "Add error handling to the function"
+ "Write unit tests for this code"
+ "Optimize the performance" +
+
+
+ + +
+
+ $ tail -n 1000 execution.log +
+
+ + [Click to view full execution trace] + +
+
{{fullExecutionTrace}}
+
+
+
+
+
+
+ `, + text: ` +[Claude-Code-Remote #{{token}}] Claude Code Task Completed - {{projectDir}} | {{shortQuestion}} + +Project: {{projectDir}} +Time: {{timestamp}} +Status: {{type}} + +📝 Your Question: +{{userQuestion}} + +🤖 Claude's Response: +{{claudeResponse}} + +{{subagentActivities}} + +<<<<<<< HEAD +====== FULL EXECUTION TRACE ====== +{{fullExecutionTrace}} +================================== + +======= +>>>>>>> upstream/master +How to Continue Conversation: +To continue conversation with Claude Code, please reply to this email directly and enter your instructions in the email body. + +Example Replies: +• "Please continue optimizing the code" +• "Generate unit tests" +• "Explain the purpose of this function" + +Session ID: {{sessionId}} +Security Note: Please do not forward this email, session will automatically expire after 24 hours + ` + }, + waiting: { + subject: '[Claude-Code-Remote #{{token}}] Claude Code Waiting for Input - {{project}}', + html: ` +
+
+ +
+ + + + + + + +
claude-code-remote@{{project}} - Waiting for Input
+
+ + +
+ +
+ $ claude-code status
+
+ PROJECT: {{projectDir}}
+ SESSION: #{{token}}
+ STATUS: ⏳ Waiting for input
+ TIME: {{timestamp}} +
+
+ + +
+ $ claude-code wait
+
+ [WAITING] Claude needs your input to continue...
+
+
+ {{message}} +
+
+ + +
+ $ claude-code help --respond
+
+
→ ACTION REQUIRED:
+
+ Claude is waiting for your guidance.

+ Reply to this email with your instructions to continue. +
+
+
+ + +
+ $ echo $SESSION_INFO
+
+ SESSION_ID={{sessionId}}
+ EXPIRES_IN=24h
+ SECURITY=Do not forward this email
+ POWERED_BY=Claude-Code-Remote +
+
+
+
+
+ `, + text: ` +[Claude-Code-Remote #{{token}}] Claude Code Waiting for Input - {{projectDir}} + +Project: {{projectDir}} +Time: {{timestamp}} +Status: {{type}} + +⏳ Waiting for Processing: {{message}} + +Claude needs your further guidance. Please reply to this email to tell Claude what to do next. + +Session ID: {{sessionId}} +Security Note: Please do not forward this email, session will automatically expire after 24 hours + ` + } + }; + + return templates[type] || templates.completed; + } + + validateConfig() { + if (!this.config.smtp) { + return { valid: false, error: 'SMTP configuration required' }; + } + + if (!this.config.smtp.host) { + return { valid: false, error: 'SMTP host required' }; + } + + if (!this.config.smtp.auth || !this.config.smtp.auth.user || !this.config.smtp.auth.pass) { + return { valid: false, error: 'SMTP authentication required' }; + } + + if (!this.config.to) { + return { valid: false, error: 'Recipient email required' }; + } + + return { valid: true }; + } + + async test() { + try { + if (!this.transporter) { + throw new Error('Email transporter not initialized'); + } + + // Verify SMTP connection + await this.transporter.verify(); + + // Send test email + const testNotification = { + type: 'completed', + title: 'Claude-Code-Remote Test', + message: 'This is a test email to verify that the email notification function is working properly.', + project: 'Claude-Code-Remote-Test', + metadata: { + test: true, + timestamp: new Date().toISOString(), + userQuestion: 'This is a test notification', + claudeResponse: 'Email notification system is working correctly.', + fullExecutionTrace: `> claude-remote test + +🧪 Testing email notification system... + +[2025-08-01T06:29:28.893Z] [Config] [INFO] Configuration loaded successfully +[2025-08-01T06:29:28.918Z] [Notifier] [INFO] Initialized 2 channels +[2025-08-01T06:29:29.015Z] [Channel:desktop] [INFO] Notification sent successfully +[2025-08-01T06:29:32.880Z] [Channel:email] [INFO] Email sent successfully + +✅ Test completed successfully! + +This is a test trace to demonstrate how the full execution trace will appear in actual usage. +When Claude Code completes a task, this section will contain the complete terminal output including: +- User commands +- Claude's responses +- Subagent activities +- Error messages +- Debug information + +The trace provides complete transparency about what happened during task execution.` + } + }; + + const result = await this._sendImpl(testNotification); + return result; + } catch (error) { + this.logger.error('Email test failed:', error.message); + return false; + } + } + + getStatus() { + const baseStatus = super.getStatus(); + return { + ...baseStatus, + configured: this.validateConfig().valid, + supportsRelay: true, + smtp: { + host: this.config.smtp?.host || 'not configured', + port: this.config.smtp?.port || 'not configured', + secure: this.config.smtp?.secure || false + }, + recipient: this.config.to || 'not configured' + }; + } +} + +module.exports = EmailChannel; \ No newline at end of file diff --git a/src/data/processed-messages.json b/src/data/processed-messages.json deleted file mode 100644 index 5c0b24a..0000000 --- a/src/data/processed-messages.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "id": 1312, - "timestamp": 1754022727524 - }, - { - "id": 1315, - "timestamp": 1754022727524 - }, - { - "id": 1310, - "timestamp": 1754022727524 - }, - { - "id": 1323, - "timestamp": 1754022727524 - }, - { - "id": 1331, - "timestamp": 1754022727524 - }, - { - "id": 1334, - "timestamp": 1754022727524 - }, - { - "id": 1342, - "timestamp": 1754022727524 - }, - { - "id": 1346, - "timestamp": 1754022727524 - }, - { - "id": 1348, - "timestamp": 1754022727524 - }, - { - "id": 180, - "timestamp": 1754022727524 - }, - { - "id": 1691, - "timestamp": 1754022727524 - } -] \ No newline at end of file diff --git a/src/data/sent-messages.json b/src/data/sent-messages.json deleted file mode 100644 index b379c13..0000000 --- a/src/data/sent-messages.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "messages": [] -} \ No newline at end of file diff --git a/src/data/session-map.json b/src/data/session-map.json deleted file mode 100644 index 9e26dfe..0000000 --- a/src/data/session-map.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/src/data/sessions/02bd4449-bdcf-464e-916e-61bc62a18dd2.json b/src/data/sessions/02bd4449-bdcf-464e-916e-61bc62a18dd2.json deleted file mode 100644 index 2836d10..0000000 --- a/src/data/sessions/02bd4449-bdcf-464e-916e-61bc62a18dd2.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "02bd4449-bdcf-464e-916e-61bc62a18dd2", - "token": "0KGM60XO", - "type": "pty", - "created": "2025-07-27T14:29:29.132Z", - "expires": "2025-07-28T14:29:29.132Z", - "createdAt": 1753626569, - "expiresAt": 1753712969, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/09466a43-c495-4a30-ac08-eb425748a28c.json b/src/data/sessions/09466a43-c495-4a30-ac08-eb425748a28c.json deleted file mode 100644 index 42856ac..0000000 --- a/src/data/sessions/09466a43-c495-4a30-ac08-eb425748a28c.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "09466a43-c495-4a30-ac08-eb425748a28c", - "token": "1NTEJPH7", - "type": "pty", - "created": "2025-07-27T14:15:43.425Z", - "expires": "2025-07-28T14:15:43.425Z", - "createdAt": 1753625743, - "expiresAt": 1753712143, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/0caed3af-35ae-4b42-9081-b1a959735bde.json b/src/data/sessions/0caed3af-35ae-4b42-9081-b1a959735bde.json deleted file mode 100644 index 63dff0f..0000000 --- a/src/data/sessions/0caed3af-35ae-4b42-9081-b1a959735bde.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "0caed3af-35ae-4b42-9081-b1a959735bde", - "token": "N9PPKGTO", - "type": "pty", - "created": "2025-07-27T13:56:45.557Z", - "expires": "2025-07-28T13:56:45.557Z", - "createdAt": 1753624605, - "expiresAt": 1753711005, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/0f694f4c-f8a4-476a-946a-3dc057c3bc46.json b/src/data/sessions/0f694f4c-f8a4-476a-946a-3dc057c3bc46.json deleted file mode 100644 index 009627e..0000000 --- a/src/data/sessions/0f694f4c-f8a4-476a-946a-3dc057c3bc46.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "0f694f4c-f8a4-476a-946a-3dc057c3bc46", - "token": "2XQP1N0P", - "type": "pty", - "created": "2025-07-27T14:09:21.440Z", - "expires": "2025-07-28T14:09:21.440Z", - "createdAt": 1753625361, - "expiresAt": 1753711761, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/117a1097-dd97-41ab-a276-e4820adc8da8.json b/src/data/sessions/117a1097-dd97-41ab-a276-e4820adc8da8.json deleted file mode 100644 index 3e835a1..0000000 --- a/src/data/sessions/117a1097-dd97-41ab-a276-e4820adc8da8.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "117a1097-dd97-41ab-a276-e4820adc8da8", - "token": "65S5UGHZ", - "type": "pty", - "created": "2025-07-27T13:54:56.875Z", - "expires": "2025-07-28T13:54:56.875Z", - "createdAt": 1753624496, - "expiresAt": 1753710896, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621.json b/src/data/sessions/2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621.json deleted file mode 100644 index f14d71d..0000000 --- a/src/data/sessions/2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621", - "token": "ONY66DAE", - "type": "pty", - "created": "2025-07-27T08:18:31.722Z", - "expires": "2025-07-28T08:18:31.722Z", - "createdAt": 1753604311, - "expiresAt": 1753690711, - "cwd": "/Users/jessytsui/dev/TaskPing", - "notification": { - "type": "waiting", - "project": "TaskPing", - "message": "[TaskPing] Claude需要您的进一步指导" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec.json b/src/data/sessions/3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec.json deleted file mode 100644 index 5650c34..0000000 --- a/src/data/sessions/3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec", - "token": "7HUMGXOT", - "type": "pty", - "created": "2025-07-27T07:27:44.413Z", - "expires": "2025-07-28T07:27:44.413Z", - "createdAt": 1753601264, - "expiresAt": 1753687664, - "cwd": "/Users/jessytsui/dev/TaskPing", - "notification": { - "type": "completed", - "project": "TaskPing", - "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/633a2687-81e7-456e-9995-3321ce3f3b2b.json b/src/data/sessions/633a2687-81e7-456e-9995-3321ce3f3b2b.json deleted file mode 100644 index 9f9c64b..0000000 --- a/src/data/sessions/633a2687-81e7-456e-9995-3321ce3f3b2b.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "633a2687-81e7-456e-9995-3321ce3f3b2b", - "token": "187JDGZ0", - "type": "pty", - "created": "2025-07-27T14:13:43.575Z", - "expires": "2025-07-28T14:13:43.575Z", - "createdAt": 1753625623, - "expiresAt": 1753712023, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/772628f1-414b-4242-bc8f-660ad53b6c23.json b/src/data/sessions/772628f1-414b-4242-bc8f-660ad53b6c23.json deleted file mode 100644 index d924513..0000000 --- a/src/data/sessions/772628f1-414b-4242-bc8f-660ad53b6c23.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "772628f1-414b-4242-bc8f-660ad53b6c23", - "token": "D8561S3A", - "type": "pty", - "created": "2025-07-27T14:18:24.930Z", - "expires": "2025-07-28T14:18:24.930Z", - "createdAt": 1753625904, - "expiresAt": 1753712304, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e.json b/src/data/sessions/7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e.json deleted file mode 100644 index 20d53bd..0000000 --- a/src/data/sessions/7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e", - "token": "G3QE3STQ", - "type": "pty", - "created": "2025-07-27T13:20:03.954Z", - "expires": "2025-07-28T13:20:03.954Z", - "createdAt": 1753622403, - "expiresAt": 1753708803, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "TaskPing-Test", - "message": "This is a test email to verify that the email notification function is working properly." - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/99132172-7a97-46f7-b282-b22054d6e599.json b/src/data/sessions/99132172-7a97-46f7-b282-b22054d6e599.json deleted file mode 100644 index b0d6e1f..0000000 --- a/src/data/sessions/99132172-7a97-46f7-b282-b22054d6e599.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "99132172-7a97-46f7-b282-b22054d6e599", - "token": "5XO64F9Z", - "type": "pty", - "created": "2025-07-27T14:17:26.966Z", - "expires": "2025-07-28T14:17:26.966Z", - "createdAt": 1753625846, - "expiresAt": 1753712246, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/a3f2b4f9-811e-4721-914f-f025919c2530.json b/src/data/sessions/a3f2b4f9-811e-4721-914f-f025919c2530.json deleted file mode 100644 index ad54ee6..0000000 --- a/src/data/sessions/a3f2b4f9-811e-4721-914f-f025919c2530.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "a3f2b4f9-811e-4721-914f-f025919c2530", - "token": "P9UBHY8L", - "type": "pty", - "created": "2025-07-27T14:25:25.848Z", - "expires": "2025-07-28T14:25:25.848Z", - "createdAt": 1753626325, - "expiresAt": 1753712725, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/a966681d-5cfd-47b9-bb1b-c7ee9655b97b.json b/src/data/sessions/a966681d-5cfd-47b9-bb1b-c7ee9655b97b.json deleted file mode 100644 index d6ade57..0000000 --- a/src/data/sessions/a966681d-5cfd-47b9-bb1b-c7ee9655b97b.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "a966681d-5cfd-47b9-bb1b-c7ee9655b97b", - "token": "QHFI9FIJ", - "type": "pty", - "created": "2025-07-27T09:15:53.533Z", - "expires": "2025-07-28T09:15:53.533Z", - "createdAt": 1753607753, - "expiresAt": 1753694153, - "cwd": "/Users/jessytsui/dev/TaskPing", - "notification": { - "type": "waiting", - "project": "TaskPing", - "message": "[TaskPing] Claude needs your further guidance" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/b2408839-8a64-4a07-8ceb-4a7a82ea5b25.json b/src/data/sessions/b2408839-8a64-4a07-8ceb-4a7a82ea5b25.json deleted file mode 100644 index d11d1b7..0000000 --- a/src/data/sessions/b2408839-8a64-4a07-8ceb-4a7a82ea5b25.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "b2408839-8a64-4a07-8ceb-4a7a82ea5b25", - "token": "Z0Q98XCC", - "type": "pty", - "created": "2025-07-27T13:52:54.736Z", - "expires": "2025-07-28T13:52:54.736Z", - "createdAt": 1753624374, - "expiresAt": 1753710774, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "TaskPing-Test", - "message": "This is a test email to verify that the email notification function is working properly." - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/b8dac307-8b4b-4286-aa73-324b9b659e60.json b/src/data/sessions/b8dac307-8b4b-4286-aa73-324b9b659e60.json deleted file mode 100644 index 2601b0d..0000000 --- a/src/data/sessions/b8dac307-8b4b-4286-aa73-324b9b659e60.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "b8dac307-8b4b-4286-aa73-324b9b659e60", - "token": "NSYKTAWC", - "type": "pty", - "created": "2025-07-27T14:14:10.103Z", - "expires": "2025-07-28T14:14:10.103Z", - "createdAt": 1753625650, - "expiresAt": 1753712050, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/c7b6750f-6246-4ed3-bca5-81201ab980ee.json b/src/data/sessions/c7b6750f-6246-4ed3-bca5-81201ab980ee.json deleted file mode 100644 index 800cf71..0000000 --- a/src/data/sessions/c7b6750f-6246-4ed3-bca5-81201ab980ee.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "c7b6750f-6246-4ed3-bca5-81201ab980ee", - "token": "5CLDW6NQ", - "type": "pty", - "created": "2025-07-27T07:42:04.988Z", - "expires": "2025-07-28T07:42:04.988Z", - "createdAt": 1753602124, - "expiresAt": 1753688524, - "cwd": "/Users/jessytsui/dev/TaskPing", - "notification": { - "type": "completed", - "project": "TaskPing", - "message": "[TaskPing] 任务已完成,Claude正在等待下一步指令" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/c7c5c95d-4541-47f6-b27a-35c0fd563413.json b/src/data/sessions/c7c5c95d-4541-47f6-b27a-35c0fd563413.json deleted file mode 100644 index 5b8a21f..0000000 --- a/src/data/sessions/c7c5c95d-4541-47f6-b27a-35c0fd563413.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "c7c5c95d-4541-47f6-b27a-35c0fd563413", - "token": "TTRQKVM9", - "type": "pty", - "created": "2025-07-27T14:24:05.433Z", - "expires": "2025-07-28T14:24:05.433Z", - "createdAt": 1753626245, - "expiresAt": 1753712645, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/d33e49aa-a58f-46b0-8829-dfef7f474600.json b/src/data/sessions/d33e49aa-a58f-46b0-8829-dfef7f474600.json deleted file mode 100644 index 5e3ff0b..0000000 --- a/src/data/sessions/d33e49aa-a58f-46b0-8829-dfef7f474600.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "d33e49aa-a58f-46b0-8829-dfef7f474600", - "token": "B7R9OR3K", - "type": "pty", - "created": "2025-07-27T14:27:25.292Z", - "expires": "2025-07-28T14:27:25.292Z", - "createdAt": 1753626445, - "expiresAt": 1753712845, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/da40ba76-7047-41e0-95f2-081db87c1b3b.json b/src/data/sessions/da40ba76-7047-41e0-95f2-081db87c1b3b.json deleted file mode 100644 index e89de3b..0000000 --- a/src/data/sessions/da40ba76-7047-41e0-95f2-081db87c1b3b.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "da40ba76-7047-41e0-95f2-081db87c1b3b", - "token": "GR0GED2E", - "type": "pty", - "created": "2025-07-27T14:23:35.122Z", - "expires": "2025-07-28T14:23:35.122Z", - "createdAt": 1753626215, - "expiresAt": 1753712615, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/e6e973b6-20dd-497f-a988-02482af63336.json b/src/data/sessions/e6e973b6-20dd-497f-a988-02482af63336.json deleted file mode 100644 index 9a3594d..0000000 --- a/src/data/sessions/e6e973b6-20dd-497f-a988-02482af63336.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "e6e973b6-20dd-497f-a988-02482af63336", - "token": "YTGT6F6F", - "type": "pty", - "created": "2025-07-27T14:04:00.467Z", - "expires": "2025-07-28T14:04:00.467Z", - "createdAt": 1753625040, - "expiresAt": 1753711440, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/e844d2ae-9098-4528-9e05-e77904a35be3.json b/src/data/sessions/e844d2ae-9098-4528-9e05-e77904a35be3.json deleted file mode 100644 index ef88a99..0000000 --- a/src/data/sessions/e844d2ae-9098-4528-9e05-e77904a35be3.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "e844d2ae-9098-4528-9e05-e77904a35be3", - "token": "GKPSGCBS", - "type": "pty", - "created": "2025-07-27T14:13:38.965Z", - "expires": "2025-07-28T14:13:38.965Z", - "createdAt": 1753625618, - "expiresAt": 1753712018, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/f0d0635b-59f2-45eb-acfc-d649b12fd2d6.json b/src/data/sessions/f0d0635b-59f2-45eb-acfc-d649b12fd2d6.json deleted file mode 100644 index 5c62133..0000000 --- a/src/data/sessions/f0d0635b-59f2-45eb-acfc-d649b12fd2d6.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "f0d0635b-59f2-45eb-acfc-d649b12fd2d6", - "token": "JQAOXCYJ", - "type": "pty", - "created": "2025-07-27T14:26:30.022Z", - "expires": "2025-07-28T14:26:30.022Z", - "createdAt": 1753626390, - "expiresAt": 1753712790, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/sessions/f8f915ee-ab64-471c-b3d2-71cb84d2b5fe.json b/src/data/sessions/f8f915ee-ab64-471c-b3d2-71cb84d2b5fe.json deleted file mode 100644 index 1f6ce6b..0000000 --- a/src/data/sessions/f8f915ee-ab64-471c-b3d2-71cb84d2b5fe.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "f8f915ee-ab64-471c-b3d2-71cb84d2b5fe", - "token": "5NXM173C", - "type": "pty", - "created": "2025-07-27T14:33:54.331Z", - "expires": "2025-07-28T14:33:54.331Z", - "createdAt": 1753626834, - "expiresAt": 1753713234, - "cwd": "/Users/jessytsui/dev/Claude-Code-Remote", - "notification": { - "type": "completed", - "project": "Claude-Code-Remote", - "message": "[Claude-Code-Remote] Task completed, Claude is waiting for next instruction" - }, - "status": "waiting", - "commandCount": 0, - "maxCommands": 10 -} \ No newline at end of file diff --git a/src/data/subagent-activities.json b/src/data/subagent-activities.json deleted file mode 100644 index 9e26dfe..0000000 --- a/src/data/subagent-activities.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/src/utils/tmux-monitor.js b/src/utils/tmux-monitor.js index 73ba423..af7b636 100644 --- a/src/utils/tmux-monitor.js +++ b/src/utils/tmux-monitor.js @@ -6,11 +6,13 @@ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); +const TraceCapture = require('./trace-capture'); class TmuxMonitor { constructor() { this.captureDir = path.join(__dirname, '../data/tmux-captures'); this._ensureCaptureDir(); + this.traceCapture = new TraceCapture(); } _ensureCaptureDir() { @@ -75,7 +77,7 @@ class TmuxMonitor { const allLines = content.split('\n'); const recentLines = allLines.slice(-lines); - return this.extractConversation(recentLines.join('\n')); + return this.extractConversation(recentLines.join('\n'), sessionName); } catch (error) { console.error(`Failed to get conversation for session ${sessionName}:`, error.message); return { userQuestion: '', claudeResponse: '' }; @@ -95,19 +97,172 @@ class TmuxMonitor { stdio: ['ignore', 'pipe', 'ignore'] }); - return this.extractConversation(buffer); + return this.extractConversation(buffer, sessionName); } catch (error) { console.error(`Failed to get tmux buffer for session ${sessionName}:`, error.message); return { userQuestion: '', claudeResponse: '' }; } } + /** + * Get full execution trace from tmux session + * @param {string} sessionName - The tmux session name + * @param {number} lines - Number of lines to retrieve + * @returns {string} - Full execution trace + */ + getFullExecutionTrace(sessionName, lines = 1000) { + try { + let content; + if (!fs.existsSync(path.join(this.captureDir, `${sessionName}.log`))) { + // If no capture file, try to get from tmux buffer + content = this.getFullTraceFromTmuxBuffer(sessionName, lines); + } else { + // Read the capture file + content = fs.readFileSync(path.join(this.captureDir, `${sessionName}.log`), 'utf8'); + } + + // Always filter content to only show from last user input + content = this._filterByTimestamp(content); + + // Clean up the trace by removing the command prompt box + return this._cleanExecutionTrace(content); + } catch (error) { + console.error(`Failed to get full trace for session ${sessionName}:`, error.message); + return ''; + } + } + + /** + * Filter content to only include lines after the last user input + * @param {string} content - The full content + * @param {number} timestamp - Unix timestamp in milliseconds (not used in current implementation) + * @returns {string} - Filtered content + */ + _filterByTimestamp(content, timestamp) { + const lines = content.split('\n'); + let lastUserInputIndex = -1; + + // Find the LAST occurrence of user input (line starting with "> ") + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i]; + // Check for user input pattern: "> " at the start of the line + if (line.startsWith('> ') && line.length > 2) { + lastUserInputIndex = i; + break; + } + } + + // If we found user input, return everything from that point + if (lastUserInputIndex >= 0) { + return lines.slice(lastUserInputIndex).join('\n'); + } + + // If no user input found, return last 100 lines as fallback + return lines.slice(-100).join('\n'); + } + + /** + * Clean execution trace by removing command prompt and status line + * Also removes the complete user input and final Claude response + * @param {string} trace - Raw execution trace + * @returns {string} - Cleaned trace + */ + _cleanExecutionTrace(trace) { + const lines = trace.split('\n'); + const cleanedLines = []; + let inUserInput = false; + let skipNextEmptyLine = false; + let lastClaudeResponseStart = -1; + + // Find where the last Claude response starts + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].startsWith('⏺ ')) { + lastClaudeResponseStart = i; + break; + } + } + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Skip everything from the last Claude response onward + if (lastClaudeResponseStart !== -1 && i >= lastClaudeResponseStart) { + // But we want to show everything BEFORE the last response + break; + } + + // Start of user input + if (line.startsWith('> ')) { + inUserInput = true; + skipNextEmptyLine = true; + continue; + } + + // Still in user input (continuation lines) + if (inUserInput) { + // Check if we've reached the end of user input + if (line.trim() === '' || line.startsWith('⏺')) { + inUserInput = false; + if (skipNextEmptyLine && line.trim() === '') { + skipNextEmptyLine = false; + continue; + } + } else { + continue; // Skip user input continuation lines + } + } + + // Check if we've hit the command prompt box + if (line.includes('╭─') && line.includes('─╮')) { + break; + } + + // Skip empty command prompt lines + if (line.match(/^│\s*>\s*│$/)) { + break; + } + + cleanedLines.push(line); + } + + // Remove empty lines at the beginning and end + while (cleanedLines.length > 0 && cleanedLines[0].trim() === '') { + cleanedLines.shift(); + } + while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') { + cleanedLines.pop(); + } + + return cleanedLines.join('\n'); + } + + /** + * Get full trace from tmux buffer + * @param {string} sessionName - The tmux session name + * @param {number} lines - Number of lines to retrieve + */ + getFullTraceFromTmuxBuffer(sessionName, lines = 1000) { + try { + // Capture the pane contents + const buffer = execSync(`tmux capture-pane -t ${sessionName} -p -S -${lines}`, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'] + }); + + return buffer; + } catch (error) { + console.error(`Failed to get tmux buffer for session ${sessionName}:`, error.message); + return ''; + } + } + /** * Extract user question and Claude response from captured text * @param {string} text - The captured text + * @param {string} sessionName - The tmux session name (optional) * @returns {Object} - { userQuestion, claudeResponse } */ - extractConversation(text) { + extractConversation(text, sessionName = null) { const lines = text.split('\n'); let userQuestion = ''; @@ -116,17 +271,39 @@ class TmuxMonitor { let inResponse = false; // Find the most recent user question and Claude response + let inUserInput = false; + let userQuestionLines = []; + for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Detect user input (line starting with "> " followed by content) if (line.startsWith('> ') && line.length > 2) { - userQuestion = line.substring(2).trim(); + userQuestionLines = [line.substring(2).trim()]; + inUserInput = true; inResponse = false; // Reset response capture responseLines = []; // Clear previous response + + // Record user input timestamp if session name provided + if (sessionName) { + this.traceCapture.recordUserInput(sessionName); + } + continue; } + // Continue capturing multi-line user input + if (inUserInput && !line.startsWith('⏺') && line.length > 0) { + userQuestionLines.push(line); + continue; + } + + // End of user input + if (inUserInput && (line.startsWith('⏺') || line.length === 0)) { + inUserInput = false; + userQuestion = userQuestionLines.join(' '); + } + // Detect Claude response (line starting with "⏺ " or other response indicators) if (line.startsWith('⏺ ') || (inResponse && line.length > 0 && diff --git a/src/utils/trace-capture.js b/src/utils/trace-capture.js new file mode 100644 index 0000000..08def2f --- /dev/null +++ b/src/utils/trace-capture.js @@ -0,0 +1,115 @@ +/** + * Trace Capture Utility + * Tracks user input timestamps for smart execution trace capture + */ + +const fs = require('fs'); +const path = require('path'); + +class TraceCapture { + constructor() { + this.dataDir = path.join(__dirname, '../data'); + this.timestampFile = path.join(this.dataDir, 'user-input-timestamps.json'); + this._ensureDataDir(); + } + + _ensureDataDir() { + if (!fs.existsSync(this.dataDir)) { + fs.mkdirSync(this.dataDir, { recursive: true }); + } + } + + /** + * Load timestamp data + */ + _loadTimestamps() { + try { + if (fs.existsSync(this.timestampFile)) { + const data = fs.readFileSync(this.timestampFile, 'utf8'); + return JSON.parse(data); + } + } catch (error) { + console.error('Failed to load timestamps:', error.message); + } + return {}; + } + + /** + * Save timestamp data + */ + _saveTimestamps(data) { + try { + fs.writeFileSync(this.timestampFile, JSON.stringify(data, null, 2)); + } catch (error) { + console.error('Failed to save timestamps:', error.message); + } + } + + /** + * Record user input timestamp for a session + * @param {string} sessionName - The tmux session name + * @param {number} timestamp - Unix timestamp in milliseconds + */ + recordUserInput(sessionName, timestamp = Date.now()) { + const timestamps = this._loadTimestamps(); + + if (!timestamps[sessionName]) { + timestamps[sessionName] = { + inputs: [] + }; + } + + timestamps[sessionName].inputs.push({ + timestamp: timestamp, + date: new Date(timestamp).toISOString() + }); + + // Keep only last 10 inputs per session to avoid growing too large + if (timestamps[sessionName].inputs.length > 10) { + timestamps[sessionName].inputs = timestamps[sessionName].inputs.slice(-10); + } + + this._saveTimestamps(timestamps); + } + + /** + * Get the most recent user input timestamp for a session + * @param {string} sessionName - The tmux session name + * @returns {number|null} - Unix timestamp or null if not found + */ + getLastUserInputTime(sessionName) { + const timestamps = this._loadTimestamps(); + + if (timestamps[sessionName] && timestamps[sessionName].inputs.length > 0) { + const lastInput = timestamps[sessionName].inputs[timestamps[sessionName].inputs.length - 1]; + return lastInput.timestamp; + } + + return null; + } + + /** + * Clean up old session data (older than 7 days) + */ + cleanup() { + const timestamps = this._loadTimestamps(); + const now = Date.now(); + const sevenDaysAgo = now - (7 * 24 * 60 * 60 * 1000); + + for (const sessionName in timestamps) { + const session = timestamps[sessionName]; + + // Remove old inputs + session.inputs = session.inputs.filter(input => input.timestamp > sevenDaysAgo); + + // Remove session if no inputs remain + if (session.inputs.length === 0) { + delete timestamps[sessionName]; + } + } + + this._saveTimestamps(timestamps); + } +} + +module.exports = TraceCapture; \ No newline at end of file