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 +174 -0
- package/bin/agent.js +189 -0
- package/lib/docker.js +51 -0
- package/lib/openclaw.js +83 -0
- package/lib/server.js +105 -0
- package/lib/stats.js +75 -0
- package/package.json +32 -0
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 };
|
package/lib/openclaw.js
ADDED
|
@@ -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
|
+
}
|