/** * HarborForge Monitor Plugin for OpenClaw * * Manages sidecar lifecycle and provides monitor-related tools. */ import { spawn } from 'child_process'; import { join } from 'path'; import { existsSync } from 'fs'; interface PluginConfig { enabled?: boolean; backendUrl?: string; identifier?: string; apiKey?: string; reportIntervalSec?: number; httpFallbackIntervalSec?: number; logLevel?: 'debug' | 'info' | 'warn' | 'error'; } interface PluginEntryLike { enabled?: boolean; config?: PluginConfig; } interface PluginAPI { logger: { info: (...args: any[]) => void; error: (...args: any[]) => void; debug: (...args: any[]) => void; warn: (...args: any[]) => void; }; version?: string; isRunning?: () => boolean; on: (event: string, handler: () => void) => void; registerTool: (factory: (ctx: any) => any) => void; } export default function register(api: PluginAPI, rawConfig: PluginConfig | PluginEntryLike | undefined) { const logger = api.logger || { info: (...args: any[]) => console.log('[HF-Monitor]', ...args), error: (...args: any[]) => console.error('[HF-Monitor]', ...args), debug: (...args: any[]) => console.debug('[HF-Monitor]', ...args), warn: (...args: any[]) => console.warn('[HF-Monitor]', ...args), }; const entryLike = rawConfig as PluginEntryLike | undefined; const config: PluginConfig = entryLike?.config ? { ...entryLike.config, enabled: entryLike.config.enabled ?? entryLike.enabled, } : ((rawConfig as PluginConfig | undefined) ?? {}); const enabled = config.enabled !== false; logger.info('HarborForge Monitor plugin config resolved', { enabled, hasApiKey: Boolean(config.apiKey), backendUrl: config.backendUrl ?? null, identifier: config.identifier ?? null, }); if (!enabled) { logger.info('HarborForge Monitor plugin disabled'); return; } if (!config.apiKey) { logger.warn('Missing config: apiKey'); logger.warn('API authentication will fail. Generate apiKey from HarborForge Monitor admin.'); } const serverPath = join(__dirname, 'server', 'telemetry.mjs'); if (!existsSync(serverPath)) { logger.error('Telemetry server not found:', serverPath); return; } let sidecar: ReturnType | null = null; function startSidecar() { if (sidecar) { logger.debug('Sidecar already running'); return; } logger.info('Starting HarborForge Monitor telemetry server...'); const env = { ...process.env, HF_MONITOR_BACKEND_URL: config.backendUrl || 'https://monitor.hangman-lab.top', HF_MONITOR_IDENTIFIER: config.identifier || '', HF_MONITOR_API_KEY: config.apiKey || '', HF_MONITOR_REPORT_INTERVAL: String(config.reportIntervalSec || 30), HF_MONITOR_HTTP_FALLBACK_INTERVAL: String(config.httpFallbackIntervalSec || 60), HF_MONITOR_LOG_LEVEL: config.logLevel || 'info', OPENCLAW_PATH: process.env.OPENCLAW_PATH || join(process.env.HOME || '/root', '.openclaw'), OPENCLAW_VERSION: api.version || 'unknown', }; sidecar = spawn('node', [serverPath], { env, detached: false, stdio: ['ignore', 'pipe', 'pipe'] }); sidecar.stdout?.on('data', (data: Buffer) => { logger.info('[telemetry]', data.toString().trim()); }); sidecar.stderr?.on('data', (data: Buffer) => { logger.error('[telemetry]', data.toString().trim()); }); sidecar.on('exit', (code, signal) => { logger.info(`Telemetry server exited (code: ${code}, signal: ${signal})`); sidecar = null; }); sidecar.on('error', (err: Error) => { logger.error('Failed to start telemetry server:', err.message); sidecar = null; }); logger.info('Telemetry server started with PID:', sidecar.pid); } function stopSidecar() { if (!sidecar) { logger.debug('Telemetry server not running'); return; } logger.info('Stopping HarborForge Monitor telemetry server...'); sidecar.kill('SIGTERM'); const timeout = setTimeout(() => { if (sidecar && !sidecar.killed) { logger.warn('Telemetry server 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 telemetry server...'); startSidecar(); }); api.on('gateway_stop', () => { logger.info('Gateway stopping, stopping telemetry server...'); stopSidecar(); }); // Handle process signals process.on('SIGTERM', stopSidecar); process.on('SIGINT', stopSidecar); // Start immediately if Gateway is already running if (api.isRunning?.()) { startSidecar(); } else { 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'); }