/** * HarborForge Monitor Plugin for OpenClaw * * Registers with OpenClaw Gateway and manages sidecar lifecycle. * Sidecar runs as separate Node process to avoid blocking Gateway. */ import { spawn } from 'child_process'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { existsSync } from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** @type {import('openclaw').Plugin} */ export default function register(api, config) { const logger = api.logger || { info: (...args) => console.log('[HF-Monitor]', ...args), error: (...args) => console.error('[HF-Monitor]', ...args), debug: (...args) => console.debug('[HF-Monitor]', ...args) }; if (!config?.enabled) { logger.info('HarborForge Monitor plugin disabled'); return; } // Validate required config if (!config.challengeUuid) { logger.error('Missing required config: challengeUuid'); logger.error('Please register server in HarborForge Monitor first'); return; } const sidecarPath = join(__dirname, 'sidecar', 'server.mjs'); if (!existsSync(sidecarPath)) { logger.error('Sidecar not found:', sidecarPath); return; } /** @type {import('child_process').ChildProcess|null} */ let sidecar = null; /** * Start the sidecar server */ function startSidecar() { if (sidecar) { logger.debug('Sidecar already running'); return; } logger.info('Starting HarborForge Monitor sidecar...'); // Prepare environment for sidecar const env = { ...process.env, HF_MONITOR_BACKEND_URL: config.backendUrl || 'https://monitor.hangman-lab.top', HF_MONITOR_IDENTIFIER: config.identifier || '', HF_MONITOR_CHALLENGE_UUID: config.challengeUuid, HF_MONITOR_REPORT_INTERVAL: String(config.reportIntervalSec || 30), HF_MONITOR_HTTP_FALLBACK_INTERVAL: String(config.httpFallbackIntervalSec || 60), HF_MONITOR_LOG_LEVEL: config.logLevel || 'info', // Pass OpenClaw info for metrics OPENCLAW_PATH: process.env.OPENCLAW_PATH || join(process.env.HOME || '/root', '.openclaw'), OPENCLAW_VERSION: api.version || 'unknown', }; // Spawn sidecar as detached process so it survives Gateway briefly during restart sidecar = spawn('node', [sidecarPath], { env, detached: false, // Keep attached for logging, but could be true for full detachment stdio: ['ignore', 'pipe', 'pipe'] }); sidecar.stdout?.on('data', (data) => { logger.info('[sidecar]', data.toString().trim()); }); sidecar.stderr?.on('data', (data) => { logger.error('[sidecar]', data.toString().trim()); }); sidecar.on('exit', (code, signal) => { logger.info(`Sidecar exited (code: ${code}, signal: ${signal})`); sidecar = null; }); sidecar.on('error', (err) => { logger.error('Failed to start sidecar:', err.message); sidecar = null; }); logger.info('Sidecar started with PID:', sidecar.pid); } /** * Stop the sidecar server */ function stopSidecar() { if (!sidecar) { logger.debug('Sidecar not running'); return; } logger.info('Stopping HarborForge Monitor sidecar...'); // Graceful shutdown sidecar.kill('SIGTERM'); // Force kill after timeout const timeout = setTimeout(() => { if (sidecar && !sidecar.killed) { logger.warn('Sidecar did not exit gracefully, forcing kill'); sidecar.kill('SIGKILL'); } }, 5000); sidecar.on('exit', () => { clearTimeout(timeout); }); } // Hook into Gateway lifecycle api.on('gateway:start', () => { logger.info('Gateway starting, starting monitor sidecar...'); startSidecar(); }); api.on('gateway:stop', () => { logger.info('Gateway stopping, stopping monitor sidecar...'); stopSidecar(); }); // Also handle process signals directly process.on('SIGTERM', () => { stopSidecar(); }); process.on('SIGINT', () => { stopSidecar(); }); // Start immediately if Gateway is already running if (api.isRunning?.()) { startSidecar(); } else { // Delay start slightly to ensure Gateway is fully up setTimeout(() => { startSidecar(); }, 1000); } // Register status tool api.registerTool(() => ({ name: 'harborforge_monitor_status', description: 'Get HarborForge Monitor plugin status', parameters: { type: 'object', properties: {} }, async execute() { return { enabled: true, sidecarRunning: sidecar !== null && sidecar.exitCode === null, pid: sidecar?.pid || null, config: { backendUrl: config.backendUrl, identifier: config.identifier || 'auto-detected', reportIntervalSec: config.reportIntervalSec } }; } })); logger.info('HarborForge Monitor plugin registered'); }