/** * HarborForge Plugin for OpenClaw * * Provides monitor-related tools and exposes OpenClaw metadata * for the HarborForge Monitor bridge (via monitor_port). * * Sidecar architecture has been removed. Telemetry data is now * served directly by the plugin when Monitor queries via the * local monitor_port communication path. */ import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os'; import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config'; import { MonitorBridgeClient, type OpenClawMeta } from './core/monitor-bridge'; interface PluginAPI { logger: { info: (...args: any[]) => void; error: (...args: any[]) => void; debug: (...args: any[]) => void; warn: (...args: any[]) => void; }; version?: string; config?: Record; pluginConfig?: Record; on: (event: string, handler: () => void) => void; registerTool: (factory: (ctx: any) => any) => void; } export default { id: 'harbor-forge', name: 'HarborForge', register(api: PluginAPI) { const logger = api.logger || { info: (...args: any[]) => console.log('[HarborForge]', ...args), error: (...args: any[]) => console.error('[HarborForge]', ...args), debug: (...args: any[]) => console.debug('[HarborForge]', ...args), warn: (...args: any[]) => console.warn('[HarborForge]', ...args), }; const baseConfig: HarborForgeMonitorConfig = { enabled: true, backendUrl: 'https://monitor.hangman-lab.top', identifier: '', reportIntervalSec: 30, httpFallbackIntervalSec: 60, logLevel: 'info', ...(api.pluginConfig || {}), }; function resolveConfig() { return getLivePluginConfig(api, baseConfig); } /** * Get the monitor bridge client if monitor_port is configured. * Legacy alias monitorPort is still accepted. */ function getBridgeClient(): MonitorBridgeClient | null { const live = resolveConfig() as any; const port = live.monitor_port ?? live.monitorPort; if (!port || port <= 0) return null; return new MonitorBridgeClient(port); } /** * Collect current system telemetry snapshot. * This data is exposed to the Monitor bridge when it queries the plugin. */ function collectTelemetry() { const live = resolveConfig(); const load = loadavg(); return { identifier: live.identifier || hostname(), platform: platform(), hostname: hostname(), uptime: uptime(), memory: { total: totalmem(), free: freemem(), used: totalmem() - freemem(), usagePercent: ((totalmem() - freemem()) / totalmem()) * 100, }, load: { avg1: load[0], avg5: load[1], avg15: load[2], }, openclaw: { version: api.version || 'unknown', pluginVersion: '0.2.0', }, timestamp: new Date().toISOString(), }; } // Periodic metadata push interval handle let metaPushInterval: ReturnType | null = null; /** * Push OpenClaw metadata to the Monitor bridge. * This enriches Monitor heartbeats with OpenClaw version/plugin/agent info. * Failures are non-fatal — Monitor continues to work without this data. */ async function pushMetaToMonitor() { const bridgeClient = getBridgeClient(); if (!bridgeClient) return; const meta: OpenClawMeta = { version: api.version || 'unknown', plugin_version: '0.2.0', agents: [], // TODO: populate from api agent list when available }; const ok = await bridgeClient.pushOpenClawMeta(meta); if (ok) { logger.debug('pushed OpenClaw metadata to Monitor bridge'); } else { logger.debug('Monitor bridge unreachable for metadata push (non-fatal)'); } } api.on('gateway_start', () => { logger.info('HarborForge plugin active'); // Push metadata to Monitor bridge on startup and periodically. // Interval aligns with typical Monitor heartbeat cycle (30s). // If Monitor bridge is unreachable, pushes silently fail. const live = resolveConfig(); const intervalSec = live.reportIntervalSec || 30; // Initial push (delayed 2s to let Monitor bridge start) setTimeout(() => pushMetaToMonitor(), 2000); metaPushInterval = setInterval( () => pushMetaToMonitor(), intervalSec * 1000, ); }); api.on('gateway_stop', () => { logger.info('HarborForge plugin stopping'); if (metaPushInterval) { clearInterval(metaPushInterval); metaPushInterval = null; } }); // Tool: plugin status api.registerTool(() => ({ name: 'harborforge_status', description: 'Get HarborForge plugin status and current telemetry snapshot', parameters: { type: 'object', properties: {}, }, async execute() { const live = resolveConfig(); const bridgeClient = getBridgeClient(); let monitorBridge = null; if (bridgeClient) { const health = await bridgeClient.health(); monitorBridge = health ? { connected: true, ...health } : { connected: false, error: 'Monitor bridge unreachable' }; } return { enabled: live.enabled !== false, config: { backendUrl: live.backendUrl, identifier: live.identifier || hostname(), monitorPort: (live as any).monitor_port ?? (live as any).monitorPort ?? null, reportIntervalSec: live.reportIntervalSec, hasApiKey: Boolean(live.apiKey), }, monitorBridge, telemetry: collectTelemetry(), }; }, })); // Tool: telemetry snapshot (for Monitor bridge queries) api.registerTool(() => ({ name: 'harborforge_telemetry', description: 'Get current system telemetry data for HarborForge Monitor', parameters: { type: 'object', properties: {}, }, async execute() { return collectTelemetry(); }, })); // Tool: query Monitor bridge for host hardware telemetry api.registerTool(() => ({ name: 'harborforge_monitor_telemetry', description: 'Query HarborForge Monitor bridge for host hardware telemetry (CPU, memory, disk, etc.)', parameters: { type: 'object', properties: {}, }, async execute() { const bridgeClient = getBridgeClient(); if (!bridgeClient) { return { error: 'Monitor bridge not configured (monitor_port not set or 0)', }; } const data = await bridgeClient.telemetry(); if (!data) { return { error: 'Monitor bridge unreachable', }; } return data; }, })); logger.info('HarborForge plugin registered (id: harbor-forge)'); }, };