From 94eca82fc7eec552f1a124b3921008053222d779 Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 13:50:41 +0000 Subject: [PATCH 1/5] feat: add skills/hf/SKILL.md for hf CLI agent integration Adds the SKILL.md that teaches agents how to use the hf CLI. Gated behind --install-cli in the installer (per plan). --- skills/hf/SKILL.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 skills/hf/SKILL.md diff --git a/skills/hf/SKILL.md b/skills/hf/SKILL.md new file mode 100644 index 0000000..f931444 --- /dev/null +++ b/skills/hf/SKILL.md @@ -0,0 +1,59 @@ +# hf - HarborForge CLI + +`hf` is the Go-based CLI for HarborForge. It manages users, projects, tasks, milestones, meetings, support tickets, proposals, and server monitoring. + +## Quick Start + +```bash +# See all available commands (including those you may not have permission for) +hf --help + +# See only the commands you're permitted to use +hf --help-brief + +# Check API health +hf health + +# Show CLI version +hf version +``` + +## Configuration + +```bash +# Set the HarborForge API URL +hf config --url https://your-harborforge.example.com + +# View current config +hf config +``` + +## Usage Tips + +- Use `hf --help-brief` to quickly see what you can do — it hides commands you don't have permission for. +- Use `hf --help` for the full subcommand list of any group (e.g. `hf task --help`). +- Add `--json` to any command for machine-readable JSON output. +- Resources use **codes** (not numeric IDs) — e.g. `hf task get TASK-42`. + +## Authentication + +If `pass_mgr` is available (padded-cell mode), authentication is automatic — no flags needed. + +Without `pass_mgr` (manual mode), pass `--token ` to authenticated commands. + +## Command Groups + +| Group | Description | +|-------------|------------------------------------| +| `user` | Manage user accounts | +| `role` | Manage roles and permissions | +| `project` | Manage projects and members | +| `milestone` | Manage project milestones | +| `task` | Manage and track tasks | +| `meeting` | Manage meetings and attendance | +| `support` | Manage support tickets | +| `propose` | Manage proposals | +| `monitor` | Monitor servers and API keys | +| `config` | CLI configuration | +| `health` | API health check | +| `version` | Show CLI version | From 9f649e2b39118ef27aa4fc6aa299dddc0219d676 Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 15:24:50 +0000 Subject: [PATCH 2/5] feat: rename plugin to harbor-forge, remove sidecar, add --install-cli Major changes: - Renamed plugin id from harborforge-monitor to harbor-forge (TODO 4.1) - Removed sidecar server/ directory and spawn logic (TODO 4.2) - Added monitorPort to plugin config schema (TODO 4.3) - Added --install-cli flag to installer for building hf CLI (TODO 4.4) - skills/hf/ only deployed when --install-cli is present (TODO 4.5) - Plugin now serves telemetry data directly via tools - Installer handles migration from old plugin name - Bumped version to 0.2.0 --- package.json | 9 +- plugin/index.ts | 179 ++++++----------- plugin/openclaw.plugin.json | 14 +- scripts/install.mjs | 207 +++++++++++++++----- server/telemetry.mjs | 380 ------------------------------------ 5 files changed, 234 insertions(+), 555 deletions(-) delete mode 100644 server/telemetry.mjs diff --git a/package.json b/package.json index c9bb563..7a00131 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { - "name": "harborforge-openclaw-plugin", - "version": "0.1.0", - "description": "OpenClaw plugin for HarborForge Monitor - streams server telemetry", + "name": "harbor-forge-openclaw-plugin", + "version": "0.2.0", + "description": "OpenClaw plugin for HarborForge - project management, monitoring, and CLI integration", "type": "module", "scripts": { "build": "cd plugin && npm run build", "install": "node scripts/install.mjs", + "install-cli": "node scripts/install.mjs --install-cli", "uninstall": "node scripts/install.mjs --uninstall" }, - "keywords": ["openclaw", "plugin", "monitoring", "harborforge"], + "keywords": ["openclaw", "plugin", "monitoring", "harborforge", "harbor-forge"], "license": "MIT", "engines": { "node": ">=18.0.0" diff --git a/plugin/index.ts b/plugin/index.ts index 18d1577..5fc2481 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -1,11 +1,14 @@ /** - * HarborForge Monitor Plugin for OpenClaw + * HarborForge Plugin for OpenClaw * - * Manages sidecar lifecycle and provides monitor-related tools. + * 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 { spawn } from 'child_process'; -import { join } from 'path'; -import { existsSync } from 'fs'; +import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os'; import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config'; interface PluginAPI { @@ -23,14 +26,14 @@ interface PluginAPI { } export default { - id: 'harborforge-monitor', - name: 'HarborForge Monitor', + id: 'harbor-forge', + name: 'HarborForge', register(api: PluginAPI) { 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), + 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 = { @@ -43,122 +46,53 @@ export default { ...(api.pluginConfig || {}), }; - const serverPath = join(__dirname, 'server', 'telemetry.mjs'); - let sidecar: ReturnType | null = null; - function resolveConfig() { return getLivePluginConfig(api, baseConfig); } - function startSidecar() { + /** + * Collect current system telemetry snapshot. + * This data is exposed to the Monitor bridge when it queries the plugin. + */ + function collectTelemetry() { const live = resolveConfig(); - const enabled = live.enabled !== false; - - logger.info('HarborForge Monitor plugin config resolved', { - enabled, - hasApiKey: Boolean(live.apiKey), - backendUrl: live.backendUrl ?? null, - identifier: live.identifier ?? null, - }); - - if (!enabled) { - logger.info('HarborForge Monitor plugin disabled'); - return; - } - - if (sidecar) { - logger.debug('Sidecar already running'); - return; - } - - if (!live.apiKey) { - logger.warn('Missing config: apiKey'); - logger.warn('API authentication will fail. Generate apiKey from HarborForge Monitor admin.'); - } - - if (!existsSync(serverPath)) { - logger.error('Telemetry server not found:', serverPath); - return; - } - - logger.info('Starting HarborForge Monitor telemetry server...'); - - const env = { - ...process.env, - HF_MONITOR_BACKEND_URL: live.backendUrl || 'https://monitor.hangman-lab.top', - HF_MONITOR_IDENTIFIER: live.identifier || '', - HF_MONITOR_API_KEY: live.apiKey || '', - HF_MONITOR_REPORT_INTERVAL: String(live.reportIntervalSec || 30), - HF_MONITOR_HTTP_FALLBACK_INTERVAL: String(live.httpFallbackIntervalSec || 60), - HF_MONITOR_LOG_LEVEL: live.logLevel || 'info', - OPENCLAW_PATH: process.env.OPENCLAW_PATH || join(process.env.HOME || '/root', '.openclaw'), - HF_MONITOR_PLUGIN_VERSION: api.version || 'unknown', + 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(), }; - - 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); - }); } api.on('gateway_start', () => { - logger.info('gateway_start received, starting telemetry server...'); - startSidecar(); + logger.info('HarborForge plugin active'); }); api.on('gateway_stop', () => { - logger.info('gateway_stop received, stopping telemetry server...'); - stopSidecar(); + logger.info('HarborForge plugin stopping'); }); - process.on('SIGTERM', stopSidecar); - process.on('SIGINT', stopSidecar); - + // Tool: plugin status api.registerTool(() => ({ - name: 'harborforge_monitor_status', - description: 'Get HarborForge Monitor plugin status', + name: 'harborforge_status', + description: 'Get HarborForge plugin status and current telemetry snapshot', parameters: { type: 'object', properties: {}, @@ -167,18 +101,31 @@ export default { const live = resolveConfig(); return { enabled: live.enabled !== false, - sidecarRunning: sidecar !== null && sidecar.exitCode === null, - pid: sidecar?.pid || null, config: { backendUrl: live.backendUrl, - identifier: live.identifier || 'auto-detected', + identifier: live.identifier || hostname(), + monitorPort: (live as any).monitorPort || null, reportIntervalSec: live.reportIntervalSec, hasApiKey: Boolean(live.apiKey), }, + telemetry: collectTelemetry(), }; }, })); - logger.info('HarborForge Monitor plugin registered'); + // 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(); + }, + })); + + logger.info('HarborForge plugin registered (id: harbor-forge)'); }, }; diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index 816481d..cba0d12 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -1,8 +1,8 @@ { - "id": "harborforge-monitor", - "name": "HarborForge Monitor", - "version": "0.1.0", - "description": "Server monitoring plugin for HarborForge - streams telemetry to Monitor", + "id": "harbor-forge", + "name": "HarborForge", + "version": "0.2.0", + "description": "HarborForge plugin for OpenClaw - project management, monitoring, and CLI integration", "entry": "./index.js", "configSchema": { "type": "object", @@ -11,7 +11,7 @@ "enabled": { "type": "boolean", "default": true, - "description": "Enable the monitor plugin" + "description": "Enable the HarborForge plugin" }, "backendUrl": { "type": "string", @@ -26,6 +26,10 @@ "type": "string", "description": "API Key from HarborForge Monitor admin panel (optional but required for authentication)" }, + "monitorPort": { + "type": "number", + "description": "Local port for communication between HarborForge Monitor and this plugin" + }, "reportIntervalSec": { "type": "number", "default": 30, diff --git a/scripts/install.mjs b/scripts/install.mjs index e343d54..718dabf 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -1,7 +1,13 @@ #!/usr/bin/env node /** - * HarborForge Monitor Plugin Installer v0.1.0 + * HarborForge Plugin Installer v0.2.0 + * + * Changes from v0.1.0: + * - Plugin renamed from harborforge-monitor to harbor-forge + * - Sidecar server removed (telemetry served directly by plugin) + * - Added --install-cli flag for building and installing the hf CLI + * - skills/hf/ only installed when --install-cli is present */ import { execSync } from 'child_process'; @@ -11,8 +17,7 @@ import { copyFileSync, readdirSync, rmSync, - readFileSync, - writeFileSync, + chmodSync, } from 'fs'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; @@ -21,9 +26,9 @@ import { homedir, platform } from 'os'; const __filename = fileURLToPath(import.meta.url); const __dirname = resolve(dirname(__filename), '..'); -const PLUGIN_NAME = 'harborforge-monitor'; +const PLUGIN_NAME = 'harbor-forge'; +const OLD_PLUGIN_NAME = 'harborforge-monitor'; const PLUGIN_SRC_DIR = join(__dirname, 'plugin'); -const SERVER_SRC_DIR = join(__dirname, 'server'); const SKILLS_SRC_DIR = join(__dirname, 'skills'); const args = process.argv.slice(2); @@ -34,6 +39,7 @@ const options = { verbose: args.includes('--verbose') || args.includes('-v'), uninstall: args.includes('--uninstall'), installOnly: args.includes('--install'), + installCli: args.includes('--install-cli'), }; const profileIdx = args.indexOf('--openclaw-profile-path'); @@ -83,19 +89,21 @@ function unsetOpenclawConfig(key) { try { exec(`openclaw config unset ${key}`, { silent: true }); } catch {} } -function copyDir(src, dest) { +function copyDir(src, dest, { exclude = [] } = {}) { mkdirSync(dest, { recursive: true }); for (const entry of readdirSync(src, { withFileTypes: true })) { const s = join(src, entry.name); const d = join(dest, entry.name); if (entry.name === 'node_modules') continue; - entry.isDirectory() ? copyDir(s, d) : copyFileSync(s, d); + if (exclude.includes(entry.name)) continue; + entry.isDirectory() ? copyDir(s, d, { exclude }) : copyFileSync(s, d); } } function detectEnvironment() { - logStep(1, 5, 'Detecting environment...'); - const env = { platform: platform(), nodeVersion: null }; + const totalSteps = options.installCli ? 6 : 5; + logStep(1, totalSteps, 'Detecting environment...'); + const env = { platform: platform(), nodeVersion: null, goVersion: null }; try { env.nodeVersion = exec('node --version', { silent: true }).trim(); @@ -109,19 +117,34 @@ function detectEnvironment() { } catch { logWarn('openclaw CLI not in PATH'); } + + if (options.installCli) { + try { + env.goVersion = exec('go version', { silent: true }).trim(); + logOk(env.goVersion); + } catch { + logWarn('Go not found (needed for --install-cli)'); + } + } return env; } function checkDeps(env) { - if (options.skipCheck) { logStep(2, 5, 'Skipping dep checks'); return; } - logStep(2, 5, 'Checking dependencies...'); + const totalSteps = options.installCli ? 6 : 5; + if (options.skipCheck) { logStep(2, totalSteps, 'Skipping dep checks'); return; } + logStep(2, totalSteps, 'Checking dependencies...'); let fail = false; if (!env.nodeVersion || parseInt(env.nodeVersion.slice(1)) < 18) { logErr('Node.js 18+ required'); fail = true; } + + if (options.installCli && !env.goVersion) { + logErr('Go is required for --install-cli'); + fail = true; + } if (fail) { log('\nInstall missing deps and retry.', 'red'); @@ -132,7 +155,8 @@ function checkDeps(env) { } async function build() { - logStep(3, 5, 'Building plugin...'); + const totalSteps = options.installCli ? 6 : 5; + logStep(3, totalSteps, 'Building plugin...'); log(' Building TypeScript plugin...', 'blue'); exec('npm install', { cwd: PLUGIN_SRC_DIR, silent: !options.verbose }); @@ -141,11 +165,18 @@ async function build() { } function clearInstallTargets(openclawPath) { + // Remove new plugin dir const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); if (existsSync(destDir)) { rmSync(destDir, { recursive: true, force: true }); logOk(`Removed ${destDir}`); } + // Remove old plugin dir if it exists + const oldDestDir = join(openclawPath, 'plugins', OLD_PLUGIN_NAME); + if (existsSync(oldDestDir)) { + rmSync(oldDestDir, { recursive: true, force: true }); + logOk(`Removed old plugin dir ${oldDestDir}`); + } } function cleanupConfig(openclawPath) { @@ -153,20 +184,31 @@ function cleanupConfig(openclawPath) { try { const allow = getOpenclawConfig('plugins.allow', []); - const idx = allow.indexOf(PLUGIN_NAME); - if (idx !== -1) { - allow.splice(idx, 1); - setOpenclawConfig('plugins.allow', allow); - logOk('Removed from allow list'); + // Remove both old and new names + for (const name of [PLUGIN_NAME, OLD_PLUGIN_NAME]) { + const idx = allow.indexOf(name); + if (idx !== -1) { + allow.splice(idx, 1); + logOk(`Removed ${name} from allow list`); + } } + setOpenclawConfig('plugins.allow', allow); unsetOpenclawConfig(`plugins.entries.${PLUGIN_NAME}`); - logOk('Removed plugin entry'); + unsetOpenclawConfig(`plugins.entries.${OLD_PLUGIN_NAME}`); + logOk('Removed plugin entries'); const paths = getOpenclawConfig('plugins.load.paths', []); - const pidx = paths.indexOf(destDir); - if (pidx !== -1) { - paths.splice(pidx, 1); + const oldDestDir = join(openclawPath, 'plugins', OLD_PLUGIN_NAME); + let changed = false; + for (const p of [destDir, oldDestDir]) { + const pidx = paths.indexOf(p); + if (pidx !== -1) { + paths.splice(pidx, 1); + changed = true; + } + } + if (changed) { setOpenclawConfig('plugins.load.paths', paths); logOk('Removed from load paths'); } @@ -176,8 +218,9 @@ function cleanupConfig(openclawPath) { } async function install() { - if (options.buildOnly) { logStep(4, 5, 'Skipping install (--build-only)'); return null; } - logStep(4, 5, 'Installing...'); + const totalSteps = options.installCli ? 6 : 5; + if (options.buildOnly) { logStep(4, totalSteps, 'Skipping install (--build-only)'); return null; } + logStep(4, totalSteps, 'Installing...'); const openclawPath = resolveOpenclawPath(); const pluginsDir = join(openclawPath, 'plugins'); @@ -186,29 +229,34 @@ async function install() { log(` OpenClaw path: ${openclawPath}`, 'blue'); if (existsSync(destDir)) { - logWarn('Existing install detected, uninstalling before install...'); + logWarn('Existing install detected, cleaning up...'); clearInstallTargets(openclawPath); cleanupConfig(openclawPath); } - if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true }); + // Clean up old plugin name if present + const oldDestDir = join(pluginsDir, OLD_PLUGIN_NAME); + if (existsSync(oldDestDir)) { + logWarn('Old plugin (harborforge-monitor) detected, removing...'); + rmSync(oldDestDir, { recursive: true, force: true }); + cleanupConfig(openclawPath); + } - // Copy compiled plugin + // Copy compiled plugin (no server directory — sidecar removed) mkdirSync(destDir, { recursive: true }); copyDir(PLUGIN_SRC_DIR, destDir); + logOk(`Plugin files → ${destDir}`); - // Copy telemetry server - const serverDestDir = join(destDir, 'server'); - mkdirSync(serverDestDir, { recursive: true }); - copyDir(SERVER_SRC_DIR, serverDestDir); - logOk(`Server files → ${serverDestDir}`); - - // Copy skills + // Copy skills (exclude hf/ unless --install-cli) if (existsSync(SKILLS_SRC_DIR)) { const skillsDestDir = join(openclawPath, 'skills'); - mkdirSync(skillsDestDir, { recursive: true }); - copyDir(SKILLS_SRC_DIR, skillsDestDir); - logOk(`Skills → ${skillsDestDir}`); + const excludeSkills = options.installCli ? [] : ['hf']; + copyDir(SKILLS_SRC_DIR, skillsDestDir, { exclude: excludeSkills }); + if (options.installCli) { + logOk(`Skills (including hf) → ${skillsDestDir}`); + } else { + logOk(`Skills (hf skipped, use --install-cli) → ${skillsDestDir}`); + } } // Install runtime deps @@ -218,9 +266,51 @@ async function install() { return { destDir }; } +async function installCli() { + if (!options.installCli) return; + const totalSteps = 6; + logStep(5, totalSteps, 'Building and installing hf CLI...'); + + const openclawPath = resolveOpenclawPath(); + const binDir = join(openclawPath, 'bin'); + mkdirSync(binDir, { recursive: true }); + + // Find CLI source — look for HarborForge.Cli relative to project root + const projectRoot = resolve(__dirname, '..'); + const cliDir = join(projectRoot, 'HarborForge.Cli'); + + if (!existsSync(cliDir)) { + // Try parent directory (monorepo layout) + const monoCliDir = resolve(projectRoot, '..', 'HarborForge.Cli'); + if (!existsSync(monoCliDir)) { + logErr(`Cannot find HarborForge.Cli at ${cliDir} or ${monoCliDir}`); + logWarn('Skipping CLI installation'); + return; + } + } + + const effectiveCliDir = existsSync(cliDir) + ? cliDir + : resolve(projectRoot, '..', 'HarborForge.Cli'); + + log(` Building hf from ${effectiveCliDir}...`, 'blue'); + + try { + const hfBinary = join(binDir, 'hf'); + exec(`go build -o ${hfBinary} ./cmd/hf`, { cwd: effectiveCliDir, silent: !options.verbose }); + chmodSync(hfBinary, 0o755); + logOk(`hf binary → ${hfBinary}`); + } catch (err) { + logErr(`Failed to build hf CLI: ${err.message}`); + logWarn('CLI installation failed, plugin still installed'); + } +} + async function configure() { - if (options.buildOnly) { logStep(5, 5, 'Skipping config'); return; } - logStep(5, 5, 'Configuring OpenClaw...'); + const totalSteps = options.installCli ? 6 : 5; + const step = options.installCli ? 6 : 5; + if (options.buildOnly) { logStep(step, totalSteps, 'Skipping config'); return; } + logStep(step, totalSteps, 'Configuring OpenClaw...'); const openclawPath = resolveOpenclawPath(); const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); @@ -240,8 +330,7 @@ async function configure() { } logOk(`plugins.allow includes ${PLUGIN_NAME}`); - // Note: apiKey must be configured manually by user - logOk('Plugin configured (remember to set apiKey in plugins.entries.harborforge-monitor.config)'); + logOk('Plugin configured (remember to set apiKey in plugins.entries.harbor-forge.config)'); } catch (err) { logWarn(`Config failed: ${err.message}`); @@ -249,11 +338,12 @@ async function configure() { } function summary() { - logStep(5, 5, 'Done!'); + const totalSteps = options.installCli ? 6 : 5; + logStep(totalSteps, totalSteps, 'Done!'); console.log(''); - log('╔══════════════════════════════════════════════╗', 'cyan'); - log('║ HarborForge Monitor v0.1.0 Install Complete ║', 'cyan'); - log('╚══════════════════════════════════════════════╝', 'cyan'); + log('╔════════════════════════════════════════════╗', 'cyan'); + log('║ HarborForge v0.2.0 Install Complete ║', 'cyan'); + log('╚════════════════════════════════════════════╝', 'cyan'); if (options.buildOnly) { log('\nBuild-only — plugin not installed.', 'yellow'); @@ -263,11 +353,11 @@ function summary() { console.log(''); log('Next steps:', 'blue'); log(' 1. Register server in HarborForge Monitor to get apiKey', 'cyan'); - log(' 2. Edit ~/.openclaw/openclaw.json under plugins.entries.harborforge-monitor.config:', 'cyan'); + log(' 2. Edit ~/.openclaw/openclaw.json under plugins.entries.harbor-forge.config:', 'cyan'); log(' {', 'cyan'); log(' "plugins": {', 'cyan'); log(' "entries": {', 'cyan'); - log(' "harborforge-monitor": {', 'cyan'); + log(' "harbor-forge": {', 'cyan'); log(' "enabled": true,', 'cyan'); log(' "config": {', 'cyan'); log(' "enabled": true,', 'cyan'); @@ -278,22 +368,36 @@ function summary() { log(' }', 'cyan'); log(' }', 'cyan'); log(' 3. openclaw gateway restart', 'cyan'); + + if (options.installCli) { + console.log(''); + log(' hf CLI installed to ~/.openclaw/bin/hf', 'green'); + log(' Ensure ~/.openclaw/bin is in your PATH', 'cyan'); + } console.log(''); } async function uninstall() { - log('Uninstalling HarborForge Monitor...', 'cyan'); + log('Uninstalling HarborForge...', 'cyan'); const openclawPath = resolveOpenclawPath(); clearInstallTargets(openclawPath); cleanupConfig(openclawPath); + + // Remove CLI binary if present + const hfBinary = join(openclawPath, 'bin', 'hf'); + if (existsSync(hfBinary)) { + rmSync(hfBinary, { force: true }); + logOk('Removed hf CLI binary'); + } + log('\nRun: openclaw gateway restart', 'yellow'); } async function main() { console.log(''); - log('╔══════════════════════════════════════════════╗', 'cyan'); - log('║ HarborForge Monitor Plugin Installer v0.1.0 ║', 'cyan'); - log('╚══════════════════════════════════════════════╝', 'cyan'); + log('╔════════════════════════════════════════════╗', 'cyan'); + log('║ HarborForge Plugin Installer v0.2.0 ║', 'cyan'); + log('╚════════════════════════════════════════════╝', 'cyan'); console.log(''); try { @@ -309,6 +413,9 @@ async function main() { if (!options.buildOnly) { await install(); + if (options.installCli) { + await installCli(); + } await configure(); } diff --git a/server/telemetry.mjs b/server/telemetry.mjs deleted file mode 100644 index fcb6e6d..0000000 --- a/server/telemetry.mjs +++ /dev/null @@ -1,380 +0,0 @@ -/** - * HarborForge Monitor Telemetry Server - * - * Runs as separate process from Gateway. - * Collects system metrics and OpenClaw status, sends to Monitor. - */ -import { readFile, access, readdir } from 'fs/promises'; -import { constants } from 'fs'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { platform, hostname, freemem, totalmem, uptime, loadavg } from 'os'; - -const execAsync = promisify(exec); - -// Config from environment (set by plugin) -const openclawPath = process.env.OPENCLAW_PATH || `${process.env.HOME}/.openclaw`; -const CONFIG = { - backendUrl: process.env.HF_MONITOR_BACKEND_URL || 'https://monitor.hangman-lab.top', - identifier: process.env.HF_MONITOR_IDENTIFIER || hostname(), - apiKey: process.env.HF_MONITOR_API_KEY, - reportIntervalSec: parseInt(process.env.HF_MONITOR_REPORT_INTERVAL || '30', 10), - httpFallbackIntervalSec: parseInt(process.env.HF_MONITOR_HTTP_FALLBACK_INTERVAL || '60', 10), - logLevel: process.env.HF_MONITOR_LOG_LEVEL || 'info', - openclawPath, - pluginVersion: process.env.HF_MONITOR_PLUGIN_VERSION || 'unknown', - cachePath: process.env.HF_MONITOR_CACHE_PATH || `${openclawPath}/telemetry_cache.json`, - maxCacheSize: parseInt(process.env.HF_MONITOR_MAX_CACHE_SIZE || '100', 10), -}; - -// Logging -const log = { - debug: (...args) => CONFIG.logLevel === 'debug' && console.log('[DEBUG]', ...args), - info: (...args) => ['debug', 'info'].includes(CONFIG.logLevel) && console.log('[INFO]', ...args), - warn: (...args) => console.log('[WARN]', ...args), - error: (...args) => console.error('[ERROR]', ...args), -}; - -// State -let wsConnection = null; -let lastSuccessfulSend = null; -let consecutiveFailures = 0; -let isShuttingDown = false; -let cachedOpenclawVersion = null; - -/** - * Collect system metrics - */ -async function collectSystemMetrics() { - try { - const cpuUsage = await getCpuUsage(); - const memTotal = totalmem(); - const memFree = freemem(); - const memUsed = memTotal - memFree; - const diskInfo = await getDiskUsage(); - const loadAvg = platform() !== 'win32' ? loadavg() : [0, 0, 0]; - - return { - cpu_pct: cpuUsage, - mem_pct: Math.round((memUsed / memTotal) * 100 * 10) / 10, - mem_used_mb: Math.round(memUsed / 1024 / 1024), - mem_total_mb: Math.round(memTotal / 1024 / 1024), - disk_pct: diskInfo.usedPct, - disk_used_gb: Math.round(diskInfo.usedGB * 10) / 10, - disk_total_gb: Math.round(diskInfo.totalGB * 10) / 10, - swap_pct: diskInfo.swapUsedPct || 0, - uptime_seconds: Math.floor(uptime()), - load_avg: [ - Math.round(loadAvg[0] * 100) / 100, - Math.round(loadAvg[1] * 100) / 100, - Math.round(loadAvg[2] * 100) / 100, - ], - platform: platform(), - hostname: hostname(), - }; - } catch (err) { - log.error('Failed to collect system metrics:', err.message); - return {}; - } -} - -/** - * Get CPU usage percentage - */ -async function getCpuUsage() { - try { - if (platform() === 'linux') { - const { stdout } = await execAsync("top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1"); - const usage = parseFloat(stdout.trim()); - return isNaN(usage) ? 0 : Math.round(usage * 10) / 10; - } else if (platform() === 'darwin') { - const { stdout } = await execAsync("top -l 1 | grep 'CPU usage' | awk '{print $3}' | cut -d'%' -f1"); - const usage = parseFloat(stdout.trim()); - return isNaN(usage) ? 0 : Math.round(usage * 10) / 10; - } - } catch { - try { - const stat = await readFile('/proc/stat', 'utf8'); - const cpuLine = stat.split('\n')[0]; - const parts = cpuLine.split(/\s+/).slice(1).map(Number); - const idle = parts[3]; - const total = parts.reduce((a, b) => a + b, 0); - const usage = ((total - idle) / total) * 100; - return Math.round(usage * 10) / 10; - } catch { - return 0; - } - } - return 0; -} - -/** - * Get disk usage - */ -async function getDiskUsage() { - try { - if (platform() === 'linux' || platform() === 'darwin') { - const { stdout } = await execAsync("df -h / | tail -1 | awk '{print $2,$3,$5}'"); - const [total, used, pct] = stdout.trim().split(/\s+/); - return { - totalGB: parseSizeToGB(total), - usedGB: parseSizeToGB(used), - usedPct: parseInt(pct.replace('%', ''), 10), - }; - } - } catch (err) { - log.debug('Failed to get disk usage:', err.message); - } - return { totalGB: 0, usedGB: 0, usedPct: 0 }; -} - -function parseSizeToGB(size) { - const num = parseFloat(size); - if (size.includes('T')) return num * 1024; - if (size.includes('G')) return num; - if (size.includes('M')) return num / 1024; - if (size.includes('K')) return num / 1024 / 1024; - return num; -} - -async function resolveOpenclawVersion() { - if (cachedOpenclawVersion) return cachedOpenclawVersion; - - try { - const { stdout } = await execAsync('openclaw --version'); - const version = stdout.trim(); - cachedOpenclawVersion = version || 'unknown'; - return cachedOpenclawVersion; - } catch (err) { - log.debug('Failed to resolve OpenClaw version:', err.message); - cachedOpenclawVersion = 'unknown'; - return cachedOpenclawVersion; - } -} - -/** - * Collect OpenClaw status - */ -async function collectOpenclawStatus() { - try { - const [agents, openclawVersion] = await Promise.all([ - getOpenclawAgents(), - resolveOpenclawVersion(), - ]); - return { - openclawVersion, - pluginVersion: CONFIG.pluginVersion, - agent_count: agents.length, - agents: agents.map(a => ({ - id: a.id, - name: a.name, - status: a.status, - })), - }; - } catch (err) { - log.debug('Failed to collect OpenClaw status:', err.message); - return { - openclawVersion: await resolveOpenclawVersion(), - pluginVersion: CONFIG.pluginVersion, - agent_count: 0, - agents: [], - }; - } -} - -/** - * Get list of OpenClaw agents from local state - */ -function extractJsonPrefix(text) { - const trimmed = text.trim(); - if (!trimmed) return null; - - const startsWith = trimmed[0]; - if (startsWith !== '[' && startsWith !== '{') return null; - - let depth = 0; - let inString = false; - let escape = false; - for (let i = 0; i < trimmed.length; i += 1) { - const ch = trimmed[i]; - if (escape) { - escape = false; - continue; - } - if (ch === '\\') { - escape = true; - continue; - } - if (ch === '"') { - inString = !inString; - continue; - } - if (inString) continue; - if (ch === '[' || ch === '{') depth += 1; - if (ch === ']' || ch === '}') depth -= 1; - if (depth === 0) { - return trimmed.slice(0, i + 1); - } - } - return null; -} - -async function getOpenclawAgents() { - try { - try { - const { stdout } = await execAsync('openclaw agents list --json 2>/dev/null'); - const jsonPrefix = extractJsonPrefix(stdout); - if (jsonPrefix) { - const agents = JSON.parse(jsonPrefix); - if (Array.isArray(agents)) { - return agents.map((agent) => ({ - id: agent.id, - name: agent.name || agent.id, - status: agent.isDefault ? 'default' : 'configured', - })); - } - } - } catch (err) { - log.debug('Failed to get agents from `openclaw agents list --json`:', err.message); - } - - const agentConfigPath = `${CONFIG.openclawPath}/agents.json`; - try { - await access(agentConfigPath, constants.R_OK); - const data = JSON.parse(await readFile(agentConfigPath, 'utf8')); - if (Array.isArray(data.agents) && data.agents.length > 0) { - return data.agents; - } - } catch { - // fall through to directory-based discovery - } - - const agentsDir = `${CONFIG.openclawPath}/agents`; - await access(agentsDir, constants.R_OK); - const entries = await readdir(agentsDir, { withFileTypes: true }); - return entries - .filter((entry) => entry.isDirectory()) - .filter((entry) => entry.name !== 'main') - .map((entry) => ({ - id: entry.name, - name: entry.name, - status: 'configured', - })); - } catch { - return []; - } -} - -/** - * Build telemetry payload - */ -async function buildPayload() { - const system = await collectSystemMetrics(); - const openclaw = await collectOpenclawStatus(); - - return { - identifier: CONFIG.identifier, - timestamp: new Date().toISOString(), - ...system, - openclaw_version: openclaw.openclawVersion, - plugin_version: openclaw.pluginVersion, - agents: openclaw.agents, - }; -} - -/** - * Send telemetry via HTTP - */ -async function sendHttpHeartbeat() { - try { - const payload = await buildPayload(); - log.debug('Sending HTTP heartbeat...'); - - const headers = { - 'Content-Type': 'application/json', - 'X-Server-Identifier': CONFIG.identifier, - }; - - if (CONFIG.apiKey) { - headers['X-API-Key'] = CONFIG.apiKey; - } - - const response = await fetch(`${CONFIG.backendUrl}/monitor/server/heartbeat-v2`, { - method: 'POST', - headers, - body: JSON.stringify(payload), - }); - - if (response.ok) { - log.debug('HTTP heartbeat sent successfully'); - lastSuccessfulSend = Date.now(); - consecutiveFailures = 0; - return true; - } else { - throw new Error(`HTTP ${response.status}: ${await response.text()}`); - } - } catch (err) { - log.error('HTTP heartbeat failed:', err.message); - consecutiveFailures++; - return false; - } -} - -/** - * Main reporting loop - */ -async function reportingLoop() { - while (!isShuttingDown) { - try { - const success = await sendHttpHeartbeat(); - - let interval = CONFIG.reportIntervalSec * 1000; - if (!success) { - const backoff = Math.min(consecutiveFailures * 10000, 300000); - interval = Math.max(interval, backoff); - log.info(`Retry in ${interval}ms (backoff)`); - } - - await new Promise(resolve => setTimeout(resolve, interval)); - } catch (err) { - log.error('Reporting loop error:', err.message); - await new Promise(resolve => setTimeout(resolve, 30000)); - } - } -} - -/** - * Graceful shutdown - */ -function shutdown() { - log.info('Shutting down telemetry server...'); - isShuttingDown = true; - - if (wsConnection) { - wsConnection.close(); - } - - sendHttpHeartbeat().finally(() => { - process.exit(0); - }); -} - -// Handle signals -process.on('SIGTERM', shutdown); -process.on('SIGINT', shutdown); - -// Start -log.info('HarborForge Monitor Telemetry Server starting...'); -log.info('Config:', { - identifier: CONFIG.identifier, - backendUrl: CONFIG.backendUrl, - reportIntervalSec: CONFIG.reportIntervalSec, - hasApiKey: !!CONFIG.apiKey, - pluginVersion: CONFIG.pluginVersion, -}); - -if (!CONFIG.apiKey) { - log.warn('Missing HF_MONITOR_API_KEY environment variable - API authentication will fail'); -} - -reportingLoop(); From 78a61e0fb2a7b6799fd9550c2fc70027c3bb1058 Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 16:07:01 +0000 Subject: [PATCH 3/5] Integrate plugin with local monitor bridge --- README.md | 15 ++++++- plugin/core/live-config.d.ts | 1 + plugin/core/live-config.d.ts.map | 2 +- plugin/core/live-config.js | 2 +- plugin/core/live-config.js.map | 2 +- plugin/core/live-config.ts | 3 +- plugin/core/monitor-bridge.d.ts | 41 +++++++++++++++++ plugin/core/monitor-bridge.d.ts.map | 1 + plugin/core/monitor-bridge.js | 44 ++++++++++++++++++ plugin/core/monitor-bridge.js.map | 1 + plugin/core/monitor-bridge.ts | 69 +++++++++++++++++++++++++++++ plugin/index.ts | 49 ++++++++++++++++++++ 12 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 plugin/core/monitor-bridge.d.ts create mode 100644 plugin/core/monitor-bridge.d.ts.map create mode 100644 plugin/core/monitor-bridge.js create mode 100644 plugin/core/monitor-bridge.js.map create mode 100644 plugin/core/monitor-bridge.ts diff --git a/README.md b/README.md index d61b5dc..2c9bfdc 100644 --- a/README.md +++ b/README.md @@ -84,13 +84,14 @@ node scripts/install.mjs --verbose { "plugins": { "entries": { - "harborforge-monitor": { + "harbor-forge": { "enabled": true, "config": { "enabled": true, "backendUrl": "https://monitor.hangman-lab.top", "identifier": "my-server-01", "apiKey": "your-api-key-here", + "monitorPort": 9100, "reportIntervalSec": 30, "httpFallbackIntervalSec": 60, "logLevel": "info" @@ -115,10 +116,22 @@ openclaw gateway restart | `backendUrl` | string | `https://monitor.hangman-lab.top` | Monitor 后端地址 | | `identifier` | string | 自动检测 hostname | 服务器标识符 | | `apiKey` | string | 必填 | HarborForge Monitor 生成的服务器 API Key | +| `monitorPort` | number | `9100`(示例) | 本地桥接端口;插件通过 `127.0.0.1:` 与 HarborForge.Monitor 通信 | | `reportIntervalSec` | number | `30` | 报告间隔(秒) | | `httpFallbackIntervalSec` | number | `60` | HTTP 回退间隔(秒) | | `logLevel` | string | `"info"` | 日志级别: debug/info/warn/error | +## Monitor 本地桥接 + +当插件配置了 `monitorPort`,并且 HarborForge.Monitor 也以相同的 `MONITOR_PORT` 启动时: + +- Monitor 会在 `127.0.0.1:` 暴露本地桥接服务 +- 插件可探测 `GET /health` +- 插件工具 `harborforge_monitor_telemetry` 可读取 `GET /telemetry` +- 若桥接端口未配置或不可达,插件仍然正常工作,只是不会拿到 Monitor 的宿主机遥测补充数据 + +这条链路是**可选增强**,不是插件或 Monitor 心跳上报的前置依赖。 + ## 收集的指标 ### 系统指标 diff --git a/plugin/core/live-config.d.ts b/plugin/core/live-config.d.ts index 244eb91..e455432 100644 --- a/plugin/core/live-config.d.ts +++ b/plugin/core/live-config.d.ts @@ -3,6 +3,7 @@ export interface HarborForgeMonitorConfig { backendUrl?: string; identifier?: string; apiKey?: string; + monitorPort?: number; reportIntervalSec?: number; httpFallbackIntervalSec?: number; logLevel?: 'debug' | 'info' | 'warn' | 'error'; diff --git a/plugin/core/live-config.d.ts.map b/plugin/core/live-config.d.ts.map index 9188b63..26a5054 100644 --- a/plugin/core/live-config.d.ts.map +++ b/plugin/core/live-config.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"live-config.d.ts","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,qBAAqB,EAC1B,QAAQ,EAAE,wBAAwB,GACjC,wBAAwB,CAqB1B"} \ No newline at end of file +{"version":3,"file":"live-config.d.ts","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,qBAAqB,EAC1B,QAAQ,EAAE,wBAAwB,GACjC,wBAAwB,CAqB1B"} \ No newline at end of file diff --git a/plugin/core/live-config.js b/plugin/core/live-config.js index a4e990e..b3201d3 100644 --- a/plugin/core/live-config.js +++ b/plugin/core/live-config.js @@ -5,7 +5,7 @@ function getLivePluginConfig(api, fallback) { const root = api.config || {}; const plugins = root.plugins || {}; const entries = plugins.entries || {}; - const entry = entries['harborforge-monitor'] || {}; + const entry = entries['harbor-forge'] || {}; const cfg = entry.config || {}; if (Object.keys(cfg).length > 0 || Object.keys(entry).length > 0) { return { diff --git a/plugin/core/live-config.js.map b/plugin/core/live-config.js.map index 197010e..650d6ac 100644 --- a/plugin/core/live-config.js.map +++ b/plugin/core/live-config.js.map @@ -1 +1 @@ -{"version":3,"file":"live-config.js","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":";;AAcA,kDAwBC;AAxBD,SAAgB,mBAAmB,CACjC,GAA0B,EAC1B,QAAkC;IAElC,MAAM,IAAI,GAAI,GAAG,CAAC,MAAkC,IAAI,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAI,IAAI,CAAC,OAAmC,IAAI,EAAE,CAAC;IAChE,MAAM,OAAO,GAAI,OAAO,CAAC,OAAmC,IAAI,EAAE,CAAC;IACnE,MAAM,KAAK,GAAI,OAAO,CAAC,qBAAqB,CAA6B,IAAI,EAAE,CAAC;IAChF,MAAM,GAAG,GAAI,KAAK,CAAC,MAAkC,IAAI,EAAE,CAAC;IAE5D,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjE,OAAO;YACL,GAAG,QAAQ;YACX,GAAG,GAAG;YACN,OAAO,EACL,OAAO,GAAG,CAAC,OAAO,KAAK,SAAS;gBAC9B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;oBAClC,CAAC,CAAC,KAAK,CAAC,OAAO;oBACf,CAAC,CAAC,QAAQ,CAAC,OAAO;SACG,CAAC;IAChC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"} \ No newline at end of file +{"version":3,"file":"live-config.js","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":";;AAeA,kDAwBC;AAxBD,SAAgB,mBAAmB,CACjC,GAA0B,EAC1B,QAAkC;IAElC,MAAM,IAAI,GAAI,GAAG,CAAC,MAAkC,IAAI,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAI,IAAI,CAAC,OAAmC,IAAI,EAAE,CAAC;IAChE,MAAM,OAAO,GAAI,OAAO,CAAC,OAAmC,IAAI,EAAE,CAAC;IACnE,MAAM,KAAK,GAAI,OAAO,CAAC,cAAc,CAA6B,IAAI,EAAE,CAAC;IACzE,MAAM,GAAG,GAAI,KAAK,CAAC,MAAkC,IAAI,EAAE,CAAC;IAE5D,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjE,OAAO;YACL,GAAG,QAAQ;YACX,GAAG,GAAG;YACN,OAAO,EACL,OAAO,GAAG,CAAC,OAAO,KAAK,SAAS;gBAC9B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;oBAClC,CAAC,CAAC,KAAK,CAAC,OAAO;oBACf,CAAC,CAAC,QAAQ,CAAC,OAAO;SACG,CAAC;IAChC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"} \ No newline at end of file diff --git a/plugin/core/live-config.ts b/plugin/core/live-config.ts index 86f60ef..a3bc3c3 100644 --- a/plugin/core/live-config.ts +++ b/plugin/core/live-config.ts @@ -3,6 +3,7 @@ export interface HarborForgeMonitorConfig { backendUrl?: string; identifier?: string; apiKey?: string; + monitorPort?: number; reportIntervalSec?: number; httpFallbackIntervalSec?: number; logLevel?: 'debug' | 'info' | 'warn' | 'error'; @@ -19,7 +20,7 @@ export function getLivePluginConfig( const root = (api.config as Record) || {}; const plugins = (root.plugins as Record) || {}; const entries = (plugins.entries as Record) || {}; - const entry = (entries['harborforge-monitor'] as Record) || {}; + const entry = (entries['harbor-forge'] as Record) || {}; const cfg = (entry.config as Record) || {}; if (Object.keys(cfg).length > 0 || Object.keys(entry).length > 0) { diff --git a/plugin/core/monitor-bridge.d.ts b/plugin/core/monitor-bridge.d.ts new file mode 100644 index 0000000..84f626f --- /dev/null +++ b/plugin/core/monitor-bridge.d.ts @@ -0,0 +1,41 @@ +/** + * Monitor Bridge Client + * + * Queries the local HarborForge.Monitor bridge endpoint on MONITOR_PORT + * to enrich plugin telemetry with host/hardware data. + * + * If the bridge is unreachable, all methods return null gracefully — + * the plugin continues to function without Monitor data. + */ +export interface MonitorHealth { + status: string; + monitor_version: string; + identifier: string; +} +export interface MonitorTelemetryResponse { + status: string; + monitor_version: string; + identifier: string; + telemetry?: { + identifier: string; + plugin_version: string; + cpu_pct: number; + mem_pct: number; + disk_pct: number; + swap_pct: number; + load_avg: number[]; + uptime_seconds: number; + nginx_installed: boolean; + nginx_sites: string[]; + }; + last_updated?: string; +} +export declare class MonitorBridgeClient { + private baseUrl; + private timeoutMs; + constructor(port: number, timeoutMs?: number); + health(): Promise; + telemetry(): Promise; + private fetchJson; +} +//# sourceMappingURL=monitor-bridge.d.ts.map \ No newline at end of file diff --git a/plugin/core/monitor-bridge.d.ts.map b/plugin/core/monitor-bridge.d.ts.map new file mode 100644 index 0000000..bffa006 --- /dev/null +++ b/plugin/core/monitor-bridge.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"monitor-bridge.d.ts","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;QACzB,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO;IAKpC,MAAM,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIvC,SAAS,IAAI,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;YAI7C,SAAS;CAgBxB"} \ No newline at end of file diff --git a/plugin/core/monitor-bridge.js b/plugin/core/monitor-bridge.js new file mode 100644 index 0000000..5657649 --- /dev/null +++ b/plugin/core/monitor-bridge.js @@ -0,0 +1,44 @@ +"use strict"; +/** + * Monitor Bridge Client + * + * Queries the local HarborForge.Monitor bridge endpoint on MONITOR_PORT + * to enrich plugin telemetry with host/hardware data. + * + * If the bridge is unreachable, all methods return null gracefully — + * the plugin continues to function without Monitor data. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MonitorBridgeClient = void 0; +class MonitorBridgeClient { + baseUrl; + timeoutMs; + constructor(port, timeoutMs = 3000) { + this.baseUrl = `http://127.0.0.1:${port}`; + this.timeoutMs = timeoutMs; + } + async health() { + return this.fetchJson('/health'); + } + async telemetry() { + return this.fetchJson('/telemetry'); + } + async fetchJson(path) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + const response = await fetch(`${this.baseUrl}${path}`, { + signal: controller.signal, + }); + clearTimeout(timeout); + if (!response.ok) + return null; + return (await response.json()); + } + catch { + return null; + } + } +} +exports.MonitorBridgeClient = MonitorBridgeClient; +//# sourceMappingURL=monitor-bridge.js.map \ No newline at end of file diff --git a/plugin/core/monitor-bridge.js.map b/plugin/core/monitor-bridge.js.map new file mode 100644 index 0000000..cdeccf0 --- /dev/null +++ b/plugin/core/monitor-bridge.js.map @@ -0,0 +1 @@ +{"version":3,"file":"monitor-bridge.js","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA2BH,MAAa,mBAAmB;IACtB,OAAO,CAAS;IAChB,SAAS,CAAS;IAE1B,YAAY,IAAY,EAAE,SAAS,GAAG,IAAI;QACxC,IAAI,CAAC,OAAO,GAAG,oBAAoB,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,IAAI,CAAC,SAAS,CAAgB,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,SAAS,CAA2B,YAAY,CAAC,CAAC;IAChE,CAAC;IAEO,KAAK,CAAC,SAAS,CAAI,IAAY;QACrC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YAC9B,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAjCD,kDAiCC"} \ No newline at end of file diff --git a/plugin/core/monitor-bridge.ts b/plugin/core/monitor-bridge.ts new file mode 100644 index 0000000..1da54e0 --- /dev/null +++ b/plugin/core/monitor-bridge.ts @@ -0,0 +1,69 @@ +/** + * Monitor Bridge Client + * + * Queries the local HarborForge.Monitor bridge endpoint on MONITOR_PORT + * to enrich plugin telemetry with host/hardware data. + * + * If the bridge is unreachable, all methods return null gracefully — + * the plugin continues to function without Monitor data. + */ + +export interface MonitorHealth { + status: string; + monitor_version: string; + identifier: string; +} + +export interface MonitorTelemetryResponse { + status: string; + monitor_version: string; + identifier: string; + telemetry?: { + identifier: string; + plugin_version: string; + cpu_pct: number; + mem_pct: number; + disk_pct: number; + swap_pct: number; + load_avg: number[]; + uptime_seconds: number; + nginx_installed: boolean; + nginx_sites: string[]; + }; + last_updated?: string; +} + +export class MonitorBridgeClient { + private baseUrl: string; + private timeoutMs: number; + + constructor(port: number, timeoutMs = 3000) { + this.baseUrl = `http://127.0.0.1:${port}`; + this.timeoutMs = timeoutMs; + } + + async health(): Promise { + return this.fetchJson('/health'); + } + + async telemetry(): Promise { + return this.fetchJson('/telemetry'); + } + + private async fetchJson(path: string): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + const response = await fetch(`${this.baseUrl}${path}`, { + signal: controller.signal, + }); + clearTimeout(timeout); + + if (!response.ok) return null; + return (await response.json()) as T; + } catch { + return null; + } + } +} diff --git a/plugin/index.ts b/plugin/index.ts index 5fc2481..7781950 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -10,6 +10,7 @@ */ import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os'; import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config'; +import { MonitorBridgeClient } from './core/monitor-bridge'; interface PluginAPI { logger: { @@ -50,6 +51,16 @@ export default { return getLivePluginConfig(api, baseConfig); } + /** + * Get the monitor bridge client if monitorPort is configured. + */ + function getBridgeClient(): MonitorBridgeClient | null { + const live = resolveConfig() as any; + const 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. @@ -99,6 +110,16 @@ export default { }, 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: { @@ -108,6 +129,7 @@ export default { reportIntervalSec: live.reportIntervalSec, hasApiKey: Boolean(live.apiKey), }, + monitorBridge, telemetry: collectTelemetry(), }; }, @@ -126,6 +148,33 @@ export default { }, })); + // 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 (monitorPort 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)'); }, }; From 27b8b74d390ad011b34c4697116c5473a513ea82 Mon Sep 17 00:00:00 2001 From: zhi Date: Sat, 21 Mar 2026 19:22:44 +0000 Subject: [PATCH 4/5] Align plugin monitor_port config --- README.md | 182 ++++++++++++++----------------- plugin/core/live-config.d.ts | 1 + plugin/core/live-config.d.ts.map | 2 +- plugin/core/live-config.js | 9 ++ plugin/core/live-config.js.map | 2 +- plugin/core/live-config.ts | 12 ++ plugin/index.ts | 9 +- plugin/openclaw.plugin.json | 2 +- plugin/package-lock.json | 8 +- plugin/package.json | 6 +- scripts/install.mjs | 1 + 11 files changed, 119 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 2c9bfdc..41ed680 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,80 @@ # HarborForge OpenClaw Plugin -OpenClaw 插件,将服务器遥测数据流式传输到 HarborForge Monitor。 +OpenClaw 插件:向 HarborForge Monitor 暴露 OpenClaw 侧元数据,并提供可选的本地桥接能力;安装时也可顺带安装 `hf` CLI。 + +## 当前状态 + +- 插件注册名:`harbor-forge` +- 旧 sidecar `server/` 架构已移除 +- 监控桥接走本地 `monitor_port` +- 安装脚本支持 `--install-cli` +- `skills/hf/` 仅在 `--install-cli` 时一并安装 ## 项目结构 -``` +```text HarborForge.OpenclawPlugin/ -├── package.json # 根 package.json -├── README.md # 本文档 -├── plugin/ # OpenClaw 插件代码 -│ ├── openclaw.plugin.json # 插件定义 -│ ├── index.ts # 插件入口 -│ ├── package.json # 插件依赖 -│ └── tsconfig.json # TypeScript 配置 -├── server/ # Sidecar 服务器 -│ └── telemetry.mjs # 遥测数据收集和发送 -├── skills/ # OpenClaw 技能 -│ └── (技能文件) +├── package.json +├── README.md +├── plugin/ +│ ├── openclaw.plugin.json +│ ├── index.ts +│ ├── core/ +│ │ ├── live-config.ts +│ │ └── monitor-bridge.ts +│ └── package.json +├── skills/ +│ └── hf/ +│ └── SKILL.md └── scripts/ - └── install.mjs # 安装脚本 -``` - -## 架构 - -``` -┌─────────────────────────────────────────────────┐ -│ OpenClaw Gateway │ -│ ┌───────────────────────────────────────────┐ │ -│ │ HarborForge.OpenclawPlugin/plugin/ │ │ -│ │ - 生命周期管理 (启动/停止) │ │ -│ │ - 配置管理 │ │ -│ └───────────────────────────────────────────┘ │ -│ │ │ -│ ▼ 启动 telemetry server │ -│ ┌───────────────────────────────────────────┐ │ -│ │ HarborForge.OpenclawPlugin/server/ │ │ -│ │ - 独立 Node 进程 │ │ -│ │ - 收集系统指标 │ │ -│ │ - 收集 OpenClaw 状态 │ │ -│ │ - 发送到 HarborForge Monitor │ │ -│ └───────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ - │ - ▼ HTTP - ┌─────────────────────┐ - │ HarborForge Monitor │ - └─────────────────────┘ + └── install.mjs ``` ## 安装 -### 快速安装 +### 普通安装 ```bash -# 克隆仓库 -git clone https://git.hangman-lab.top/zhi/HarborForge.OpenclawPlugin.git -cd HarborForge.OpenclawPlugin - -# 运行安装脚本 node scripts/install.mjs ``` -### 开发安装 +这会: +- 构建并安装 OpenClaw 插件 +- 复制常规 skills +- **不会**安装 `hf` 二进制 +- **不会**复制 `skills/hf/` + +### 安装插件 + `hf` CLI ```bash -# 仅构建不安装 +node scripts/install.mjs --install-cli +``` + +这会额外: +- 构建 `HarborForge.Cli` +- 安装 `hf` 到 `~/.openclaw/bin/hf` +- `chmod +x ~/.openclaw/bin/hf` +- 复制 `skills/hf/` 到 OpenClaw profile skills 目录 + +### 常用选项 + +```bash +# 仅构建 node scripts/install.mjs --build-only -# 指定 OpenClaw 路径 +# 指定 OpenClaw profile node scripts/install.mjs --openclaw-profile-path /custom/path/.openclaw -# 详细输出 +# 详细日志 node scripts/install.mjs --verbose + +# 卸载 +node scripts/install.mjs --uninstall ``` ## 配置 -1. 在 HarborForge Monitor 中注册服务器,并生成 `apiKey` - -2. 编辑 `~/.openclaw/openclaw.json`: +编辑 `~/.openclaw/openclaw.json`: ```json { @@ -91,7 +87,7 @@ node scripts/install.mjs --verbose "backendUrl": "https://monitor.hangman-lab.top", "identifier": "my-server-01", "apiKey": "your-api-key-here", - "monitorPort": 9100, + "monitor_port": 9100, "reportIntervalSec": 30, "httpFallbackIntervalSec": 60, "logLevel": "info" @@ -102,84 +98,68 @@ node scripts/install.mjs --verbose } ``` -3. 重启 OpenClaw Gateway: +> 说明:`monitor_port` 是当前主字段;为兼容旧配置,插件仍接受 `monitorPort`。 + +然后重启: ```bash openclaw gateway restart ``` -## 配置选项 +## 配置项 | 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `enabled` | boolean | `true` | 是否启用插件 | -| `backendUrl` | string | `https://monitor.hangman-lab.top` | Monitor 后端地址 | -| `identifier` | string | 自动检测 hostname | 服务器标识符 | -| `apiKey` | string | 必填 | HarborForge Monitor 生成的服务器 API Key | -| `monitorPort` | number | `9100`(示例) | 本地桥接端口;插件通过 `127.0.0.1:` 与 HarborForge.Monitor 通信 | +| `backendUrl` | string | `https://monitor.hangman-lab.top` | HarborForge Monitor 后端地址 | +| `identifier` | string | 主机名 | 服务器标识符 | +| `apiKey` | string | 无 | HarborForge Monitor 生成的服务器 API Key | +| `monitor_port` | number | 无 | 本地桥接端口;插件通过 `127.0.0.1:` 与 HarborForge.Monitor 通信 | | `reportIntervalSec` | number | `30` | 报告间隔(秒) | | `httpFallbackIntervalSec` | number | `60` | HTTP 回退间隔(秒) | -| `logLevel` | string | `"info"` | 日志级别: debug/info/warn/error | +| `logLevel` | string | `info` | 日志级别:`debug` / `info` / `warn` / `error` | -## Monitor 本地桥接 +## 本地桥接说明 -当插件配置了 `monitorPort`,并且 HarborForge.Monitor 也以相同的 `MONITOR_PORT` 启动时: +当插件配置了 `monitor_port`,并且 HarborForge.Monitor 也使用相同的 `MONITOR_PORT` 时: -- Monitor 会在 `127.0.0.1:` 暴露本地桥接服务 +- Monitor 在 `127.0.0.1:` 提供本地桥接服务 - 插件可探测 `GET /health` - 插件工具 `harborforge_monitor_telemetry` 可读取 `GET /telemetry` -- 若桥接端口未配置或不可达,插件仍然正常工作,只是不会拿到 Monitor 的宿主机遥测补充数据 +- 如果桥接端口未配置或不可达,插件仍可正常运行 -这条链路是**可选增强**,不是插件或 Monitor 心跳上报的前置依赖。 +也就是说,这条链路是**可选增强**,不是插件启动或 Monitor 心跳的前置条件。 -## 收集的指标 +## 插件提供的信息 -### 系统指标 -- CPU 使用率 (%) -- 内存使用率 (%)、已用/总量 (MB) -- 磁盘使用率 (%)、已用/总量 (GB) -- 交换分区使用率 (%) -- 系统运行时间 (秒) -- 1分钟平均负载 -- 平台 (linux/darwin/win32) -- 主机名 +### OpenClaw 元数据 +- OpenClaw version +- plugin version +- 标识符 / 主机名 +- 时间戳 -### OpenClaw 指标 -- OpenClaw 版本 -- Agent 数量 -- Agent 列表 (id, name, status) - -## 卸载 - -```bash -node scripts/install.mjs --uninstall -``` +### 系统快照 +- uptime +- memory total/free/used/usagePercent +- load avg1/avg5/avg15 +- platform ## 开发 -### 构建插件 - ```bash cd plugin npm install npm run build ``` -### 本地测试 telemetry server - -```bash -cd server -HF_MONITOR_API_KEY=test-api-key \ -HF_MONITOR_BACKEND_URL=http://localhost:8000 \ -HF_MONITOR_LOG_LEVEL=debug \ -node telemetry.mjs -``` - ## 依赖 - Node.js 18+ - OpenClaw Gateway +- Go 1.20+(仅 `--install-cli` 需要) -## 文档 +## 相关提示 -- [监控连接器规划](./docs/monitor-server-connector-plan.md) - 原始设计文档 +- 安装 `hf` 后,建议把 `~/.openclaw/bin` 加到 `PATH` +- Agent 使用 `hf` 时,优先试 `hf --help-brief` +- 完整命令树看 `hf --help` diff --git a/plugin/core/live-config.d.ts b/plugin/core/live-config.d.ts index e455432..3f78c43 100644 --- a/plugin/core/live-config.d.ts +++ b/plugin/core/live-config.d.ts @@ -3,6 +3,7 @@ export interface HarborForgeMonitorConfig { backendUrl?: string; identifier?: string; apiKey?: string; + monitor_port?: number; monitorPort?: number; reportIntervalSec?: number; httpFallbackIntervalSec?: number; diff --git a/plugin/core/live-config.d.ts.map b/plugin/core/live-config.d.ts.map index 26a5054..074ffc5 100644 --- a/plugin/core/live-config.d.ts.map +++ b/plugin/core/live-config.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"live-config.d.ts","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,qBAAqB,EAC1B,QAAQ,EAAE,wBAAwB,GACjC,wBAAwB,CAqB1B"} \ No newline at end of file +{"version":3,"file":"live-config.d.ts","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAChD;AAED,UAAU,qBAAqB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,qBAAqB,EAC1B,QAAQ,EAAE,wBAAwB,GACjC,wBAAwB,CAgC1B"} \ No newline at end of file diff --git a/plugin/core/live-config.js b/plugin/core/live-config.js index b3201d3..463dc7c 100644 --- a/plugin/core/live-config.js +++ b/plugin/core/live-config.js @@ -8,9 +8,18 @@ function getLivePluginConfig(api, fallback) { const entry = entries['harbor-forge'] || {}; const cfg = entry.config || {}; if (Object.keys(cfg).length > 0 || Object.keys(entry).length > 0) { + const monitorPort = typeof cfg.monitor_port === 'number' + ? cfg.monitor_port + : typeof cfg.monitorPort === 'number' + ? cfg.monitorPort + : typeof fallback.monitor_port === 'number' + ? fallback.monitor_port + : fallback.monitorPort; return { ...fallback, ...cfg, + monitor_port: monitorPort, + monitorPort, enabled: typeof cfg.enabled === 'boolean' ? cfg.enabled : typeof entry.enabled === 'boolean' diff --git a/plugin/core/live-config.js.map b/plugin/core/live-config.js.map index 650d6ac..e0573e7 100644 --- a/plugin/core/live-config.js.map +++ b/plugin/core/live-config.js.map @@ -1 +1 @@ -{"version":3,"file":"live-config.js","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":";;AAeA,kDAwBC;AAxBD,SAAgB,mBAAmB,CACjC,GAA0B,EAC1B,QAAkC;IAElC,MAAM,IAAI,GAAI,GAAG,CAAC,MAAkC,IAAI,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAI,IAAI,CAAC,OAAmC,IAAI,EAAE,CAAC;IAChE,MAAM,OAAO,GAAI,OAAO,CAAC,OAAmC,IAAI,EAAE,CAAC;IACnE,MAAM,KAAK,GAAI,OAAO,CAAC,cAAc,CAA6B,IAAI,EAAE,CAAC;IACzE,MAAM,GAAG,GAAI,KAAK,CAAC,MAAkC,IAAI,EAAE,CAAC;IAE5D,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjE,OAAO;YACL,GAAG,QAAQ;YACX,GAAG,GAAG;YACN,OAAO,EACL,OAAO,GAAG,CAAC,OAAO,KAAK,SAAS;gBAC9B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;oBAClC,CAAC,CAAC,KAAK,CAAC,OAAO;oBACf,CAAC,CAAC,QAAQ,CAAC,OAAO;SACG,CAAC;IAChC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"} \ No newline at end of file +{"version":3,"file":"live-config.js","sourceRoot":"","sources":["live-config.ts"],"names":[],"mappings":";;AAgBA,kDAmCC;AAnCD,SAAgB,mBAAmB,CACjC,GAA0B,EAC1B,QAAkC;IAElC,MAAM,IAAI,GAAI,GAAG,CAAC,MAAkC,IAAI,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAI,IAAI,CAAC,OAAmC,IAAI,EAAE,CAAC;IAChE,MAAM,OAAO,GAAI,OAAO,CAAC,OAAmC,IAAI,EAAE,CAAC;IACnE,MAAM,KAAK,GAAI,OAAO,CAAC,cAAc,CAA6B,IAAI,EAAE,CAAC;IACzE,MAAM,GAAG,GAAI,KAAK,CAAC,MAAkC,IAAI,EAAE,CAAC;IAE5D,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjE,MAAM,WAAW,GACf,OAAO,GAAG,CAAC,YAAY,KAAK,QAAQ;YAClC,CAAC,CAAC,GAAG,CAAC,YAAY;YAClB,CAAC,CAAC,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ;gBACnC,CAAC,CAAC,GAAG,CAAC,WAAW;gBACjB,CAAC,CAAC,OAAO,QAAQ,CAAC,YAAY,KAAK,QAAQ;oBACzC,CAAC,CAAC,QAAQ,CAAC,YAAY;oBACvB,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;QAE/B,OAAO;YACL,GAAG,QAAQ;YACX,GAAG,GAAG;YACN,YAAY,EAAE,WAAW;YACzB,WAAW;YACX,OAAO,EACL,OAAO,GAAG,CAAC,OAAO,KAAK,SAAS;gBAC9B,CAAC,CAAC,GAAG,CAAC,OAAO;gBACb,CAAC,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;oBAClC,CAAC,CAAC,KAAK,CAAC,OAAO;oBACf,CAAC,CAAC,QAAQ,CAAC,OAAO;SACG,CAAC;IAChC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"} \ No newline at end of file diff --git a/plugin/core/live-config.ts b/plugin/core/live-config.ts index a3bc3c3..6e3de73 100644 --- a/plugin/core/live-config.ts +++ b/plugin/core/live-config.ts @@ -3,6 +3,7 @@ export interface HarborForgeMonitorConfig { backendUrl?: string; identifier?: string; apiKey?: string; + monitor_port?: number; monitorPort?: number; reportIntervalSec?: number; httpFallbackIntervalSec?: number; @@ -24,9 +25,20 @@ export function getLivePluginConfig( const cfg = (entry.config as Record) || {}; if (Object.keys(cfg).length > 0 || Object.keys(entry).length > 0) { + const monitorPort = + typeof cfg.monitor_port === 'number' + ? cfg.monitor_port + : typeof cfg.monitorPort === 'number' + ? cfg.monitorPort + : typeof fallback.monitor_port === 'number' + ? fallback.monitor_port + : fallback.monitorPort; + return { ...fallback, ...cfg, + monitor_port: monitorPort, + monitorPort, enabled: typeof cfg.enabled === 'boolean' ? cfg.enabled diff --git a/plugin/index.ts b/plugin/index.ts index 7781950..b71fbdd 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -52,11 +52,12 @@ export default { } /** - * Get the monitor bridge client if monitorPort is configured. + * 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.monitorPort; + const port = live.monitor_port ?? live.monitorPort; if (!port || port <= 0) return null; return new MonitorBridgeClient(port); } @@ -125,7 +126,7 @@ export default { config: { backendUrl: live.backendUrl, identifier: live.identifier || hostname(), - monitorPort: (live as any).monitorPort || null, + monitorPort: (live as any).monitor_port ?? (live as any).monitorPort ?? null, reportIntervalSec: live.reportIntervalSec, hasApiKey: Boolean(live.apiKey), }, @@ -160,7 +161,7 @@ export default { const bridgeClient = getBridgeClient(); if (!bridgeClient) { return { - error: 'Monitor bridge not configured (monitorPort not set or 0)', + error: 'Monitor bridge not configured (monitor_port not set or 0)', }; } diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index cba0d12..fe9fbc9 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -26,7 +26,7 @@ "type": "string", "description": "API Key from HarborForge Monitor admin panel (optional but required for authentication)" }, - "monitorPort": { + "monitor_port": { "type": "number", "description": "Local port for communication between HarborForge Monitor and this plugin" }, diff --git a/plugin/package-lock.json b/plugin/package-lock.json index 23f5869..d57e993 100644 --- a/plugin/package-lock.json +++ b/plugin/package-lock.json @@ -1,12 +1,12 @@ { - "name": "harborforge-monitor-plugin", - "version": "0.1.0", + "name": "harbor-forge-plugin", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "harborforge-monitor-plugin", - "version": "0.1.0", + "name": "harbor-forge-plugin", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@types/node": "^20.0.0", diff --git a/plugin/package.json b/plugin/package.json index d3cd063..0e17992 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -1,7 +1,7 @@ { - "name": "harborforge-monitor-plugin", - "version": "0.1.0", - "description": "OpenClaw plugin for HarborForge Monitor", + "name": "harbor-forge-plugin", + "version": "0.2.0", + "description": "OpenClaw plugin for HarborForge monitor bridge and CLI integration", "main": "index.js", "scripts": { "build": "tsc", diff --git a/scripts/install.mjs b/scripts/install.mjs index 718dabf..12a9bfb 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -354,6 +354,7 @@ function summary() { log('Next steps:', 'blue'); log(' 1. Register server in HarborForge Monitor to get apiKey', 'cyan'); log(' 2. Edit ~/.openclaw/openclaw.json under plugins.entries.harbor-forge.config:', 'cyan'); + log(' (prefer monitor_port; legacy monitorPort is still accepted)', 'cyan'); log(' {', 'cyan'); log(' "plugins": {', 'cyan'); log(' "entries": {', 'cyan'); From e7ba98212827010ab459a656c7cf142d2ef0c056 Mon Sep 17 00:00:00 2001 From: zhi Date: Sun, 22 Mar 2026 01:37:21 +0000 Subject: [PATCH 5/5] feat: push OpenClaw metadata to Monitor bridge periodically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MonitorBridgeClient gains pushOpenClawMeta() method for POST /openclaw - OpenClawMeta interface defines version/plugin_version/agents payload - Plugin pushes metadata on gateway_start (delayed 2s) and periodically - Interval aligns with reportIntervalSec (default 30s) - Pushes are non-fatal — plugin continues if Monitor is unreachable - Interval cleanup on gateway_stop - Updated monitor-server-connector-plan.md with new architecture --- docs/monitor-server-connector-plan.md | 53 +++++++++++++++------------ plugin/core/monitor-bridge.d.ts | 14 +++++++ plugin/core/monitor-bridge.d.ts.map | 2 +- plugin/core/monitor-bridge.js | 22 +++++++++++ plugin/core/monitor-bridge.js.map | 2 +- plugin/core/monitor-bridge.ts | 33 +++++++++++++++++ plugin/index.ts | 46 ++++++++++++++++++++++- 7 files changed, 146 insertions(+), 26 deletions(-) diff --git a/docs/monitor-server-connector-plan.md b/docs/monitor-server-connector-plan.md index 291a9ee..2a2ae0b 100644 --- a/docs/monitor-server-connector-plan.md +++ b/docs/monitor-server-connector-plan.md @@ -2,46 +2,53 @@ ## Current design -The plugin uses: +The plugin and Monitor communicate over a local bridge port (`monitor_port` / `MONITOR_PORT`). -- **HTTP heartbeat** to `/monitor/server/heartbeat-v2` -- **API Key authentication** via `X-API-Key` -- **Gateway lifecycle hooks**: `gateway_start` / `gateway_stop` +### Data flow + +1. **Monitor → Plugin** (GET): Plugin queries `GET /telemetry` on the bridge for host hardware data. +2. **Plugin → Monitor** (POST): Plugin pushes OpenClaw metadata via `POST /openclaw` to the bridge. +3. **Monitor → Backend**: Monitor heartbeats to `POST /monitor/server/heartbeat-v2` with `X-API-Key`, enriched with any available OpenClaw metadata. + +### Bridge endpoints (on Monitor, 127.0.0.1:MONITOR_PORT) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check, returns monitor version and identifier | +| `/telemetry` | GET | Latest hardware telemetry snapshot | +| `/openclaw` | POST | Receive OpenClaw metadata from plugin | + +### Plugin behavior + +- On `gateway_start`, plugin begins periodic metadata push (aligned with `reportIntervalSec`). +- Initial push is delayed 2s to allow Monitor bridge startup. +- If bridge is unreachable, pushes fail silently. Plugin remains fully functional. +- On `gateway_stop`, periodic push is stopped. ## No longer used The following design has been retired: -- challenge UUID -- RSA public key fetch -- encrypted handshake payload -- WebSocket telemetry +- challenge UUID / RSA handshake / WebSocket telemetry +- Plugin-side `server/` sidecar process -## Runtime flow - -1. Gateway loads `harborforge-monitor` -2. Plugin reads config from OpenClaw plugin config -3. On `gateway_start`, plugin launches `server/telemetry.mjs` -4. Sidecar collects: - - system metrics - - OpenClaw version - - plugin version - - configured agents -5. Sidecar posts telemetry to backend with `X-API-Key` - -## Payload +## Heartbeat payload ```json { "identifier": "vps.t1", "openclaw_version": "OpenClaw 2026.3.13 (61d171a)", - "plugin_version": "0.1.0", + "plugin_version": "0.2.0", "agents": [], "cpu_pct": 10.5, "mem_pct": 52.1, "disk_pct": 81.0, "swap_pct": 0.0, "load_avg": [0.12, 0.09, 0.03], - "uptime_seconds": 12345 + "uptime_seconds": 12345, + "nginx_installed": true, + "nginx_sites": ["default"] } ``` + +`openclaw_version`, `plugin_version`, and `agents` are optional enrichment from the plugin. If plugin never pushes metadata, these fields are omitted and the heartbeat contains only hardware telemetry. diff --git a/plugin/core/monitor-bridge.d.ts b/plugin/core/monitor-bridge.d.ts index 84f626f..afa27f9 100644 --- a/plugin/core/monitor-bridge.d.ts +++ b/plugin/core/monitor-bridge.d.ts @@ -36,6 +36,20 @@ export declare class MonitorBridgeClient { constructor(port: number, timeoutMs?: number); health(): Promise; telemetry(): Promise; + /** + * POST OpenClaw metadata to the Monitor bridge so it can enrich + * its heartbeat uploads with OpenClaw version, plugin version, + * and agent information. + */ + pushOpenClawMeta(meta: OpenClawMeta): Promise; private fetchJson; } +/** + * OpenClaw metadata payload sent to the Monitor bridge. + */ +export interface OpenClawMeta { + version: string; + plugin_version: string; + agents?: any[]; +} //# sourceMappingURL=monitor-bridge.d.ts.map \ No newline at end of file diff --git a/plugin/core/monitor-bridge.d.ts.map b/plugin/core/monitor-bridge.d.ts.map index bffa006..d170573 100644 --- a/plugin/core/monitor-bridge.d.ts.map +++ b/plugin/core/monitor-bridge.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"monitor-bridge.d.ts","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;QACzB,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO;IAKpC,MAAM,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIvC,SAAS,IAAI,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;YAI7C,SAAS;CAgBxB"} \ No newline at end of file +{"version":3,"file":"monitor-bridge.d.ts","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;QACzB,WAAW,EAAE,MAAM,EAAE,CAAC;KACvB,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO;IAKpC,MAAM,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAIvC,SAAS,IAAI,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;IAI3D;;;;OAIG;IACG,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;YAmB9C,SAAS;CAgBxB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;CAChB"} \ No newline at end of file diff --git a/plugin/core/monitor-bridge.js b/plugin/core/monitor-bridge.js index 5657649..144b4e4 100644 --- a/plugin/core/monitor-bridge.js +++ b/plugin/core/monitor-bridge.js @@ -23,6 +23,28 @@ class MonitorBridgeClient { async telemetry() { return this.fetchJson('/telemetry'); } + /** + * POST OpenClaw metadata to the Monitor bridge so it can enrich + * its heartbeat uploads with OpenClaw version, plugin version, + * and agent information. + */ + async pushOpenClawMeta(meta) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + const response = await fetch(`${this.baseUrl}/openclaw`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(meta), + signal: controller.signal, + }); + clearTimeout(timeout); + return response.ok; + } + catch { + return false; + } + } async fetchJson(path) { try { const controller = new AbortController(); diff --git a/plugin/core/monitor-bridge.js.map b/plugin/core/monitor-bridge.js.map index cdeccf0..174276a 100644 --- a/plugin/core/monitor-bridge.js.map +++ b/plugin/core/monitor-bridge.js.map @@ -1 +1 @@ -{"version":3,"file":"monitor-bridge.js","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA2BH,MAAa,mBAAmB;IACtB,OAAO,CAAS;IAChB,SAAS,CAAS;IAE1B,YAAY,IAAY,EAAE,SAAS,GAAG,IAAI;QACxC,IAAI,CAAC,OAAO,GAAG,oBAAoB,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,IAAI,CAAC,SAAS,CAAgB,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,SAAS,CAA2B,YAAY,CAAC,CAAC;IAChE,CAAC;IAEO,KAAK,CAAC,SAAS,CAAI,IAAY;QACrC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YAC9B,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAjCD,kDAiCC"} \ No newline at end of file +{"version":3,"file":"monitor-bridge.js","sourceRoot":"","sources":["monitor-bridge.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AA2BH,MAAa,mBAAmB;IACtB,OAAO,CAAS;IAChB,SAAS,CAAS;IAE1B,YAAY,IAAY,EAAE,SAAS,GAAG,IAAI;QACxC,IAAI,CAAC,OAAO,GAAG,oBAAoB,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,IAAI,CAAC,SAAS,CAAgB,SAAS,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,SAAS,CAA2B,YAAY,CAAC,CAAC;IAChE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,gBAAgB,CAAC,IAAkB;QACvC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,WAAW,EAAE;gBACvD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,OAAO,QAAQ,CAAC,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,SAAS,CAAI,IAAY;QACrC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YAEtB,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,OAAO,IAAI,CAAC;YAC9B,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAzDD,kDAyDC"} \ No newline at end of file diff --git a/plugin/core/monitor-bridge.ts b/plugin/core/monitor-bridge.ts index 1da54e0..e755c35 100644 --- a/plugin/core/monitor-bridge.ts +++ b/plugin/core/monitor-bridge.ts @@ -50,6 +50,30 @@ export class MonitorBridgeClient { return this.fetchJson('/telemetry'); } + /** + * POST OpenClaw metadata to the Monitor bridge so it can enrich + * its heartbeat uploads with OpenClaw version, plugin version, + * and agent information. + */ + async pushOpenClawMeta(meta: OpenClawMeta): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + const response = await fetch(`${this.baseUrl}/openclaw`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(meta), + signal: controller.signal, + }); + clearTimeout(timeout); + + return response.ok; + } catch { + return false; + } + } + private async fetchJson(path: string): Promise { try { const controller = new AbortController(); @@ -67,3 +91,12 @@ export class MonitorBridgeClient { } } } + +/** + * OpenClaw metadata payload sent to the Monitor bridge. + */ +export interface OpenClawMeta { + version: string; + plugin_version: string; + agents?: any[]; +} diff --git a/plugin/index.ts b/plugin/index.ts index b71fbdd..abef652 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -10,7 +10,7 @@ */ import { hostname, freemem, totalmem, uptime, loadavg, platform } from 'os'; import { getLivePluginConfig, type HarborForgeMonitorConfig } from './core/live-config'; -import { MonitorBridgeClient } from './core/monitor-bridge'; +import { MonitorBridgeClient, type OpenClawMeta } from './core/monitor-bridge'; interface PluginAPI { logger: { @@ -93,12 +93,56 @@ export default { }; } + // 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