* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <developer> 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * fix typo(?) in changelog (#13) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Naich <an.naich@gmail.com> Co-authored-by: JessyTsui <51992423+JessyTsui@users.noreply.github.com>
This commit is contained in:
parent
44e67a0c82
commit
61dd2e30c3
|
|
@ -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
|
||||
41
README.md
41
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
},
|
||||
"enabled": true,
|
||||
"enableSubagentNotifications": false,
|
||||
"showSubagentActivitiesInEmail": false,
|
||||
"subagentActivityDetail": "medium",
|
||||
"timeout": 5,
|
||||
"customMessages": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<!-- Full Execution Trace (Terminal Style) -->
|
||||
<div style="margin-top: 40px; border-top: 1px solid #333; padding-top: 30px;">
|
||||
<div style="color: #666; margin-bottom: 15px;">
|
||||
<span style="color: #666;">$</span> <span style="color: #666;">tail -n 1000 execution.log</span>
|
||||
</div>
|
||||
<div style="margin-left: 20px;">
|
||||
<div style="color: #666; font-size: 12px; margin-bottom: 10px;">
|
||||
<span style="color: #999;">[</span><span style="color: #666;">Execution Trace - Scroll to view</span><span style="color: #999;">]</span>
|
||||
</div>
|
||||
<div style="background-color: #0d0d0d; border: 1px solid #222; padding: 15px; max-height: 300px; overflow-y: auto; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #444 #0d0d0d;">
|
||||
<pre style="margin: 0; color: #888; font-size: 11px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;">{{fullExecutionTrace}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
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 {
|
|||
|
||||
<!-- Terminal Content -->
|
||||
<div style="padding: 20px; background-color: #1a1a1a; min-height: 400px;">
|
||||
<!-- Session Info -->
|
||||
<div style="color: #00ff00; margin-bottom: 20px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code status</span><br>
|
||||
<div style="margin-left: 20px; margin-top: 5px; color: #ccc;">
|
||||
<span style="color: #ff9800;">PROJECT:</span> {{projectDir}}<br>
|
||||
<span style="color: #ff9800;">SESSION:</span> #{{token}}<br>
|
||||
<span style="color: #ff9800;">STATUS:</span> <span style="color: #00ff00;">✓ Task Completed</span><br>
|
||||
<span style="color: #ff9800;">TIME:</span> {{timestamp}}
|
||||
<!-- User Input (Terminal Style) -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<div style="color: #00ff00; margin-bottom: 10px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">cat user_request.txt</span>
|
||||
</div>
|
||||
<div style="background-color: #262626; border-left: 4px solid #ff9800; padding: 15px 20px; margin-left: 20px; color: #f0f0f0; font-size: 15px; line-height: 1.6; font-weight: 500;">{{userQuestion}}</div>
|
||||
</div>
|
||||
|
||||
<!-- User Input -->
|
||||
<div style="margin: 20px 0;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">cat user_input.txt</span><br>
|
||||
<div style="background-color: #262626; border-left: 3px solid #ff9800; padding: 10px 15px; margin: 10px 0; color: #f0f0f0; white-space: pre-wrap; word-wrap: break-word;">{{userQuestion}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Response -->
|
||||
<div style="margin: 20px 0;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code execute</span><br>
|
||||
<div style="color: #999; margin: 5px 0;">
|
||||
<span style="color: #00bcd4;">[INFO]</span> Processing request...<br>
|
||||
<span style="color: #00bcd4;">[INFO]</span> Executing task...
|
||||
<!-- Claude Response (Terminal Style) -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<div style="color: #00ff00; margin-bottom: 10px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code execute</span>
|
||||
</div>
|
||||
<div style="background-color: #262626; border-left: 3px solid #00ff00; padding: 15px; margin: 10px 0; color: #f0f0f0; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%; font-size: 14px; line-height: 1.6;">{{claudeResponse}}</div>
|
||||
<div style="color: #00ff00; margin-top: 10px;">
|
||||
<span style="color: #00bcd4;">[SUCCESS]</span> Task completed successfully ✓
|
||||
<div style="margin-left: 20px;">
|
||||
<div style="color: #999; margin-bottom: 10px; font-size: 13px;">
|
||||
<span style="color: #00bcd4;">[INFO]</span> Processing request...<br>
|
||||
<span style="color: #00bcd4;">[INFO]</span> Task execution started at {{timestamp}}
|
||||
</div>
|
||||
<div style="background-color: #1f1f1f; border-left: 4px solid #00ff00; padding: 15px 20px; color: #f0f0f0; font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">{{claudeResponse}}</div>
|
||||
<div style="color: #00ff00; margin-top: 10px; font-size: 13px;">
|
||||
<span style="color: #00bcd4;">[SUCCESS]</span> Task completed successfully ✓
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{subagentActivities}}
|
||||
|
||||
<!-- Continue Instructions -->
|
||||
<div style="margin: 30px 0 20px 0; border-top: 1px solid #333; padding-top: 20px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code help --continue</span><br>
|
||||
<div style="color: #f0f0f0; margin: 10px 0;">
|
||||
<div style="color: #ff9800; margin-bottom: 10px;">→ TO CONTINUE THIS SESSION:</div>
|
||||
<div style="background-color: #262626; padding: 15px; border: 1px solid #333; margin: 10px 0;">
|
||||
<!-- Continue Instructions (Terminal Style) -->
|
||||
<div style="margin: 40px 0; padding-top: 30px; border-top: 1px solid #333;">
|
||||
<div style="color: #ff9800; margin-bottom: 15px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #ff9800;">claude-code --help continue</span>
|
||||
</div>
|
||||
<div style="margin-left: 20px; background-color: #0d0d0d; padding: 15px; border: 1px solid #333;">
|
||||
<div style="color: #00ff00; margin-bottom: 10px; font-weight: bold;">TO CONTINUE THIS SESSION:</div>
|
||||
<div style="color: #ccc; font-size: 13px; line-height: 1.8;">
|
||||
Reply to this email directly with your next instruction.<br><br>
|
||||
<span style="color: #999;">Examples:</span><br>
|
||||
<span style="color: #00ff00;"> • "Add error handling to the function"</span><br>
|
||||
<span style="color: #00ff00;"> • "Write unit tests for this code"</span><br>
|
||||
<span style="color: #00ff00;"> • "Optimize the performance"</span>
|
||||
<span style="color: #666;">Examples:</span><br>
|
||||
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Add error handling to the function"</span><br>
|
||||
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Write unit tests for this code"</span><br>
|
||||
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Optimize the performance"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Footer -->
|
||||
<div style="color: #666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #333;">
|
||||
<span style="color: #999;">$</span> <span style="color: #666;">echo $SESSION_INFO</span><br>
|
||||
<div style="margin-left: 20px; margin-top: 5px;">
|
||||
SESSION_ID={{sessionId}}<br>
|
||||
EXPIRES_IN=24h<br>
|
||||
SECURITY=Do not forward this email<br>
|
||||
POWERED_BY=Claude-Code-Remote
|
||||
</div>
|
||||
</div>
|
||||
{{executionTraceSection}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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.`
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `
|
||||
<div style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; background-color: #f5f5f5; padding: 0; margin: 0;">
|
||||
<div style="max-width: 900px; margin: 0 auto; background-color: #1e1e1e; border: 1px solid #333; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);">
|
||||
<!-- Terminal Header -->
|
||||
<div style="background-color: #2d2d2d; padding: 10px 15px; border-bottom: 1px solid #444;">
|
||||
<table style="display: inline-table; vertical-align: middle;" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding: 0;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ff5f56;"></div></td>
|
||||
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ffbd2e;"></div></td>
|
||||
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #27c93f;"></div></td>
|
||||
<td style="padding: 0 0 0 12px; color: #999; font-size: 14px; white-space: nowrap;">claude-code-remote@{{project}} - Task Completed</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Content -->
|
||||
<div style="padding: 20px; background-color: #1a1a1a; min-height: 400px;">
|
||||
<!-- User Input (Terminal Style) -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<div style="color: #00ff00; margin-bottom: 10px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">cat user_request.txt</span>
|
||||
</div>
|
||||
<div style="background-color: #262626; border-left: 4px solid #ff9800; padding: 15px 20px; margin-left: 20px; color: #f0f0f0; font-size: 15px; line-height: 1.6; font-weight: 500;">{{userQuestion}}</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Response (Terminal Style) -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<div style="color: #00ff00; margin-bottom: 10px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code execute</span>
|
||||
</div>
|
||||
<<<<<<< HEAD
|
||||
<div style="margin-left: 20px;">
|
||||
<div style="color: #999; margin-bottom: 10px; font-size: 13px;">
|
||||
<span style="color: #00bcd4;">[INFO]</span> Processing request...<br>
|
||||
<span style="color: #00bcd4;">[INFO]</span> Task execution started at {{timestamp}}
|
||||
</div>
|
||||
<div style="background-color: #1f1f1f; border-left: 4px solid #00ff00; padding: 15px 20px; color: #f0f0f0; font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;">{{claudeResponse}}</div>
|
||||
<div style="color: #00ff00; margin-top: 10px; font-size: 13px;">
|
||||
<span style="color: #00bcd4;">[SUCCESS]</span> Task completed successfully ✓
|
||||
=======
|
||||
<div style="background-color: #262626; border-left: 3px solid #00ff00; padding: 15px; margin: 10px 0; color: #f0f0f0; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%; font-size: 14px; line-height: 1.6;">{{claudeResponse}}</div>
|
||||
<div style="color: #00ff00; margin-top: 10px;">
|
||||
<span style="color: #00bcd4;">[SUCCESS]</span> Task completed successfully ✓
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{subagentActivities}}
|
||||
|
||||
<!-- Continue Instructions -->
|
||||
<div style="margin: 30px 0 20px 0; border-top: 1px solid #333; padding-top: 20px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code help --continue</span><br>
|
||||
<div style="color: #f0f0f0; margin: 10px 0;">
|
||||
<div style="color: #ff9800; margin-bottom: 10px;">→ TO CONTINUE THIS SESSION:</div>
|
||||
<div style="background-color: #262626; padding: 15px; border: 1px solid #333; margin: 10px 0;">
|
||||
Reply to this email directly with your next instruction.<br><br>
|
||||
<span style="color: #999;">Examples:</span><br>
|
||||
<span style="color: #00ff00;"> • "Add error handling to the function"</span><br>
|
||||
<span style="color: #00ff00;"> • "Write unit tests for this code"</span><br>
|
||||
<span style="color: #00ff00;"> • "Optimize the performance"</span>
|
||||
>>>>>>> upstream/master
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{subagentActivities}}
|
||||
|
||||
<!-- Continue Instructions (Terminal Style) -->
|
||||
<div style="margin: 40px 0; padding-top: 30px; border-top: 1px solid #333;">
|
||||
<div style="color: #ff9800; margin-bottom: 15px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #ff9800;">claude-code --help continue</span>
|
||||
</div>
|
||||
<div style="margin-left: 20px; background-color: #0d0d0d; padding: 15px; border: 1px solid #333;">
|
||||
<div style="color: #00ff00; margin-bottom: 10px; font-weight: bold;">TO CONTINUE THIS SESSION:</div>
|
||||
<div style="color: #ccc; font-size: 13px; line-height: 1.8;">
|
||||
Reply to this email directly with your next instruction.<br><br>
|
||||
<span style="color: #666;">Examples:</span><br>
|
||||
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Add error handling to the function"</span><br>
|
||||
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Write unit tests for this code"</span><br>
|
||||
<span style="color: #999;">•</span> <span style="color: #00ff00;">"Optimize the performance"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Execution Trace (Terminal Style) -->
|
||||
<div style="margin-top: 40px; border-top: 1px solid #333; padding-top: 30px;">
|
||||
<div style="color: #666; margin-bottom: 15px;">
|
||||
<span style="color: #666;">$</span> <span style="color: #666;">tail -n 1000 execution.log</span>
|
||||
</div>
|
||||
<details style="margin-left: 20px;">
|
||||
<summary style="color: #666; font-size: 12px; cursor: pointer; user-select: none; margin-bottom: 10px;">
|
||||
<span style="color: #999;">[</span><span style="color: #666;">Click to view full execution trace</span><span style="color: #999;">]</span>
|
||||
</summary>
|
||||
<div style="background-color: #0d0d0d; border: 1px solid #222; padding: 15px; max-height: 300px; overflow-y: auto; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #444 #0d0d0d;">
|
||||
<pre style="margin: 0; color: #888; font-size: 11px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;">{{fullExecutionTrace}}</pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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: `
|
||||
<div style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; background-color: #f5f5f5; padding: 0; margin: 0;">
|
||||
<div style="max-width: 900px; margin: 0 auto; background-color: #1e1e1e; border: 1px solid #333; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);">
|
||||
<!-- Terminal Header -->
|
||||
<div style="background-color: #2d2d2d; padding: 10px 15px; border-bottom: 1px solid #444;">
|
||||
<table style="display: inline-table; vertical-align: middle;" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding: 0;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ff5f56;"></div></td>
|
||||
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #ffbd2e;"></div></td>
|
||||
<td style="padding: 0 0 0 5px;"><div style="width: 12px; height: 12px; border-radius: 50%; background-color: #27c93f;"></div></td>
|
||||
<td style="padding: 0 0 0 12px; color: #999; font-size: 14px; white-space: nowrap;">claude-code-remote@{{project}} - Waiting for Input</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Content -->
|
||||
<div style="padding: 20px; background-color: #1a1a1a; min-height: 400px;">
|
||||
<!-- Session Info -->
|
||||
<div style="color: #00ff00; margin-bottom: 20px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code status</span><br>
|
||||
<div style="margin-left: 20px; margin-top: 5px; color: #ccc;">
|
||||
<span style="color: #ff9800;">PROJECT:</span> {{projectDir}}<br>
|
||||
<span style="color: #ff9800;">SESSION:</span> #{{token}}<br>
|
||||
<span style="color: #ff9800;">STATUS:</span> <span style="color: #ffeb3b;">⏳ Waiting for input</span><br>
|
||||
<span style="color: #ff9800;">TIME:</span> {{timestamp}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Waiting Message -->
|
||||
<div style="margin: 20px 0;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code wait</span><br>
|
||||
<div style="color: #ffeb3b; margin: 10px 0;">
|
||||
<span style="color: #ff9800;">[WAITING]</span> Claude needs your input to continue...<br>
|
||||
</div>
|
||||
<div style="background-color: #262626; border-left: 3px solid #ffeb3b; padding: 15px; margin: 10px 0; color: #f0f0f0;">
|
||||
{{message}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Continue Instructions -->
|
||||
<div style="margin: 30px 0 20px 0; border-top: 1px solid #333; padding-top: 20px;">
|
||||
<span style="color: #999;">$</span> <span style="color: #00ff00;">claude-code help --respond</span><br>
|
||||
<div style="color: #f0f0f0; margin: 10px 0;">
|
||||
<div style="color: #ff9800; margin-bottom: 10px;">→ ACTION REQUIRED:</div>
|
||||
<div style="background-color: #262626; padding: 15px; border: 1px solid #333; margin: 10px 0;">
|
||||
<span style="color: #ffeb3b;">Claude is waiting for your guidance.</span><br><br>
|
||||
Reply to this email with your instructions to continue.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Footer -->
|
||||
<div style="color: #666; font-size: 12px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #333;">
|
||||
<span style="color: #999;">$</span> <span style="color: #666;">echo $SESSION_INFO</span><br>
|
||||
<div style="margin-left: 20px; margin-top: 5px;">
|
||||
SESSION_ID={{sessionId}}<br>
|
||||
EXPIRES_IN=24h<br>
|
||||
SECURITY=Do not forward this email<br>
|
||||
POWERED_BY=Claude-Code-Remote
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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;
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"messages": []
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue