|
#!/usr/bin/env node |
|
/** |
|
* Claude Monitor Daemon |
|
* Monitors Grok's test results and sends corrections via Clauder MCP |
|
* Runs continuously, checking .grok-test-results.json every 5 seconds |
|
*/ |
|
|
|
const fs = require('fs'); |
|
const path = require('path'); |
|
const { spawn } = require('child_process'); |
|
const os = require('os'); |
|
|
|
// Configuration |
|
const CHECK_INTERVAL_MS = 5000; // Check every 5 seconds |
|
const TEST_RESULTS_FILE = path.join(process.cwd(), '.grok-test-results.json'); |
|
const LOG_FILE = '/tmp/claude-monitor.log'; |
|
const GROK_CONTROL_SCRIPT = path.join(os.homedir(), '.local/bin/grok-control.sh'); |
|
|
|
// Logging utility |
|
function log(message, level = 'INFO') { |
|
const timestamp = new Date().toISOString(); |
|
const logMessage = `[${timestamp}] [${level}] ${message}\n`; |
|
|
|
// Console output |
|
process.stdout.write(logMessage); |
|
|
|
// File output (append) |
|
try { |
|
fs.appendFileSync(LOG_FILE, logMessage); |
|
} catch (err) { |
|
// Ignore logging errors |
|
} |
|
} |
|
|
|
// Read test results file |
|
function readTestResults() { |
|
try { |
|
if (!fs.existsSync(TEST_RESULTS_FILE)) { |
|
return null; |
|
} |
|
|
|
const content = fs.readFileSync(TEST_RESULTS_FILE, 'utf8'); |
|
return JSON.parse(content); |
|
} catch (err) { |
|
log(`Error reading test results: ${err.message}`, 'ERROR'); |
|
return null; |
|
} |
|
} |
|
|
|
// Get file modification time |
|
function getFileMtime(filepath) { |
|
try { |
|
const stats = fs.statSync(filepath); |
|
return stats.mtimeMs; |
|
} catch (err) { |
|
return 0; |
|
} |
|
} |
|
|
|
// Send message to Grok via grok-control.sh |
|
function sendCorrectionToGrok(message) { |
|
return new Promise((resolve) => { |
|
log(`Sending correction to Grok: ${message}`, 'INFO'); |
|
|
|
const grokControl = spawn(GROK_CONTROL_SCRIPT, ['send', message], { |
|
stdio: ['ignore', 'pipe', 'pipe'] |
|
}); |
|
|
|
let stdout = ''; |
|
let stderr = ''; |
|
|
|
grokControl.stdout.on('data', (data) => { |
|
stdout += data.toString(); |
|
}); |
|
|
|
grokControl.stderr.on('data', (data) => { |
|
stderr += data.toString(); |
|
}); |
|
|
|
grokControl.on('close', (code) => { |
|
if (code === 0) { |
|
log('Correction sent successfully', 'INFO'); |
|
if (stdout.trim()) log(`stdout: ${stdout.trim()}`, 'DEBUG'); |
|
resolve(true); |
|
} else { |
|
log(`Failed to send correction (exit code ${code})`, 'WARN'); |
|
if (stderr.trim()) log(`stderr: ${stderr.trim()}`, 'DEBUG'); |
|
resolve(false); |
|
} |
|
}); |
|
|
|
grokControl.on('error', (err) => { |
|
log(`Error spawning grok-control: ${err.message}`, 'ERROR'); |
|
resolve(false); |
|
}); |
|
}); |
|
} |
|
|
|
// Analyze test failure and generate correction |
|
function analyzeFailure(failure) { |
|
const { test, error, file, line } = failure; |
|
|
|
// Pattern-based analysis |
|
let correction = null; |
|
|
|
// Null/undefined errors |
|
if (error.includes('Cannot read property') || error.includes('undefined')) { |
|
const match = error.match(/Cannot read property '(\w+)'/); |
|
const property = match ? match[1] : 'property'; |
|
correction = `[SUPERVISOR] Bug found: ${test} |
|
|
|
Fix this by: Add null/undefined check before accessing '${property}' in ${file}:${line} |
|
|
|
Example fix: |
|
if (!obj || typeof obj.${property} === 'undefined') { |
|
return { error: 'Invalid state: ${property} is undefined' }; |
|
}`; |
|
} |
|
// Type errors |
|
else if (error.includes('TypeError') || error.includes('is not a')) { |
|
correction = `[SUPERVISOR] Bug found: ${test} |
|
|
|
Fix this by: Check type before operation in ${file}:${line} |
|
|
|
Error: ${error} |
|
|
|
Add type validation: |
|
if (typeof variable !== 'expected_type') { |
|
return { error: 'Invalid type' }; |
|
}`; |
|
} |
|
// Reference errors |
|
else if (error.includes('ReferenceError') || error.includes('not defined')) { |
|
correction = `[SUPERVISOR] Bug found: ${test} |
|
|
|
Fix this by: Ensure variable is defined before use in ${file}:${line} |
|
|
|
Error: ${error} |
|
|
|
Check variable scope and declaration.`; |
|
} |
|
// Async/await errors |
|
else if (error.includes('async') || error.includes('Promise')) { |
|
correction = `[SUPERVISOR] Bug found: ${test} |
|
|
|
Fix this by: Ensure async/await is used correctly in ${file}:${line} |
|
|
|
Error: ${error} |
|
|
|
Make sure to await async calls and handle promises properly.`; |
|
} |
|
// Generic error |
|
else { |
|
correction = `[SUPERVISOR] Bug found: ${test} |
|
|
|
Fix this by: Debug and fix this error in ${file}:${line} |
|
|
|
Error: ${error} |
|
|
|
Review the code logic and add appropriate error handling.`; |
|
} |
|
|
|
return correction; |
|
} |
|
|
|
// Process test results |
|
async function processTestResults(results) { |
|
const { total_tests, passed, failed, failures, timestamp } = results; |
|
|
|
log(`Processing test results: ${passed}/${total_tests} passed, ${failed} failed`, 'INFO'); |
|
|
|
if (failed === 0) { |
|
log('All tests passed! No corrections needed.', 'INFO'); |
|
return true; |
|
} |
|
|
|
if (!failures || failures.length === 0) { |
|
log('Warning: failed count > 0 but no failures array', 'WARN'); |
|
return false; |
|
} |
|
|
|
// Process each failure |
|
let criticalBug = false; |
|
|
|
for (const failure of failures) { |
|
const correction = analyzeFailure(failure); |
|
|
|
if (correction) { |
|
log(`Generated correction for: ${failure.test}`, 'INFO'); |
|
|
|
// Send correction |
|
const sent = await sendCorrectionToGrok(correction); |
|
|
|
if (sent) { |
|
// Check if this is a critical bug (should pause Grok) |
|
if (failure.error.includes('TypeError') || |
|
failure.error.includes('ReferenceError') || |
|
failure.error.includes('Cannot read')) { |
|
criticalBug = true; |
|
} |
|
} else { |
|
log('Failed to send correction, continuing...', 'WARN'); |
|
} |
|
|
|
// Small delay between corrections |
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
} |
|
} |
|
|
|
// If critical bug found, consider pausing Grok |
|
if (criticalBug) { |
|
log('Critical bug detected. Consider pausing Grok.', 'WARN'); |
|
log('Run: grok-control.sh pause', 'WARN'); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
// Main monitoring loop |
|
let lastMtime = 0; |
|
let lastResults = null; |
|
|
|
async function monitoringLoop() { |
|
log('Starting monitoring cycle...', 'DEBUG'); |
|
|
|
// Check if test results file exists and has been modified |
|
const currentMtime = getFileMtime(TEST_RESULTS_FILE); |
|
|
|
if (currentMtime === 0) { |
|
log('Test results file does not exist yet', 'DEBUG'); |
|
} else if (currentMtime > lastMtime) { |
|
log('Test results file has been modified', 'INFO'); |
|
lastMtime = currentMtime; |
|
|
|
// Read and process results |
|
const results = readTestResults(); |
|
|
|
if (results) { |
|
// Check if results are actually different |
|
const resultsJson = JSON.stringify(results); |
|
const lastResultsJson = lastResults ? JSON.stringify(lastResults) : ''; |
|
|
|
if (resultsJson !== lastResultsJson) { |
|
await processTestResults(results); |
|
lastResults = results; |
|
} else { |
|
log('Test results unchanged (same timestamp)', 'DEBUG'); |
|
} |
|
} |
|
} else { |
|
log('Test results file unchanged', 'DEBUG'); |
|
} |
|
|
|
// Schedule next check |
|
setTimeout(monitoringLoop, CHECK_INTERVAL_MS); |
|
} |
|
|
|
// Graceful shutdown handler |
|
function handleShutdown(signal) { |
|
log(`Received ${signal}, shutting down gracefully...`, 'INFO'); |
|
process.exit(0); |
|
} |
|
|
|
// Main entry point |
|
async function main() { |
|
log('=================================', 'INFO'); |
|
log('Claude Monitor Daemon Starting...', 'INFO'); |
|
log(`PID: ${process.pid}`, 'INFO'); |
|
log(`Check Interval: ${CHECK_INTERVAL_MS}ms`, 'INFO'); |
|
log(`Test Results File: ${TEST_RESULTS_FILE}`, 'INFO'); |
|
log('=================================', 'INFO'); |
|
|
|
// Register signal handlers |
|
process.on('SIGTERM', () => handleShutdown('SIGTERM')); |
|
process.on('SIGINT', () => handleShutdown('SIGINT')); |
|
|
|
// Initial check |
|
const initialResults = readTestResults(); |
|
if (initialResults) { |
|
log('Found existing test results on startup', 'INFO'); |
|
lastMtime = getFileMtime(TEST_RESULTS_FILE); |
|
await processTestResults(initialResults); |
|
lastResults = initialResults; |
|
} else { |
|
log('No existing test results found, waiting for Grok...', 'INFO'); |
|
} |
|
|
|
// Start monitoring loop |
|
monitoringLoop(); |
|
} |
|
|
|
// Start the daemon |
|
main().catch((err) => { |
|
log(`Fatal error: ${err.message}`, 'ERROR'); |
|
console.error(err); |
|
process.exit(1); |
|
}); |