diff --git a/README.md b/README.md index 159c1e0..c3bd2b0 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,39 @@ OpenClaw 插件,将服务器遥测数据流式传输到 HarborForge Monitor。 +## 项目结构 + +``` +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 技能 +│ └── (技能文件) +└── scripts/ + └── install.mjs # 安装脚本 +``` + ## 架构 ``` ┌─────────────────────────────────────────────────┐ │ OpenClaw Gateway │ │ ┌───────────────────────────────────────────┐ │ -│ │ HarborForge.OpenclawPlugin (index.mjs) │ │ +│ │ HarborForge.OpenclawPlugin/plugin/ │ │ │ │ - 生命周期管理 (启动/停止) │ │ │ │ - 配置管理 │ │ │ └───────────────────────────────────────────┘ │ │ │ │ -│ ▼ 启动 sidecar │ +│ ▼ 启动 telemetry server │ │ ┌───────────────────────────────────────────┐ │ -│ │ Sidecar (sidecar/server.mjs) │ │ +│ │ HarborForge.OpenclawPlugin/server/ │ │ │ │ - 独立 Node 进程 │ │ │ │ - 收集系统指标 │ │ │ │ - 收集 OpenClaw 状态 │ │ @@ -31,26 +50,35 @@ OpenClaw 插件,将服务器遥测数据流式传输到 HarborForge Monitor。 ## 安装 -### 1. 复制插件到 OpenClaw 插件目录 +### 快速安装 ```bash -# 找到 OpenClaw 插件目录 -# 通常是 ~/.openclaw/plugins/ 或 /usr/lib/node_modules/openclaw/plugins/ +# 克隆仓库 +git clone https://git.hangman-lab.top/zhi/HarborForge.OpenclawPlugin.git +cd HarborForge.OpenclawPlugin -# 复制插件 -cp -r HarborForge.OpenclawPlugin ~/.openclaw/plugins/harborforge-monitor +# 运行安装脚本 +node scripts/install.mjs ``` -### 2. 在 HarborForge Monitor 中注册服务器 +### 开发安装 -1. 登录 HarborForge Monitor -2. 进入 Server Management -3. 点击 "Register New Server" -4. 获取 `challengeUuid` +```bash +# 仅构建不安装 +node scripts/install.mjs --build-only -### 3. 配置 OpenClaw +# 指定 OpenClaw 路径 +node scripts/install.mjs --openclaw-profile-path /custom/path/.openclaw -编辑 `~/.openclaw/openclaw.json`: +# 详细输出 +node scripts/install.mjs --verbose +``` + +## 配置 + +1. 在 HarborForge Monitor 中注册服务器,获取 `challengeUuid` + +2. 编辑 `~/.openclaw/openclaw.json`: ```json { @@ -68,7 +96,7 @@ cp -r HarborForge.OpenclawPlugin ~/.openclaw/plugins/harborforge-monitor } ``` -### 4. 重启 OpenClaw Gateway +3. 重启 OpenClaw Gateway: ```bash openclaw gateway restart @@ -79,7 +107,7 @@ openclaw gateway restart | 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `enabled` | boolean | `true` | 是否启用插件 | -| `backendUrl` | string | `"https://monitor.hangman-lab.top"` | Monitor 后端地址 | +| `backendUrl` | string | `https://monitor.hangman-lab.top` | Monitor 后端地址 | | `identifier` | string | 自动检测 hostname | 服务器标识符 | | `challengeUuid` | string | 必填 | 注册挑战 UUID | | `reportIntervalSec` | number | `30` | 报告间隔(秒) | @@ -103,52 +131,37 @@ openclaw gateway restart - Agent 数量 - Agent 列表 (id, name, status) -## 故障排查 - -### 查看日志 +## 卸载 ```bash -# 查看 Gateway 日志 -openclaw gateway logs | grep HF-Monitor - -# 或者直接查看 sidecar 输出(如果独立运行) -node sidecar/server.mjs 2>&1 | tee monitor.log +node scripts/install.mjs --uninstall ``` -### 检查状态 - -在 OpenClaw 对话中: - -``` -使用 harborforge_monitor_status 工具检查插件状态 -``` - -### 常见问题 - -1. **challengeUuid 未设置** - - 错误: `Missing required config: challengeUuid` - - 解决: 在 Monitor 中注册服务器并配置 challengeUuid - -2. **Sidecar 无法启动** - - 检查 Node.js 版本 (>=18) - - 检查 `sidecar/server.mjs` 是否存在 - -3. **无法连接到 Monitor** - - 检查 `backendUrl` 配置 - - 检查网络连接和防火墙 - ## 开发 -### 本地测试 sidecar +### 构建插件 ```bash -cd sidecar +cd plugin +npm install +npm run build +``` + +### 本地测试 telemetry server + +```bash +cd server HF_MONITOR_CHALLENGE_UUID=test-uuid \ HF_MONITOR_BACKEND_URL=http://localhost:8000 \ HF_MONITOR_LOG_LEVEL=debug \ -node server.mjs +node telemetry.mjs ``` +## 依赖 + +- Node.js 18+ +- OpenClaw Gateway + ## 文档 - [监控连接器规划](./docs/monitor-server-connector-plan.md) - 原始设计文档 diff --git a/package.json b/package.json index 2deaf45..c9bb563 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "description": "OpenClaw plugin for HarborForge Monitor - streams server telemetry", "type": "module", - "main": "index.mjs", "scripts": { - "start": "node index.mjs", - "sidecar": "node sidecar/server.mjs" + "build": "cd plugin && npm run build", + "install": "node scripts/install.mjs", + "uninstall": "node scripts/install.mjs --uninstall" }, "keywords": ["openclaw", "plugin", "monitoring", "harborforge"], "license": "MIT", diff --git a/index.mjs b/plugin/index.ts similarity index 54% rename from index.mjs rename to plugin/index.ts index ef14b7d..4d70382 100644 --- a/index.mjs +++ b/plugin/index.ts @@ -1,8 +1,7 @@ /** * HarborForge Monitor Plugin for OpenClaw * - * Registers with OpenClaw Gateway and manages sidecar lifecycle. - * Sidecar runs as separate Node process to avoid blocking Gateway. + * Manages sidecar lifecycle and provides monitor-related tools. */ import { spawn } from 'child_process'; import { fileURLToPath } from 'url'; @@ -12,12 +11,35 @@ import { existsSync } from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -/** @type {import('openclaw').Plugin} */ -export default function register(api, config) { +interface PluginConfig { + enabled?: boolean; + backendUrl?: string; + identifier?: string; + challengeUuid?: string; + reportIntervalSec?: number; + httpFallbackIntervalSec?: number; + logLevel?: 'debug' | 'info' | 'warn' | 'error'; +} + +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, config: PluginConfig) { 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) + 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), }; if (!config?.enabled) { @@ -25,35 +47,29 @@ export default function register(api, config) { 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'); + const serverPath = join(__dirname, '..', 'server', 'telemetry.mjs'); - if (!existsSync(sidecarPath)) { - logger.error('Sidecar not found:', sidecarPath); + if (!existsSync(serverPath)) { + logger.error('Telemetry server not found:', serverPath); return; } - /** @type {import('child_process').ChildProcess|null} */ - let sidecar = null; + let sidecar: ReturnType | null = null; - /** - * Start the sidecar server - */ function startSidecar() { if (sidecar) { logger.debug('Sidecar already running'); return; } - logger.info('Starting HarborForge Monitor sidecar...'); + logger.info('Starting HarborForge Monitor telemetry server...'); - // Prepare environment for sidecar const env = { ...process.env, HF_MONITOR_BACKEND_URL: config.backendUrl || 'https://monitor.hangman-lab.top', @@ -62,57 +78,49 @@ export default function register(api, config) { 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], { + sidecar = spawn('node', [serverPath], { env, - detached: false, // Keep attached for logging, but could be true for full detachment + detached: false, stdio: ['ignore', 'pipe', 'pipe'] }); - sidecar.stdout?.on('data', (data) => { - logger.info('[sidecar]', data.toString().trim()); + sidecar.stdout?.on('data', (data: Buffer) => { + logger.info('[telemetry]', data.toString().trim()); }); - sidecar.stderr?.on('data', (data) => { - logger.error('[sidecar]', data.toString().trim()); + sidecar.stderr?.on('data', (data: Buffer) => { + logger.error('[telemetry]', data.toString().trim()); }); sidecar.on('exit', (code, signal) => { - logger.info(`Sidecar exited (code: ${code}, signal: ${signal})`); + logger.info(`Telemetry server exited (code: ${code}, signal: ${signal})`); sidecar = null; }); - sidecar.on('error', (err) => { - logger.error('Failed to start sidecar:', err.message); + sidecar.on('error', (err: Error) => { + logger.error('Failed to start telemetry server:', err.message); sidecar = null; }); - logger.info('Sidecar started with PID:', sidecar.pid); + logger.info('Telemetry server started with PID:', sidecar.pid); } - /** - * Stop the sidecar server - */ function stopSidecar() { if (!sidecar) { - logger.debug('Sidecar not running'); + logger.debug('Telemetry server not running'); return; } - logger.info('Stopping HarborForge Monitor sidecar...'); - - // Graceful shutdown + logger.info('Stopping HarborForge Monitor telemetry server...'); sidecar.kill('SIGTERM'); - // Force kill after timeout const timeout = setTimeout(() => { if (sidecar && !sidecar.killed) { - logger.warn('Sidecar did not exit gracefully, forcing kill'); + logger.warn('Telemetry server did not exit gracefully, forcing kill'); sidecar.kill('SIGKILL'); } }, 5000); @@ -124,32 +132,24 @@ export default function register(api, config) { // Hook into Gateway lifecycle api.on('gateway:start', () => { - logger.info('Gateway starting, starting monitor sidecar...'); + logger.info('Gateway starting, starting telemetry server...'); startSidecar(); }); api.on('gateway:stop', () => { - logger.info('Gateway stopping, stopping monitor sidecar...'); + logger.info('Gateway stopping, stopping telemetry server...'); stopSidecar(); }); - // Also handle process signals directly - process.on('SIGTERM', () => { - stopSidecar(); - }); - - process.on('SIGINT', () => { - stopSidecar(); - }); + // Handle process signals + 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); + setTimeout(() => startSidecar(), 1000); } // Register status tool diff --git a/openclaw.plugin.json b/plugin/openclaw.plugin.json similarity index 98% rename from openclaw.plugin.json rename to plugin/openclaw.plugin.json index e39e010..6186f53 100644 --- a/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -3,7 +3,7 @@ "name": "HarborForge Monitor", "version": "0.1.0", "description": "Server monitoring plugin for HarborForge - streams telemetry to Monitor", - "entry": "./index.mjs", + "entry": "./index.js", "configSchema": { "type": "object", "additionalProperties": false, diff --git a/plugin/package.json b/plugin/package.json new file mode 100644 index 0000000..d3cd063 --- /dev/null +++ b/plugin/package.json @@ -0,0 +1,15 @@ +{ + "name": "harborforge-monitor-plugin", + "version": "0.1.0", + "description": "OpenClaw plugin for HarborForge Monitor", + "main": "index.js", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "license": "MIT" +} diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json new file mode 100644 index 0000000..9ae7628 --- /dev/null +++ b/plugin/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./", + "rootDir": "./", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["*.ts"], + "exclude": ["node_modules"] +} diff --git a/scripts/install.mjs b/scripts/install.mjs new file mode 100644 index 0000000..a66e003 --- /dev/null +++ b/scripts/install.mjs @@ -0,0 +1,317 @@ +#!/usr/bin/env node + +/** + * HarborForge Monitor Plugin Installer v0.1.0 + */ + +import { execSync } from 'child_process'; +import { + existsSync, + mkdirSync, + copyFileSync, + readdirSync, + rmSync, + readFileSync, + writeFileSync, +} from 'fs'; +import { dirname, join, resolve } from 'path'; +import { fileURLToPath } from 'url'; +import { homedir, platform } from 'os'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = resolve(dirname(__filename), '..'); + +const 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); +const options = { + openclawProfilePath: null, + buildOnly: args.includes('--build-only'), + skipCheck: args.includes('--skip-check'), + verbose: args.includes('--verbose') || args.includes('-v'), + uninstall: args.includes('--uninstall'), + installOnly: args.includes('--install'), +}; + +const profileIdx = args.indexOf('--openclaw-profile-path'); +if (profileIdx !== -1 && args[profileIdx + 1]) { + options.openclawProfilePath = resolve(args[profileIdx + 1]); +} + +function resolveOpenclawPath() { + if (options.openclawProfilePath) return options.openclawProfilePath; + if (process.env.OPENCLAW_PATH) return resolve(process.env.OPENCLAW_PATH); + return join(homedir(), '.openclaw'); +} + +const c = { + reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', + yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', +}; + +function log(msg, color = 'reset') { console.log(`${c[color]}${msg}${c.reset}`); } +function logStep(n, total, msg) { log(`[${n}/${total}] ${msg}`, 'cyan'); } +function logOk(msg) { log(` ✓ ${msg}`, 'green'); } +function logWarn(msg) { log(` ⚠ ${msg}`, 'yellow'); } +function logErr(msg) { log(` ✗ ${msg}`, 'red'); } + +function exec(command, opts = {}) { + return execSync(command, { + cwd: __dirname, + stdio: opts.silent ? 'pipe' : 'inherit', + encoding: 'utf8', + ...opts, + }); +} + +function getOpenclawConfig(key, def = undefined) { + try { + const out = exec(`openclaw config get ${key} --json 2>/dev/null || echo "undefined"`, { silent: true }).trim(); + if (out === 'undefined' || out === '') return def; + return JSON.parse(out); + } catch { return def; } +} + +function setOpenclawConfig(key, value) { + exec(`openclaw config set ${key} '${JSON.stringify(value)}' --json`, { silent: true }); +} + +function unsetOpenclawConfig(key) { + try { exec(`openclaw config unset ${key}`, { silent: true }); } catch {} +} + +function copyDir(src, dest) { + 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); + } +} + +function detectEnvironment() { + logStep(1, 5, 'Detecting environment...'); + const env = { platform: platform(), nodeVersion: null }; + + try { + env.nodeVersion = exec('node --version', { silent: true }).trim(); + logOk(`Node.js ${env.nodeVersion}`); + } catch { + logErr('Node.js not found'); + } + + try { + logOk(`openclaw at ${exec('which openclaw', { silent: true }).trim()}`); + } catch { + logWarn('openclaw CLI not in PATH'); + } + + return env; +} + +function checkDeps(env) { + if (options.skipCheck) { logStep(2, 5, 'Skipping dep checks'); return; } + logStep(2, 5, 'Checking dependencies...'); + + let fail = false; + if (!env.nodeVersion || parseInt(env.nodeVersion.slice(1)) < 18) { + logErr('Node.js 18+ required'); + fail = true; + } + + if (fail) { + log('\nInstall missing deps and retry.', 'red'); + process.exit(1); + } + + logOk('All deps OK'); +} + +async function build() { + logStep(3, 5, 'Building plugin...'); + + log(' Building TypeScript plugin...', 'blue'); + exec('npm install', { cwd: PLUGIN_SRC_DIR, silent: !options.verbose }); + exec('npm run build', { cwd: PLUGIN_SRC_DIR, silent: !options.verbose }); + logOk('plugin compiled'); +} + +function clearInstallTargets(openclawPath) { + const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); + if (existsSync(destDir)) { + rmSync(destDir, { recursive: true, force: true }); + logOk(`Removed ${destDir}`); + } +} + +function cleanupConfig(openclawPath) { + const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); + + 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'); + } + + unsetOpenclawConfig(`plugins.entries.${PLUGIN_NAME}`); + logOk('Removed plugin entry'); + + const paths = getOpenclawConfig('plugins.load.paths', []); + const pidx = paths.indexOf(destDir); + if (pidx !== -1) { + paths.splice(pidx, 1); + setOpenclawConfig('plugins.load.paths', paths); + logOk('Removed from load paths'); + } + } catch (err) { + logWarn(`Config cleanup: ${err.message}`); + } +} + +async function install() { + if (options.buildOnly) { logStep(4, 5, 'Skipping install (--build-only)'); return null; } + logStep(4, 5, 'Installing...'); + + const openclawPath = resolveOpenclawPath(); + const pluginsDir = join(openclawPath, 'plugins'); + const destDir = join(pluginsDir, PLUGIN_NAME); + + log(` OpenClaw path: ${openclawPath}`, 'blue'); + + if (existsSync(destDir)) { + logWarn('Existing install detected, uninstalling before install...'); + clearInstallTargets(openclawPath); + cleanupConfig(openclawPath); + } + + if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true }); + + // Copy compiled plugin + mkdirSync(destDir, { recursive: true }); + copyDir(PLUGIN_SRC_DIR, destDir); + + // Copy telemetry server + const serverDestDir = join(destDir, 'server'); + mkdirSync(serverDestDir, { recursive: true }); + copyDir(SERVER_SRC_DIR, serverDestDir); + logOk(`Server files → ${serverDestDir}`); + + // Copy skills + if (existsSync(SKILLS_SRC_DIR)) { + const skillsDestDir = join(openclawPath, 'skills'); + mkdirSync(skillsDestDir, { recursive: true }); + copyDir(SKILLS_SRC_DIR, skillsDestDir); + logOk(`Skills → ${skillsDestDir}`); + } + + // Install runtime deps + exec('npm install --omit=dev', { cwd: destDir, silent: !options.verbose }); + logOk('Runtime deps installed'); + + return { destDir }; +} + +async function configure() { + if (options.buildOnly) { logStep(5, 5, 'Skipping config'); return; } + logStep(5, 5, 'Configuring OpenClaw...'); + + const openclawPath = resolveOpenclawPath(); + const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); + + try { + const paths = getOpenclawConfig('plugins.load.paths', []); + if (!paths.includes(destDir)) { + paths.push(destDir); + setOpenclawConfig('plugins.load.paths', paths); + } + logOk(`plugins.load.paths includes ${destDir}`); + + const allow = getOpenclawConfig('plugins.allow', []); + if (!allow.includes(PLUGIN_NAME)) { + allow.push(PLUGIN_NAME); + setOpenclawConfig('plugins.allow', allow); + } + logOk(`plugins.allow includes ${PLUGIN_NAME}`); + + // Note: challengeUuid must be configured manually by user + logOk('Plugin configured (remember to set challengeUuid in ~/.openclaw/openclaw.json)'); + + } catch (err) { + logWarn(`Config failed: ${err.message}`); + } +} + +function summary() { + logStep(5, 5, 'Done!'); + console.log(''); + log('╔══════════════════════════════════════════════╗', 'cyan'); + log('║ HarborForge Monitor v0.1.0 Install Complete ║', 'cyan'); + log('╚══════════════════════════════════════════════╝', 'cyan'); + + if (options.buildOnly) { + log('\nBuild-only — plugin not installed.', 'yellow'); + return; + } + + console.log(''); + log('Next steps:', 'blue'); + log(' 1. Register server in HarborForge Monitor to get challengeUuid', 'cyan'); + log(' 2. Edit ~/.openclaw/openclaw.json:', 'cyan'); + log(' {', 'cyan'); + log(' "plugins": {', 'cyan'); + log(' "harborforge-monitor": {', 'cyan'); + log(' "enabled": true,', 'cyan'); + log(' "challengeUuid": "your-challenge-uuid"', 'cyan'); + log(' }', 'cyan'); + log(' }', 'cyan'); + log(' }', 'cyan'); + log(' 3. openclaw gateway restart', 'cyan'); + console.log(''); +} + +async function uninstall() { + log('Uninstalling HarborForge Monitor...', 'cyan'); + const openclawPath = resolveOpenclawPath(); + clearInstallTargets(openclawPath); + cleanupConfig(openclawPath); + log('\nRun: openclaw gateway restart', 'yellow'); +} + +async function main() { + console.log(''); + log('╔══════════════════════════════════════════════╗', 'cyan'); + log('║ HarborForge Monitor Plugin Installer v0.1.0 ║', 'cyan'); + log('╚══════════════════════════════════════════════╝', 'cyan'); + console.log(''); + + try { + const env = detectEnvironment(); + + if (options.uninstall) { + await uninstall(); + process.exit(0); + } + + checkDeps(env); + await build(); + + if (!options.buildOnly) { + await install(); + await configure(); + } + + summary(); + } catch (err) { + log(`\nInstallation failed: ${err.message}`, 'red'); + process.exit(1); + } +} + +main(); diff --git a/sidecar/server.mjs b/server/telemetry.mjs similarity index 91% rename from sidecar/server.mjs rename to server/telemetry.mjs index 913727c..3434aac 100644 --- a/sidecar/server.mjs +++ b/server/telemetry.mjs @@ -1,10 +1,9 @@ /** - * HarborForge Monitor Sidecar Server + * HarborForge Monitor Telemetry Server * * Runs as separate process from Gateway. * Collects system metrics and OpenClaw status, sends to Monitor. */ -import { createServer } from 'http'; import { readFile, access } from 'fs/promises'; import { constants } from 'fs'; import { exec } from 'child_process'; @@ -44,18 +43,11 @@ let isShuttingDown = false; */ async function collectSystemMetrics() { try { - // CPU usage (average over 1 second) const cpuUsage = await getCpuUsage(); - - // Memory const memTotal = totalmem(); const memFree = freemem(); const memUsed = memTotal - memFree; - - // Disk usage const diskInfo = await getDiskUsage(); - - // Load average const loadAvg = platform() !== 'win32' ? require('os').loadavg() : [0, 0, 0]; return { @@ -93,7 +85,6 @@ async function getCpuUsage() { return isNaN(usage) ? 0 : Math.round(usage * 10) / 10; } } catch { - // Fallback: calculate from /proc/stat on Linux try { const stat = await readFile('/proc/stat', 'utf8'); const cpuLine = stat.split('\n')[0]; @@ -129,9 +120,6 @@ async function getDiskUsage() { return { totalGB: 0, usedGB: 0, usedPct: 0 }; } -/** - * Parse size string (like '50G' or '100M') to GB - */ function parseSizeToGB(size) { const num = parseFloat(size); if (size.includes('T')) return num * 1024; @@ -147,7 +135,6 @@ function parseSizeToGB(size) { async function collectOpenclawStatus() { try { const agents = await getOpenclawAgents(); - return { version: CONFIG.openclawVersion, agent_count: agents.length, @@ -168,14 +155,12 @@ async function collectOpenclawStatus() { */ async function getOpenclawAgents() { try { - // Try to read agent config/state from OpenClaw directory const agentConfigPath = `${CONFIG.openclawPath}/agents.json`; try { await access(agentConfigPath, constants.R_OK); const data = JSON.parse(await readFile(agentConfigPath, 'utf8')); return data.agents || []; } catch { - // Fallback: return empty list return []; } } catch { @@ -207,7 +192,6 @@ async function buildPayload() { async function sendHttpHeartbeat() { try { const payload = await buildPayload(); - log.debug('Sending HTTP heartbeat...'); const response = await fetch(`${CONFIG.backendUrl}/monitor/server/heartbeat`, { @@ -241,21 +225,16 @@ async function sendHttpHeartbeat() { async function reportingLoop() { while (!isShuttingDown) { try { - // Try HTTP (WebSocket can be added later) const success = await sendHttpHeartbeat(); - // Calculate next interval with backoff on failure let interval = CONFIG.reportIntervalSec * 1000; if (!success) { - // Exponential backoff: max 5 minutes const backoff = Math.min(consecutiveFailures * 10000, 300000); interval = Math.max(interval, backoff); log.info(`Retry in ${interval}ms (backoff)`); } - // Sleep until next report await new Promise(resolve => setTimeout(resolve, interval)); - } catch (err) { log.error('Reporting loop error:', err.message); await new Promise(resolve => setTimeout(resolve, 30000)); @@ -267,14 +246,13 @@ async function reportingLoop() { * Graceful shutdown */ function shutdown() { - log.info('Shutting down sidecar...'); + log.info('Shutting down telemetry server...'); isShuttingDown = true; if (wsConnection) { wsConnection.close(); } - // Send final heartbeat sendHttpHeartbeat().finally(() => { process.exit(0); }); @@ -285,18 +263,16 @@ process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); // Start -log.info('HarborForge Monitor Sidecar starting...'); +log.info('HarborForge Monitor Telemetry Server starting...'); log.info('Config:', { identifier: CONFIG.identifier, backendUrl: CONFIG.backendUrl, reportIntervalSec: CONFIG.reportIntervalSec, }); -// Validate config if (!CONFIG.challengeUuid) { log.error('Missing HF_MONITOR_CHALLENGE_UUID environment variable'); process.exit(1); } -// Start reporting loop reportingLoop();