lobsterboard-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # 🦞 LobsterBoard Agent
2
+
3
+ A lightweight stats agent for remote [LobsterBoard](https://github.com/Curbob/LobsterBoard) monitoring. Run it on your VPS or remote servers, then connect from your local LobsterBoard dashboard.
4
+
5
+ ## Features
6
+
7
+ - **System stats** — CPU, memory, disk, network, uptime
8
+ - **Docker stats** — Container list and status (optional)
9
+ - **OpenClaw stats** — Cron jobs, sessions, gateway status (optional)
10
+ - **API key auth** — Secure access to your server stats
11
+ - **Lightweight** — Minimal footprint, runs anywhere Node runs
12
+ - **Multi-server** — Monitor multiple servers from one LobsterBoard
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # Install globally
18
+ npm install -g lobsterboard-agent
19
+
20
+ # Initialize (generates API key)
21
+ lobsterboard-agent init
22
+
23
+ # Start the agent
24
+ lobsterboard-agent serve
25
+ ```
26
+
27
+ ## Commands
28
+
29
+ | Command | Description |
30
+ |---------|-------------|
31
+ | `init` | Initialize config and generate API key |
32
+ | `serve` | Start the stats server |
33
+ | `rotate-key` | Generate a new API key (invalidates old one) |
34
+ | `show-key` | Display current API key |
35
+ | `status` | Show agent configuration |
36
+
37
+ ## Options
38
+
39
+ ```bash
40
+ lobsterboard-agent serve --port=9090 # Custom port (default: 9090)
41
+ lobsterboard-agent serve --host=0.0.0.0 # Bind address (default: 0.0.0.0)
42
+ lobsterboard-agent serve --name=prod-1 # Server name for identification
43
+ ```
44
+
45
+ ## API Endpoints
46
+
47
+ All endpoints require the `X-API-Key` header.
48
+
49
+ | Endpoint | Description |
50
+ |----------|-------------|
51
+ | `GET /stats` | Full system stats (JSON) |
52
+ | `GET /health` | Health check |
53
+
54
+ ### Example Request
55
+
56
+ ```bash
57
+ curl -H "X-API-Key: sk_your_key_here" http://your-server:9090/stats
58
+ ```
59
+
60
+ ### Example Response
61
+
62
+ ```json
63
+ {
64
+ "serverName": "prod-vps-1",
65
+ "timestamp": "2026-03-07T12:00:00.000Z",
66
+ "hostname": "vps-12345",
67
+ "platform": "linux",
68
+ "distro": "Ubuntu",
69
+ "uptime": 1234567,
70
+ "cpu": {
71
+ "model": "Intel Xeon",
72
+ "cores": 2,
73
+ "usage": 12.5
74
+ },
75
+ "memory": {
76
+ "total": 2147483648,
77
+ "used": 1073741824,
78
+ "percent": 50.0
79
+ },
80
+ "disk": {
81
+ "total": 42949672960,
82
+ "used": 21474836480,
83
+ "percent": 50.0
84
+ },
85
+ "network": {
86
+ "rxSec": 1024,
87
+ "txSec": 512
88
+ },
89
+ "docker": {
90
+ "available": true,
91
+ "running": 3,
92
+ "total": 5,
93
+ "containers": [...]
94
+ },
95
+ "openclaw": {
96
+ "installed": true,
97
+ "gateway": { "running": true },
98
+ "cron": { "total": 5, "enabled": 4 },
99
+ "sessions": { "total": 12, "recent24h": 3 }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Configuration
105
+
106
+ Config is stored in `~/.lobsterboard-agent/config.json`:
107
+
108
+ ```json
109
+ {
110
+ "apiKey": "sk_...",
111
+ "port": 9090,
112
+ "host": "0.0.0.0",
113
+ "serverName": "my-vps",
114
+ "enableDocker": true,
115
+ "enableOpenClaw": true
116
+ }
117
+ ```
118
+
119
+ ## Run as Service
120
+
121
+ ### systemd (Linux)
122
+
123
+ ```bash
124
+ sudo cat > /etc/systemd/system/lobsterboard-agent.service << 'EOF'
125
+ [Unit]
126
+ Description=LobsterBoard Agent
127
+ After=network.target
128
+
129
+ [Service]
130
+ Type=simple
131
+ User=your-user
132
+ ExecStart=/usr/bin/lobsterboard-agent serve
133
+ Restart=always
134
+ RestartSec=5
135
+
136
+ [Install]
137
+ WantedBy=multi-user.target
138
+ EOF
139
+
140
+ sudo systemctl enable lobsterboard-agent
141
+ sudo systemctl start lobsterboard-agent
142
+ ```
143
+
144
+ ### pm2
145
+
146
+ ```bash
147
+ pm2 start lobsterboard-agent -- serve
148
+ pm2 save
149
+ pm2 startup
150
+ ```
151
+
152
+ ## Connecting from LobsterBoard
153
+
154
+ In LobsterBoard, add a **Remote Server** widget and configure:
155
+
156
+ - **URL**: `http://your-server:9090`
157
+ - **API Key**: Your agent's API key
158
+
159
+ You can add multiple widgets for multiple servers.
160
+
161
+ ## Security
162
+
163
+ - Always use an API key (never disable it)
164
+ - Consider running behind a reverse proxy with HTTPS
165
+ - Use firewall rules to limit access by IP if possible
166
+ - Rotate keys periodically with `lobsterboard-agent rotate-key`
167
+
168
+ ## License
169
+
170
+ MIT
171
+
172
+ ---
173
+
174
+ Made with 🦞 by [Curbob](https://github.com/Curbob)
package/bin/agent.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * LobsterBoard Agent CLI
5
+ *
6
+ * Commands:
7
+ * init - Initialize config and generate API key
8
+ * serve - Start the stats server
9
+ * rotate-key - Generate a new API key
10
+ * show-key - Display current API key
11
+ * status - Show agent status
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+ const os = require('os');
18
+
19
+ const CONFIG_DIR = path.join(os.homedir(), '.lobsterboard-agent');
20
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
21
+
22
+ const DEFAULT_CONFIG = {
23
+ apiKey: null,
24
+ port: 9090,
25
+ host: '0.0.0.0',
26
+ enableDocker: true,
27
+ enableOpenClaw: true,
28
+ serverName: os.hostname(),
29
+ };
30
+
31
+ // Ensure config directory exists
32
+ function ensureConfigDir() {
33
+ if (!fs.existsSync(CONFIG_DIR)) {
34
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
35
+ }
36
+ }
37
+
38
+ // Load config
39
+ function loadConfig() {
40
+ ensureConfigDir();
41
+ if (fs.existsSync(CONFIG_FILE)) {
42
+ try {
43
+ return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) };
44
+ } catch (e) {
45
+ console.error('Error reading config:', e.message);
46
+ return { ...DEFAULT_CONFIG };
47
+ }
48
+ }
49
+ return { ...DEFAULT_CONFIG };
50
+ }
51
+
52
+ // Save config
53
+ function saveConfig(config) {
54
+ ensureConfigDir();
55
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
56
+ }
57
+
58
+ // Generate API key
59
+ function generateApiKey() {
60
+ return 'sk_' + crypto.randomBytes(24).toString('base64url');
61
+ }
62
+
63
+ // Commands
64
+ const commands = {
65
+ init() {
66
+ const config = loadConfig();
67
+ if (config.apiKey) {
68
+ console.log('Config already exists.');
69
+ console.log('Use "lobsterboard-agent rotate-key" to generate a new key.');
70
+ console.log('\nCurrent config:', CONFIG_FILE);
71
+ return;
72
+ }
73
+
74
+ config.apiKey = generateApiKey();
75
+ saveConfig(config);
76
+
77
+ console.log('✅ LobsterBoard Agent initialized!\n');
78
+ console.log('Your API key (save this!):\n');
79
+ console.log(` ${config.apiKey}\n`);
80
+ console.log('Config saved to:', CONFIG_FILE);
81
+ console.log('\nStart the agent with:');
82
+ console.log(' lobsterboard-agent serve');
83
+ },
84
+
85
+ serve() {
86
+ const config = loadConfig();
87
+
88
+ if (!config.apiKey) {
89
+ console.error('No API key configured. Run "lobsterboard-agent init" first.');
90
+ process.exit(1);
91
+ }
92
+
93
+ // Import and start server
94
+ const { startServer } = require('../lib/server.js');
95
+ startServer(config);
96
+ },
97
+
98
+ 'rotate-key'() {
99
+ const config = loadConfig();
100
+ const oldKey = config.apiKey;
101
+ config.apiKey = generateApiKey();
102
+ saveConfig(config);
103
+
104
+ console.log('✅ API key rotated!\n');
105
+ if (oldKey) {
106
+ console.log('Old key (now invalid):', oldKey.slice(0, 10) + '...');
107
+ }
108
+ console.log('New key:', config.apiKey);
109
+ console.log('\nRestart the agent to apply.');
110
+ },
111
+
112
+ 'show-key'() {
113
+ const config = loadConfig();
114
+ if (!config.apiKey) {
115
+ console.log('No API key configured. Run "lobsterboard-agent init" first.');
116
+ return;
117
+ }
118
+ console.log('API Key:', config.apiKey);
119
+ },
120
+
121
+ status() {
122
+ const config = loadConfig();
123
+ console.log('LobsterBoard Agent Status\n');
124
+ console.log('Config file:', CONFIG_FILE);
125
+ console.log('API key:', config.apiKey ? config.apiKey.slice(0, 10) + '...' : '(not set)');
126
+ console.log('Port:', config.port);
127
+ console.log('Host:', config.host);
128
+ console.log('Server name:', config.serverName);
129
+ console.log('Docker stats:', config.enableDocker ? 'enabled' : 'disabled');
130
+ console.log('OpenClaw stats:', config.enableOpenClaw ? 'enabled' : 'disabled');
131
+ },
132
+
133
+ help() {
134
+ console.log(`
135
+ LobsterBoard Agent - Remote stats for LobsterBoard dashboards
136
+
137
+ Usage: lobsterboard-agent <command> [options]
138
+
139
+ Commands:
140
+ init Initialize config and generate API key
141
+ serve Start the stats server
142
+ rotate-key Generate a new API key (invalidates old one)
143
+ show-key Display current API key
144
+ status Show agent configuration
145
+ help Show this help
146
+
147
+ Options:
148
+ --port=PORT Override port (default: 9090)
149
+ --host=HOST Override host (default: 0.0.0.0)
150
+ --name=NAME Set server name for identification
151
+
152
+ Examples:
153
+ lobsterboard-agent init
154
+ lobsterboard-agent serve
155
+ lobsterboard-agent serve --port=8888
156
+ `);
157
+ }
158
+ };
159
+
160
+ // Parse args
161
+ const args = process.argv.slice(2);
162
+ let command = args[0] || 'help';
163
+
164
+ // Parse options
165
+ const options = {};
166
+ args.slice(1).forEach(arg => {
167
+ const match = arg.match(/^--(\w+)=(.+)$/);
168
+ if (match) {
169
+ options[match[1]] = match[2];
170
+ }
171
+ });
172
+
173
+ // Apply option overrides to config for serve command
174
+ if (command === 'serve' && Object.keys(options).length > 0) {
175
+ const config = loadConfig();
176
+ if (options.port) config.port = parseInt(options.port, 10);
177
+ if (options.host) config.host = options.host;
178
+ if (options.name) config.serverName = options.name;
179
+ saveConfig(config);
180
+ }
181
+
182
+ // Run command
183
+ if (commands[command]) {
184
+ commands[command]();
185
+ } else {
186
+ console.error(`Unknown command: ${command}`);
187
+ commands.help();
188
+ process.exit(1);
189
+ }
package/lib/docker.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Docker stats collection (optional)
3
+ */
4
+
5
+ const { exec } = require('child_process');
6
+ const { promisify } = require('util');
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ async function collectDockerStats() {
11
+ try {
12
+ // Check if Docker is available
13
+ await execAsync('docker info', { timeout: 5000 });
14
+ } catch (e) {
15
+ return { available: false };
16
+ }
17
+
18
+ try {
19
+ // Get container list
20
+ const { stdout } = await execAsync(
21
+ 'docker ps --format "{{.ID}}|{{.Names}}|{{.Status}}|{{.Image}}"',
22
+ { timeout: 10000 }
23
+ );
24
+
25
+ const containers = stdout.trim().split('\n').filter(Boolean).map(line => {
26
+ const [id, name, status, image] = line.split('|');
27
+ return {
28
+ id: id?.slice(0, 12),
29
+ name,
30
+ status,
31
+ image,
32
+ running: status?.toLowerCase().includes('up'),
33
+ };
34
+ });
35
+
36
+ // Get counts
37
+ const { stdout: allCount } = await execAsync('docker ps -aq | wc -l', { timeout: 5000 });
38
+ const { stdout: runningCount } = await execAsync('docker ps -q | wc -l', { timeout: 5000 });
39
+
40
+ return {
41
+ available: true,
42
+ total: parseInt(allCount.trim(), 10) || 0,
43
+ running: parseInt(runningCount.trim(), 10) || 0,
44
+ containers: containers.slice(0, 20), // Limit to 20
45
+ };
46
+ } catch (e) {
47
+ return { available: true, error: e.message };
48
+ }
49
+ }
50
+
51
+ module.exports = { collectDockerStats };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * OpenClaw stats collection (optional)
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
10
+ const CRON_FILE = path.join(OPENCLAW_DIR, 'cron', 'jobs.json');
11
+ const SESSIONS_DIR = path.join(OPENCLAW_DIR, 'sessions');
12
+
13
+ async function collectOpenClawStats() {
14
+ // Check if OpenClaw is installed
15
+ if (!fs.existsSync(OPENCLAW_DIR)) {
16
+ return { installed: false };
17
+ }
18
+
19
+ const stats = { installed: true };
20
+
21
+ // Cron jobs
22
+ try {
23
+ if (fs.existsSync(CRON_FILE)) {
24
+ const data = JSON.parse(fs.readFileSync(CRON_FILE, 'utf8'));
25
+ const jobs = data.jobs || [];
26
+ stats.cron = {
27
+ total: jobs.length,
28
+ enabled: jobs.filter(j => j.enabled !== false).length,
29
+ jobs: jobs.slice(0, 10).map(j => ({
30
+ name: j.name,
31
+ schedule: j.schedule?.expr || j.schedule?.kind,
32
+ enabled: j.enabled !== false,
33
+ })),
34
+ };
35
+ }
36
+ } catch (e) {
37
+ stats.cron = { error: e.message };
38
+ }
39
+
40
+ // Sessions (count recent activity)
41
+ try {
42
+ if (fs.existsSync(SESSIONS_DIR)) {
43
+ const sessionFiles = fs.readdirSync(SESSIONS_DIR)
44
+ .filter(f => f.endsWith('.json'));
45
+
46
+ // Count sessions active in last 24h
47
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
48
+ let recentCount = 0;
49
+
50
+ for (const file of sessionFiles.slice(0, 100)) {
51
+ try {
52
+ const filePath = path.join(SESSIONS_DIR, file);
53
+ const stat = fs.statSync(filePath);
54
+ if (stat.mtimeMs > oneDayAgo) {
55
+ recentCount++;
56
+ }
57
+ } catch (e) { /* skip */ }
58
+ }
59
+
60
+ stats.sessions = {
61
+ total: sessionFiles.length,
62
+ recent24h: recentCount,
63
+ };
64
+ }
65
+ } catch (e) {
66
+ stats.sessions = { error: e.message };
67
+ }
68
+
69
+ // Gateway status (check if process is running)
70
+ try {
71
+ const { execSync } = require('child_process');
72
+ const result = execSync('pgrep -f "openclaw.*gateway" || true', { encoding: 'utf8' });
73
+ stats.gateway = {
74
+ running: result.trim().length > 0,
75
+ };
76
+ } catch (e) {
77
+ stats.gateway = { running: false };
78
+ }
79
+
80
+ return stats;
81
+ }
82
+
83
+ module.exports = { collectOpenClawStats };
package/lib/server.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * LobsterBoard Agent HTTP Server
3
+ */
4
+
5
+ const http = require('http');
6
+ const { collectStats } = require('./stats.js');
7
+ const { collectDockerStats } = require('./docker.js');
8
+ const { collectOpenClawStats } = require('./openclaw.js');
9
+
10
+ function startServer(config) {
11
+ const { apiKey, port, host, serverName, enableDocker, enableOpenClaw } = config;
12
+
13
+ const server = http.createServer(async (req, res) => {
14
+ // CORS headers for browser requests
15
+ res.setHeader('Access-Control-Allow-Origin', '*');
16
+ res.setHeader('Access-Control-Allow-Headers', 'X-API-Key, Content-Type');
17
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
18
+
19
+ // Handle preflight
20
+ if (req.method === 'OPTIONS') {
21
+ res.writeHead(204);
22
+ res.end();
23
+ return;
24
+ }
25
+
26
+ // Check API key
27
+ const providedKey = req.headers['x-api-key'];
28
+ if (providedKey !== apiKey) {
29
+ res.writeHead(401, { 'Content-Type': 'application/json' });
30
+ res.end(JSON.stringify({ error: 'Invalid or missing API key' }));
31
+ return;
32
+ }
33
+
34
+ const url = new URL(req.url, `http://${req.headers.host}`);
35
+ const pathname = url.pathname;
36
+
37
+ // Routes
38
+ if (req.method === 'GET' && pathname === '/stats') {
39
+ try {
40
+ const stats = await collectStats();
41
+ stats.serverName = serverName;
42
+ stats.timestamp = new Date().toISOString();
43
+
44
+ // Add Docker stats if enabled
45
+ if (enableDocker) {
46
+ stats.docker = await collectDockerStats();
47
+ }
48
+
49
+ // Add OpenClaw stats if enabled
50
+ if (enableOpenClaw) {
51
+ stats.openclaw = await collectOpenClawStats();
52
+ }
53
+
54
+ res.writeHead(200, { 'Content-Type': 'application/json' });
55
+ res.end(JSON.stringify(stats));
56
+ } catch (err) {
57
+ res.writeHead(500, { 'Content-Type': 'application/json' });
58
+ res.end(JSON.stringify({ error: err.message }));
59
+ }
60
+ return;
61
+ }
62
+
63
+ if (req.method === 'GET' && pathname === '/health') {
64
+ res.writeHead(200, { 'Content-Type': 'application/json' });
65
+ res.end(JSON.stringify({ status: 'ok', serverName }));
66
+ return;
67
+ }
68
+
69
+ if (req.method === 'GET' && pathname === '/') {
70
+ res.writeHead(200, { 'Content-Type': 'application/json' });
71
+ res.end(JSON.stringify({
72
+ name: 'LobsterBoard Agent',
73
+ version: require('../package.json').version,
74
+ serverName,
75
+ endpoints: ['/stats', '/health']
76
+ }));
77
+ return;
78
+ }
79
+
80
+ // 404
81
+ res.writeHead(404, { 'Content-Type': 'application/json' });
82
+ res.end(JSON.stringify({ error: 'Not found' }));
83
+ });
84
+
85
+ server.listen(port, host, () => {
86
+ console.log(`
87
+ 🦞 LobsterBoard Agent running!
88
+
89
+ Server: http://${host}:${port}
90
+ Name: ${serverName}
91
+
92
+ Endpoints:
93
+ GET /stats - System stats (requires X-API-Key header)
94
+ GET /health - Health check (requires X-API-Key header)
95
+
96
+ Press Ctrl+C to stop
97
+ `);
98
+ });
99
+
100
+ // Graceful shutdown
101
+ process.on('SIGTERM', () => server.close(() => process.exit(0)));
102
+ process.on('SIGINT', () => server.close(() => process.exit(0)));
103
+ }
104
+
105
+ module.exports = { startServer };
package/lib/stats.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * System stats collection using systeminformation
3
+ */
4
+
5
+ const si = require('systeminformation');
6
+ const os = require('os');
7
+
8
+ async function collectStats() {
9
+ const [cpu, mem, fsSize, networkStats, osInfo, currentLoad] = await Promise.all([
10
+ si.cpu(),
11
+ si.mem(),
12
+ si.fsSize(),
13
+ si.networkStats(),
14
+ si.osInfo(),
15
+ si.currentLoad(),
16
+ ]);
17
+
18
+ // Calculate network totals
19
+ let netRx = 0, netTx = 0;
20
+ for (const iface of networkStats) {
21
+ netRx += iface.rx_sec || 0;
22
+ netTx += iface.tx_sec || 0;
23
+ }
24
+
25
+ // Get primary disk (largest or root)
26
+ const primaryDisk = fsSize.reduce((best, disk) => {
27
+ if (!best || disk.size > best.size) return disk;
28
+ return best;
29
+ }, null);
30
+
31
+ return {
32
+ hostname: os.hostname(),
33
+ platform: osInfo.platform,
34
+ distro: osInfo.distro,
35
+ release: osInfo.release,
36
+ uptime: os.uptime(),
37
+
38
+ cpu: {
39
+ model: cpu.brand,
40
+ cores: cpu.cores,
41
+ speed: cpu.speed,
42
+ usage: Math.round(currentLoad.currentLoad * 10) / 10,
43
+ },
44
+
45
+ memory: {
46
+ total: mem.total,
47
+ used: mem.used,
48
+ free: mem.free,
49
+ available: mem.available,
50
+ percent: Math.round((mem.used / mem.total) * 1000) / 10,
51
+ },
52
+
53
+ disk: primaryDisk ? {
54
+ mount: primaryDisk.mount,
55
+ type: primaryDisk.type,
56
+ total: primaryDisk.size,
57
+ used: primaryDisk.used,
58
+ free: primaryDisk.available,
59
+ percent: Math.round(primaryDisk.use * 10) / 10,
60
+ } : null,
61
+
62
+ network: {
63
+ rxSec: Math.round(netRx),
64
+ txSec: Math.round(netTx),
65
+ },
66
+
67
+ load: {
68
+ avg1: os.loadavg()[0],
69
+ avg5: os.loadavg()[1],
70
+ avg15: os.loadavg()[2],
71
+ },
72
+ };
73
+ }
74
+
75
+ module.exports = { collectStats };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "lobsterboard-agent",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight stats agent for remote LobsterBoard monitoring",
5
+ "main": "lib/server.js",
6
+ "bin": {
7
+ "lobsterboard-agent": "./bin/agent.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/agent.js serve",
11
+ "test": "node --test"
12
+ },
13
+ "keywords": [
14
+ "lobsterboard",
15
+ "monitoring",
16
+ "stats",
17
+ "agent",
18
+ "openclaw"
19
+ ],
20
+ "author": "Curbob",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Curbob/lobsterboard-agent.git"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "dependencies": {
30
+ "systeminformation": "^5.21.0"
31
+ }
32
+ }