294 lines
11 KiB
JavaScript
294 lines
11 KiB
JavaScript
#!/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-session';
|
||
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();
|