claude-code-remote-remake/smart-monitor.js

293 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Smart Monitor - 智能監控器,能檢測歷史回應和新回應
* 解決監控器錯過已完成回應的問題
*/
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
const { execSync } = require('child_process');
// Load environment variables
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
}
const TelegramChannel = require('./src/channels/telegram/telegram');
class SmartMonitor {
constructor() {
this.sessionName = process.env.TMUX_SESSION || 'claude-real';
this.lastOutput = '';
this.processedResponses = new Set(); // 記錄已處理的回應
this.checkInterval = 1000; // Check every 1 second
this.isRunning = false;
this.startupTime = Date.now();
// Setup Telegram
if (process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID) {
const telegramConfig = {
botToken: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID
};
this.telegram = new TelegramChannel(telegramConfig);
console.log('📱 Smart Monitor configured successfully');
} else {
console.log('❌ Telegram not configured');
process.exit(1);
}
}
start() {
this.isRunning = true;
console.log(`🧠 Starting smart monitor for session: ${this.sessionName}`);
// Check for any unprocessed responses on startup
this.checkForUnprocessedResponses();
// Initial capture
this.lastOutput = this.captureOutput();
// Start monitoring
this.monitor();
}
async checkForUnprocessedResponses() {
console.log('🔍 Checking for unprocessed responses...');
const currentOutput = this.captureOutput();
const responses = this.extractAllResponses(currentOutput);
// Check if there are recent responses (within 5 minutes) that might be unprocessed
const recentResponses = responses.filter(response => {
const responseAge = Date.now() - this.startupTime;
return responseAge < 5 * 60 * 1000; // 5 minutes
});
if (recentResponses.length > 0) {
console.log(`🎯 Found ${recentResponses.length} potentially unprocessed responses`);
// Send notification for the most recent response
const latestResponse = recentResponses[recentResponses.length - 1];
await this.sendNotificationForResponse(latestResponse);
} else {
console.log('✅ No unprocessed responses found');
}
}
captureOutput() {
try {
return execSync(`tmux capture-pane -t ${this.sessionName} -p`, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
});
} catch (error) {
console.error('Error capturing tmux:', error.message);
return '';
}
}
autoApproveDialog() {
try {
console.log('🤖 Auto-approving Claude tool usage dialog...');
// Send "1" to select the first option (usually "Yes")
execSync(`tmux send-keys -t ${this.sessionName} '1'`, { encoding: 'utf8' });
setTimeout(() => {
execSync(`tmux send-keys -t ${this.sessionName} Enter`, { encoding: 'utf8' });
}, 100);
console.log('✅ Auto-approval sent successfully');
} catch (error) {
console.error('❌ Failed to auto-approve dialog:', error.message);
}
}
extractAllResponses(content) {
const lines = content.split('\n');
const responses = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Look for standard Claude responses
if (line.startsWith('⏺ ') && line.length > 2) {
const responseText = line.substring(2).trim();
// Find the corresponding user question
let userQuestion = 'Recent command';
for (let j = i - 1; j >= 0; j--) {
const prevLine = lines[j].trim();
if (prevLine.startsWith('> ') && prevLine.length > 2) {
userQuestion = prevLine.substring(2).trim();
break;
}
}
responses.push({
userQuestion,
claudeResponse: responseText,
lineIndex: i,
responseId: `${userQuestion}-${responseText}`.substring(0, 50),
type: 'standard'
});
}
// Look for interactive dialogs/tool confirmations
if (line.includes('Do you want to proceed?') ||
line.includes(' 1. Yes') ||
line.includes('Tool use') ||
(line.includes('│') && (line.includes('serena') || line.includes('MCP') || line.includes('initial_instructions')))) {
// Check if this is part of a tool use dialog
let dialogContent = '';
let userQuestion = 'Recent command';
// Look backward to find the start of the dialog and user question
for (let j = i; j >= Math.max(0, i - 50); j--) {
const prevLine = lines[j];
if (prevLine.includes('╭') || prevLine.includes('Tool use')) {
// Found start of dialog box, now collect all content
for (let k = j; k <= Math.min(lines.length - 1, i + 20); k++) {
if (lines[k].includes('╰')) {
dialogContent += lines[k] + '\n';
break; // End of dialog box
}
dialogContent += lines[k] + '\n';
}
break;
}
// Look for user question
if (prevLine.startsWith('> ') && prevLine.length > 2) {
userQuestion = prevLine.substring(2).trim();
}
}
if (dialogContent.length > 50) { // Only if we found substantial dialog
// Auto-approve the dialog instead of asking user to go to iTerm2
this.autoApproveDialog();
responses.push({
userQuestion,
claudeResponse: 'Claude requested tool permission - automatically approved. Processing...',
lineIndex: i,
responseId: `dialog-${userQuestion}-${Date.now()}`.substring(0, 50),
type: 'interactive',
fullDialog: dialogContent.substring(0, 500)
});
break; // Only send one dialog notification per check
}
}
}
return responses;
}
async monitor() {
while (this.isRunning) {
await this.sleep(this.checkInterval);
const currentOutput = this.captureOutput();
if (currentOutput !== this.lastOutput) {
console.log('📝 Output changed, checking for new responses...');
const oldResponses = this.extractAllResponses(this.lastOutput);
const newResponses = this.extractAllResponses(currentOutput);
// Find truly new responses
const newResponseIds = new Set(newResponses.map(r => r.responseId));
const oldResponseIds = new Set(oldResponses.map(r => r.responseId));
const actuallyNewResponses = newResponses.filter(response =>
!oldResponseIds.has(response.responseId) &&
!this.processedResponses.has(response.responseId)
);
if (actuallyNewResponses.length > 0) {
console.log(`🎯 Found ${actuallyNewResponses.length} new responses`);
for (const response of actuallyNewResponses) {
await this.sendNotificationForResponse(response);
this.processedResponses.add(response.responseId);
}
} else {
console.log(' No new responses detected');
}
this.lastOutput = currentOutput;
}
}
}
async sendNotificationForResponse(response) {
try {
console.log('📤 Sending notification for response:', response.claudeResponse.substring(0, 50) + '...');
const notification = {
type: 'completed',
title: 'Claude Response Ready',
message: 'Claude has responded to your command',
project: 'claude-code-line',
metadata: {
userQuestion: response.userQuestion,
claudeResponse: response.claudeResponse,
tmuxSession: this.sessionName,
workingDirectory: process.cwd(),
timestamp: new Date().toISOString(),
autoDetected: true
}
};
const result = await this.telegram.send(notification);
if (result) {
console.log('✅ Notification sent successfully');
} else {
console.log('❌ Failed to send notification');
}
} catch (error) {
console.error('❌ Notification error:', error.message);
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
stop() {
this.isRunning = false;
console.log('⏹️ Smart Monitor stopped');
}
getStatus() {
return {
isRunning: this.isRunning,
sessionName: this.sessionName,
processedCount: this.processedResponses.size,
uptime: Math.floor((Date.now() - this.startupTime) / 1000) + 's'
};
}
}
// Handle graceful shutdown
const monitor = new SmartMonitor();
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down...');
monitor.stop();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 Shutting down...');
monitor.stop();
process.exit(0);
});
// Start monitoring
monitor.start();