From 61dd2e30c396a994966da2f5c5c0d23f6f516147 Mon Sep 17 00:00:00 2001 From: Song-Ze Yu <125909247+vaclisinc@users.noreply.github.com> Date: Sat, 2 Aug 2025 04:07:34 +0800 Subject: [PATCH] Add full execution trace in email notifications (#11) (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add configuration option to disable subagent notifications - Added 'enableSubagentNotifications' config option (default: false) - Modified notification handler to check config before sending subagent notifications - Created documentation explaining the feature - Updated README with note about subagent notifications This addresses the issue where frequent subagent notifications can be distracting. Users can now control whether they receive notifications when subagents stop/start. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Add subagent activity tracking in completion emails - Track subagent activities instead of sending individual notifications - Include subagent activity summary in completion emails - Update email templates to display subagent activities - Add SubagentTracker utility to manage activity tracking - Update documentation to explain the new behavior This provides a better user experience by: 1. Reducing notification noise from frequent subagent activities 2. Still providing full visibility into what subagents did 3. Consolidating all information in the completion email 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Improve subagent activity details and documentation - Enhanced activity capture to include up to 1000 characters (was 200) - Improved email display format with better visual separation - Added detection for initialization-only captures with helpful message - Added configuration option for activity detail level - Created comprehensive documentation explaining the timing limitation - Added visual indicators for processing status This addresses the issue where subagent outputs were truncated or only showed initialization messages. Users now get better visibility into what subagents are doing, with clear indication when full output is available in tmux. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * delete redundant files and modify README * Fix HTML escaping in email notifications - Add _escapeHtml function to properly escape HTML entities in emails - Escape user-generated content to prevent HTML tags from being hidden - Fix issue where and other HTML-like strings disappeared in emails - Apply escaping to both main email content and subagent activity summaries 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Add full execution trace to email notifications - Add getFullExecutionTrace method to capture complete terminal output - Include execution trace in a scrollable section within emails - Add CSS styling for visible scrollbars on desktop - Clean trace output by removing command prompt boxes - Add fallback message when trace is not available This addresses issue #11 - providing transparency about task execution 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Redesign email notification UI with improved terminal aesthetics - Reorganize content hierarchy: user request at top, response second, trace at bottom - Preserve terminal-style commands ($ cat, $ claude-code execute, etc.) - Remove redundant session info footer - Implement smart execution trace capture from user input to completion - Add TraceCapture utility to track user input timestamps - Improve visual hierarchy while maintaining terminal aesthetics - Use border colors (orange for user, green for success) for better distinction - Make execution trace collapsible and de-emphasized This improves readability while preserving the terminal charm of the project. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix execution trace filtering to show only recent activity - Change filter logic to find LAST user input instead of first - Search backwards through content to find most recent "> " prompt - Only include content from that point forward - Add fallback to show last 100 lines if no user input found - This ensures trace shows only relevant recent execution, not entire history 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Remove duplicate content from execution trace - Skip the first user input line (already shown in main content) - Skip the last Claude response (already shown in main content) - Only show intermediate execution steps and tool usage - Clean up empty lines at beginning and end - This avoids redundancy and focuses trace on execution details 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix execution trace to properly remove complete user input and output - Track multi-line user input and skip all continuation lines - Detect when user input ends (empty line or next command) - Stop before the last Claude response to avoid truncation - Only show intermediate execution steps between input and output This ensures the trace shows the complete execution process without duplicating content already displayed in the main sections. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix multi-line user input capture and trace filtering - Capture complete multi-line user input (not just first line) - Join continuation lines with spaces for proper display - Preserve all execution details in trace (tool calls, outputs) - Only skip user input and final response, keep everything in between * Add configuration toggle for subagent activities in email - Added 'showSubagentActivitiesInEmail' config option (default: false) - Modified claude-remote.js to check config before including subagent activities - Created documentation explaining the configuration - Allows users to choose between concise emails (execution trace only) or detailed emails (both summaries) This addresses the redundancy between execution trace and subagent activities summary, giving users control over email content. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix Git merge conflict in email template and document subagent config - Removed Git merge conflict markers from email text template - Added documentation for showSubagentActivitiesInEmail config in README - Explained that subagent activities are disabled by default for concise emails 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Update README changelog for PR #10 and issue #11 - Added entry for subagent notifications configuration (PR #10) - Added entry for execution trace feature (issue #11) - Maintained chronological order in changelog 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * delete redundant files * fix #15: add local database into .gitignore * Fix execution trace display in email notifications - Remove collapsible details tag for better email client compatibility - Add configuration option to toggle execution trace display - Fix HTML escaping issue for executionTraceSection variable 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Remove data files from git tracking These session-specific data files should not be tracked in version control as they are machine-specific and cause issues when pulling on other machines. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix typo(?) in changelog (#13) --------- Co-authored-by: Claude Co-authored-by: Naich Co-authored-by: JessyTsui <51992423+JessyTsui@users.noreply.github.com> --- .gitignore | 12 +- README.md | 41 ++ claude-remote.js | 36 +- config/defaults/config.json | 1 + config/test-with-subagent.json | 16 + src/channels/email/smtp.js | 151 +++-- src/channels/email/smtp.js.backup | 640 ++++++++++++++++++ src/data/processed-messages.json | 46 -- src/data/sent-messages.json | 3 - src/data/session-map.json | 1 - .../02bd4449-bdcf-464e-916e-61bc62a18dd2.json | 18 - .../09466a43-c495-4a30-ac08-eb425748a28c.json | 18 - .../0caed3af-35ae-4b42-9081-b1a959735bde.json | 18 - .../0f694f4c-f8a4-476a-946a-3dc057c3bc46.json | 18 - .../117a1097-dd97-41ab-a276-e4820adc8da8.json | 18 - .../2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621.json | 18 - .../3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec.json | 18 - .../633a2687-81e7-456e-9995-3321ce3f3b2b.json | 18 - .../772628f1-414b-4242-bc8f-660ad53b6c23.json | 18 - .../7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e.json | 18 - .../99132172-7a97-46f7-b282-b22054d6e599.json | 18 - .../a3f2b4f9-811e-4721-914f-f025919c2530.json | 18 - .../a966681d-5cfd-47b9-bb1b-c7ee9655b97b.json | 18 - .../b2408839-8a64-4a07-8ceb-4a7a82ea5b25.json | 18 - .../b8dac307-8b4b-4286-aa73-324b9b659e60.json | 18 - .../c7b6750f-6246-4ed3-bca5-81201ab980ee.json | 18 - .../c7c5c95d-4541-47f6-b27a-35c0fd563413.json | 18 - .../d33e49aa-a58f-46b0-8829-dfef7f474600.json | 18 - .../da40ba76-7047-41e0-95f2-081db87c1b3b.json | 18 - .../e6e973b6-20dd-497f-a988-02482af63336.json | 18 - .../e844d2ae-9098-4528-9e05-e77904a35be3.json | 18 - .../f0d0635b-59f2-45eb-acfc-d649b12fd2d6.json | 18 - .../f8f915ee-ab64-471c-b3d2-71cb84d2b5fe.json | 18 - src/data/subagent-activities.json | 1 - src/utils/tmux-monitor.js | 185 ++++- src/utils/trace-capture.js | 115 ++++ 36 files changed, 1129 insertions(+), 533 deletions(-) create mode 100644 config/test-with-subagent.json create mode 100644 src/channels/email/smtp.js.backup delete mode 100644 src/data/processed-messages.json delete mode 100644 src/data/sent-messages.json delete mode 100644 src/data/session-map.json delete mode 100644 src/data/sessions/02bd4449-bdcf-464e-916e-61bc62a18dd2.json delete mode 100644 src/data/sessions/09466a43-c495-4a30-ac08-eb425748a28c.json delete mode 100644 src/data/sessions/0caed3af-35ae-4b42-9081-b1a959735bde.json delete mode 100644 src/data/sessions/0f694f4c-f8a4-476a-946a-3dc057c3bc46.json delete mode 100644 src/data/sessions/117a1097-dd97-41ab-a276-e4820adc8da8.json delete mode 100644 src/data/sessions/2a8622dd-a0e9-4f4d-9cc8-a3bb432e9621.json delete mode 100644 src/data/sessions/3248adc2-eb7b-4eb2-a57c-b9ce320cb4ec.json delete mode 100644 src/data/sessions/633a2687-81e7-456e-9995-3321ce3f3b2b.json delete mode 100644 src/data/sessions/772628f1-414b-4242-bc8f-660ad53b6c23.json delete mode 100644 src/data/sessions/7f6e11b3-0ac9-44b1-a75f-d3a15a8ec46e.json delete mode 100644 src/data/sessions/99132172-7a97-46f7-b282-b22054d6e599.json delete mode 100644 src/data/sessions/a3f2b4f9-811e-4721-914f-f025919c2530.json delete mode 100644 src/data/sessions/a966681d-5cfd-47b9-bb1b-c7ee9655b97b.json delete mode 100644 src/data/sessions/b2408839-8a64-4a07-8ceb-4a7a82ea5b25.json delete mode 100644 src/data/sessions/b8dac307-8b4b-4286-aa73-324b9b659e60.json delete mode 100644 src/data/sessions/c7b6750f-6246-4ed3-bca5-81201ab980ee.json delete mode 100644 src/data/sessions/c7c5c95d-4541-47f6-b27a-35c0fd563413.json delete mode 100644 src/data/sessions/d33e49aa-a58f-46b0-8829-dfef7f474600.json delete mode 100644 src/data/sessions/da40ba76-7047-41e0-95f2-081db87c1b3b.json delete mode 100644 src/data/sessions/e6e973b6-20dd-497f-a988-02482af63336.json delete mode 100644 src/data/sessions/e844d2ae-9098-4528-9e05-e77904a35be3.json delete mode 100644 src/data/sessions/f0d0635b-59f2-45eb-acfc-d649b12fd2d6.json delete mode 100644 src/data/sessions/f8f915ee-ab64-471c-b3d2-71cb84d2b5fe.json delete mode 100644 src/data/subagent-activities.json create mode 100644 src/utils/trace-capture.js 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