clawps 1.0.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/LICENSE +21 -0
- package/README.md +116 -0
- package/clawps.js +351 -0
- package/clawtop.js +646 -0
- package/package.json +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kevin Smith
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# clawps 🐾
|
|
2
|
+
|
|
3
|
+
A procps-style package of utilities for [OpenClaw](https://openclaw.ai) sessions.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ clawps
|
|
7
|
+
|
|
8
|
+
STATUS AGENT MODEL CONTEXT IDLE CHANNEL KIND
|
|
9
|
+
-------------------------------------------------------------------------------------------------------------
|
|
10
|
+
active Kevin Smith (@spleck) kimi-k2.5 15K/250K 2m telegram other
|
|
11
|
+
stale Daily SPA Generator kimi-k2.5 250K/250K 4h cron other
|
|
12
|
+
-----------------------------------------------------------------------------------------------------------
|
|
13
|
+
2 sessions
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
$ clawtop
|
|
18
|
+
|
|
19
|
+
PID SESSIONS MODEL CPU MSGS CTX/MAX UPTIME
|
|
20
|
+
-------------------------------------------------------------------------------------------------
|
|
21
|
+
1699 Kevin Smith (main) minimax-m2.5 0.1 142 45K/250K 12m
|
|
22
|
+
1705 pm-daily-spas (isolated) kimi-k2.5 0.0 28 12K/250K 4h
|
|
23
|
+
-------------------------------------------------------------------------------------------------
|
|
24
|
+
Total: 2 sessions | Context: 57K/500K (11%)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Clone or download
|
|
31
|
+
git clone https://github.com/spleck/clawps.git
|
|
32
|
+
cd clawps
|
|
33
|
+
|
|
34
|
+
# Make executable and link to your PATH
|
|
35
|
+
chmod +x clawps.js
|
|
36
|
+
ln -s $(pwd)/clawps.js ~/.local/bin/clawps
|
|
37
|
+
|
|
38
|
+
chmod +x clawtop.js
|
|
39
|
+
ln -s $(pwd)/clawtop.js ~/.local/bin/clawtop
|
|
40
|
+
|
|
41
|
+
# Or install globally via npm
|
|
42
|
+
npm link
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Make sure `~/.local/bin` is in your PATH:
|
|
46
|
+
```bash
|
|
47
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
- Node.js 18+
|
|
53
|
+
- OpenClaw gateway running (default: localhost:18789)
|
|
54
|
+
- Gateway auth token read from `~/.openclaw/openclaw.json`
|
|
55
|
+
|
|
56
|
+
## Utilities
|
|
57
|
+
|
|
58
|
+
### clawps
|
|
59
|
+
|
|
60
|
+
Process-style session listing.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
clawps # Basic session listing
|
|
64
|
+
clawps -v # Verbose/detailed output
|
|
65
|
+
clawps --json # JSON output for scripting
|
|
66
|
+
clawps -w # Watch mode (auto-refresh)
|
|
67
|
+
clawps -w -n5 # Watch mode, refresh every 5 seconds
|
|
68
|
+
clawps --no-color # Disable colors
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### clawtop
|
|
72
|
+
|
|
73
|
+
Top-style real-time session monitor.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
clawtop # Real-time monitoring (default 3s refresh)
|
|
77
|
+
clawtop -n5 # Refresh every 5 seconds
|
|
78
|
+
clawtop --json # JSON output for scripting
|
|
79
|
+
clawtop --no-color # Disable colors
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Output
|
|
83
|
+
|
|
84
|
+
### clawps
|
|
85
|
+
|
|
86
|
+
| Column | Description |
|
|
87
|
+
|--------|-------------|
|
|
88
|
+
| STATUS | active 🟢 / idle 🟡 / stale 🔴 |
|
|
89
|
+
| AGENT | Session/agent display name |
|
|
90
|
+
| MODEL | AI model in use (shortened) |
|
|
91
|
+
| CONTEXT | Current / max tokens (e.g., `15K/250K`) |
|
|
92
|
+
| IDLE | Time since last activity |
|
|
93
|
+
| CHANNEL | Communication channel |
|
|
94
|
+
| KIND | Session type |
|
|
95
|
+
|
|
96
|
+
### clawtop
|
|
97
|
+
|
|
98
|
+
| Column | Description |
|
|
99
|
+
|--------|-------------|
|
|
100
|
+
| PID | Process/Session ID |
|
|
101
|
+
| SESSIONS | Session name and type |
|
|
102
|
+
| MODEL | AI model in use |
|
|
103
|
+
| CPU | Estimated CPU usage |
|
|
104
|
+
| MSGS | Message count this session |
|
|
105
|
+
| CTX/MAX | Context usage |
|
|
106
|
+
| UPTIME | Session runtime |
|
|
107
|
+
|
|
108
|
+
## How It Works
|
|
109
|
+
|
|
110
|
+
Both tools query your local OpenClaw gateway via the `sessions_list` tool:
|
|
111
|
+
- `clawps` — formats sessions like Unix `ps`
|
|
112
|
+
- `clawtop` — monitors like Unix `top`
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/clawps.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* clawps - List OpenClaw sessions like the `ps` command
|
|
4
|
+
* Usage: clawps [options]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
// ANSI color codes
|
|
13
|
+
const COLORS = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
bright: '\x1b[1m',
|
|
16
|
+
dim: '\x1b[2m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
green: '\x1b[32m',
|
|
19
|
+
yellow: '\x1b[33m',
|
|
20
|
+
blue: '\x1b[34m',
|
|
21
|
+
magenta: '\x1b[35m',
|
|
22
|
+
cyan: '\x1b[36m',
|
|
23
|
+
white: '\x1b[37m',
|
|
24
|
+
gray: '\x1b[90m',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Parse CLI arguments
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const options = {
|
|
30
|
+
color: !args.includes('--no-color') && process.stdout.isTTY,
|
|
31
|
+
verbose: args.includes('-v') || args.includes('--verbose'),
|
|
32
|
+
help: args.includes('-h') || args.includes('--help'),
|
|
33
|
+
json: args.includes('--json'),
|
|
34
|
+
watch: args.includes('-w') || args.includes('--watch'),
|
|
35
|
+
interval: 2000, // ms for watch mode
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Watch interval override
|
|
39
|
+
const intervalArg = args.find(a => a.startsWith('-n'));
|
|
40
|
+
if (intervalArg) {
|
|
41
|
+
const val = parseInt(intervalArg.replace('-n', ''), 10);
|
|
42
|
+
if (!isNaN(val)) options.interval = val * 1000;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printHelp() {
|
|
46
|
+
console.log(`
|
|
47
|
+
Usage: clawps [options]
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
-h, --help Show this help message
|
|
51
|
+
-v, --verbose Show detailed session information
|
|
52
|
+
--no-color Disable colored output
|
|
53
|
+
--json Output as JSON
|
|
54
|
+
-w, --watch Refresh continuously (like watch command)
|
|
55
|
+
-n<secs> Watch interval in seconds (default: 2)
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
clawps # Basic session listing
|
|
59
|
+
clawps -v # Verbose output
|
|
60
|
+
clawps --no-color # Plain text output
|
|
61
|
+
clawps -w -n5 # Refresh every 5 seconds
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (options.help) {
|
|
66
|
+
printHelp();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function color(code, text) {
|
|
71
|
+
return options.color ? `${COLORS[code]}${text}${COLORS.reset}` : text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getGatewayConfig() {
|
|
75
|
+
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
76
|
+
try {
|
|
77
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
78
|
+
const config = JSON.parse(raw);
|
|
79
|
+
return {
|
|
80
|
+
port: config.gateway?.port || 18789,
|
|
81
|
+
token: config.gateway?.auth?.token,
|
|
82
|
+
};
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return { port: 18789, token: null };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function invokeTool(tool, args = {}) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const { port, token } = getGatewayConfig();
|
|
91
|
+
const postData = JSON.stringify({ tool, args });
|
|
92
|
+
|
|
93
|
+
const headers = {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
'Accept': 'application/json',
|
|
96
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (token) {
|
|
100
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const req = http.request({
|
|
104
|
+
hostname: 'localhost',
|
|
105
|
+
port,
|
|
106
|
+
path: '/tools/invoke',
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers,
|
|
109
|
+
}, (res) => {
|
|
110
|
+
let data = '';
|
|
111
|
+
res.on('data', chunk => data += chunk);
|
|
112
|
+
res.on('end', () => {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(data);
|
|
115
|
+
if (parsed.ok && parsed.result) {
|
|
116
|
+
// Extract sessions from the tool result
|
|
117
|
+
const result = parsed.result;
|
|
118
|
+
if (result.content && result.content[0]?.text) {
|
|
119
|
+
// Parse the JSON text content
|
|
120
|
+
const innerResult = JSON.parse(result.content[0].text);
|
|
121
|
+
resolve(innerResult.sessions || []);
|
|
122
|
+
} else if (result.details?.sessions) {
|
|
123
|
+
resolve(result.details.sessions);
|
|
124
|
+
} else {
|
|
125
|
+
resolve([]);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
reject(new Error(parsed.error?.message || 'Unknown error'));
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
reject(new Error('Invalid JSON response'));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
req.on('error', reject);
|
|
137
|
+
req.setTimeout(5000, () => {
|
|
138
|
+
req.destroy();
|
|
139
|
+
reject(new Error('Request timeout'));
|
|
140
|
+
});
|
|
141
|
+
req.write(postData);
|
|
142
|
+
req.end();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatDuration(ms) {
|
|
147
|
+
if (!ms || ms < 0) return '0s';
|
|
148
|
+
const seconds = Math.floor(ms / 1000);
|
|
149
|
+
const minutes = Math.floor(seconds / 60);
|
|
150
|
+
const hours = Math.floor(minutes / 60);
|
|
151
|
+
const days = Math.floor(hours / 24);
|
|
152
|
+
|
|
153
|
+
if (days > 0) return `${days}d${hours % 24}h`;
|
|
154
|
+
if (hours > 0) return `${hours}h${minutes % 60}m`;
|
|
155
|
+
if (minutes > 0) return `${minutes}m${seconds % 60}s`;
|
|
156
|
+
return `${seconds}s`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatBytes(bytes) {
|
|
160
|
+
if (!bytes) return '0B';
|
|
161
|
+
const units = ['B', 'K', 'M', 'G'];
|
|
162
|
+
let idx = 0;
|
|
163
|
+
while (bytes >= 1024 && idx < units.length - 1) {
|
|
164
|
+
bytes /= 1024;
|
|
165
|
+
idx++;
|
|
166
|
+
}
|
|
167
|
+
return `${Math.round(bytes)}${units[idx]}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getAgentName(session) {
|
|
171
|
+
// Extract agent name from session key or display name
|
|
172
|
+
if (session.displayName) {
|
|
173
|
+
// Remove common prefixes/suffixes for cleaner display
|
|
174
|
+
return session.displayName
|
|
175
|
+
.replace(/^Cron: /, '')
|
|
176
|
+
.replace(/^agent:main:/, '');
|
|
177
|
+
}
|
|
178
|
+
const parts = session.key?.split(':') || [];
|
|
179
|
+
return parts[parts.length - 1] || 'unknown';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getModelShort(model) {
|
|
183
|
+
if (!model) return '-';
|
|
184
|
+
return model
|
|
185
|
+
.replace('moonshot/', '')
|
|
186
|
+
.replace('openrouter/', 'or/')
|
|
187
|
+
.substring(0, 20);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getStatusIndicator(session) {
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
const idle = now - (session.updatedAt || 0);
|
|
193
|
+
|
|
194
|
+
// Consider session active if updated in last 5 minutes
|
|
195
|
+
if (idle < 5 * 60 * 1000) {
|
|
196
|
+
return color('green', '●');
|
|
197
|
+
}
|
|
198
|
+
// Idle if updated in last 30 minutes
|
|
199
|
+
if (idle < 30 * 60 * 1000) {
|
|
200
|
+
return color('yellow', '○');
|
|
201
|
+
}
|
|
202
|
+
return color('red', '○');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function truncate(str, len) {
|
|
206
|
+
if (!str) return '-'.padEnd(len);
|
|
207
|
+
if (str.length <= len) return str.padEnd(len);
|
|
208
|
+
return str.substring(0, len - 1) + '…';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function listSessions() {
|
|
212
|
+
try {
|
|
213
|
+
const sessions = await invokeTool('sessions_list', {});
|
|
214
|
+
|
|
215
|
+
if (options.json) {
|
|
216
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (sessions.length === 0) {
|
|
221
|
+
console.log(color('dim', 'No active sessions.'));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
|
|
227
|
+
if (options.verbose) {
|
|
228
|
+
// Verbose table format
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(color('bright', 'OpenClaw Sessions'));
|
|
231
|
+
console.log(color('dim', '═'.repeat(100)));
|
|
232
|
+
|
|
233
|
+
sessions.forEach((s, i) => {
|
|
234
|
+
const idle = now - (s.updatedAt || 0);
|
|
235
|
+
const status = getStatusIndicator(s);
|
|
236
|
+
const agentName = getAgentName(s);
|
|
237
|
+
|
|
238
|
+
console.log(`${status} ${color('bright', agentName)}`);
|
|
239
|
+
console.log(` Key: ${color('gray', s.key || '-')}`);
|
|
240
|
+
console.log(` Session: ${color('cyan', s.sessionId?.substring(0, 8) || '-')}`);
|
|
241
|
+
console.log(` Kind: ${s.kind || '-'}`);
|
|
242
|
+
console.log(` Channel: ${s.channel || '-'}`);
|
|
243
|
+
console.log(` Model: ${getModelShort(s.model)}`);
|
|
244
|
+
const currentTokens = s.totalTokens || 0;
|
|
245
|
+
const maxTokens = s.contextWindow || s.contextTokens || 0;
|
|
246
|
+
console.log(` Context: ${formatBytes(currentTokens)} / ${formatBytes(maxTokens)}`);
|
|
247
|
+
console.log(` Idle: ${formatDuration(idle)}`);
|
|
248
|
+
console.log(` Updated: ${new Date(s.updatedAt).toLocaleTimeString()}`);
|
|
249
|
+
|
|
250
|
+
if (s.label) {
|
|
251
|
+
console.log(` Label: ${color('magenta', s.label)}`);
|
|
252
|
+
}
|
|
253
|
+
if (s.abortedLastRun) {
|
|
254
|
+
console.log(` ${color('red', '⚠ Last run aborted')}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (i < sessions.length - 1) console.log();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
console.log(color('dim', '═'.repeat(100)));
|
|
261
|
+
console.log(`${color('green', '●')} Active ${color('yellow', '○')} Idle ${color('red', '○')} Stale`);
|
|
262
|
+
console.log();
|
|
263
|
+
} else {
|
|
264
|
+
// Compact ps-like format
|
|
265
|
+
// Columns: STATUS AGENT MODEL CONTEXT IDLE CHANNEL KIND
|
|
266
|
+
const headers = ['STATUS', 'AGENT', 'MODEL', 'CONTEXT', 'IDLE', 'CHANNEL', 'KIND'];
|
|
267
|
+
const widths = [8, 35, 18, 12, 10, 12, 12];
|
|
268
|
+
|
|
269
|
+
// Header
|
|
270
|
+
console.log();
|
|
271
|
+
const headerLine = [
|
|
272
|
+
color('bright', truncate(headers[0], widths[0])),
|
|
273
|
+
color('bright', truncate(headers[1], widths[1])),
|
|
274
|
+
' ',
|
|
275
|
+
color('bright', truncate(headers[2], widths[2])),
|
|
276
|
+
color('bright', truncate(headers[3], widths[3])),
|
|
277
|
+
color('bright', truncate(headers[4], widths[4])),
|
|
278
|
+
color('bright', truncate(headers[5], widths[5])),
|
|
279
|
+
color('bright', truncate(headers[6], widths[6])),
|
|
280
|
+
].join('');
|
|
281
|
+
console.log(headerLine);
|
|
282
|
+
console.log(color('dim', '-'.repeat(widths.reduce((a, b) => a + b, 0) + 2)));
|
|
283
|
+
|
|
284
|
+
// Rows
|
|
285
|
+
sessions.forEach(s => {
|
|
286
|
+
const idle = now - (s.updatedAt || 0);
|
|
287
|
+
const idleStr = formatDuration(idle);
|
|
288
|
+
const agentName = getAgentName(s);
|
|
289
|
+
const model = getModelShort(s.model);
|
|
290
|
+
const currentTokens = s.totalTokens || 0;
|
|
291
|
+
const maxTokens = s.contextWindow || s.contextTokens || 0;
|
|
292
|
+
const context = `${formatBytes(currentTokens)}/${formatBytes(maxTokens)}`;
|
|
293
|
+
const channel = s.channel || '-';
|
|
294
|
+
const kind = s.kind || '-';
|
|
295
|
+
|
|
296
|
+
let statusStr;
|
|
297
|
+
if (idle < 5 * 60 * 1000) {
|
|
298
|
+
statusStr = color('green', 'active'.padEnd(8));
|
|
299
|
+
} else if (idle < 30 * 60 * 1000) {
|
|
300
|
+
statusStr = color('yellow', 'idle'.padEnd(8));
|
|
301
|
+
} else {
|
|
302
|
+
statusStr = color('red', 'stale'.padEnd(8));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const row = [
|
|
306
|
+
statusStr,
|
|
307
|
+
truncate(agentName, widths[1]),
|
|
308
|
+
' ',
|
|
309
|
+
truncate(model, widths[2]),
|
|
310
|
+
context.padEnd(widths[3]),
|
|
311
|
+
idleStr.padEnd(widths[4]),
|
|
312
|
+
truncate(channel, widths[5]),
|
|
313
|
+
truncate(kind, widths[6]),
|
|
314
|
+
].join('');
|
|
315
|
+
|
|
316
|
+
console.log(row);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
console.log(color('dim', '-'.repeat(widths.reduce((a, b) => a + b, 0))));
|
|
320
|
+
console.log(`${sessions.length} session${sessions.length !== 1 ? 's' : ''}`);
|
|
321
|
+
console.log();
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error(color('red', `Error: ${err.message}`));
|
|
325
|
+
if (err.message.includes('ECONNREFUSED')) {
|
|
326
|
+
console.error(color('dim', 'Is the OpenClaw gateway running?'));
|
|
327
|
+
}
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function main() {
|
|
333
|
+
if (options.watch) {
|
|
334
|
+
console.clear();
|
|
335
|
+
console.log(color('dim', `Watching every ${options.interval/1000}s (Ctrl+C to exit)...`));
|
|
336
|
+
console.log();
|
|
337
|
+
|
|
338
|
+
const run = async () => {
|
|
339
|
+
console.clear();
|
|
340
|
+
console.log(color('dim', `Watching every ${options.interval/1000}s (Ctrl+C to exit)...`));
|
|
341
|
+
await listSessions();
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
await run();
|
|
345
|
+
setInterval(run, options.interval);
|
|
346
|
+
} else {
|
|
347
|
+
await listSessions();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
main();
|
package/clawtop.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* clawtop - A top-like utility for monitoring OpenClaw instances
|
|
5
|
+
* Lightweight, cross-platform session monitor
|
|
6
|
+
*
|
|
7
|
+
* Usage: node clawtop.js [options]
|
|
8
|
+
* -n, --iterations Number of iterations (default: infinite)
|
|
9
|
+
* -d, --delay Delay in seconds between updates (default: 2)
|
|
10
|
+
* -s, --sort Sort by: cpu, mem, idle, tokens (default: cpu)
|
|
11
|
+
* -h, --help Show this help
|
|
12
|
+
*
|
|
13
|
+
* Keyboard shortcuts (when running):
|
|
14
|
+
* q Quit
|
|
15
|
+
* r Reverse sort order
|
|
16
|
+
* s Change sort field
|
|
17
|
+
* Space Pause/Resume updates
|
|
18
|
+
* h Show help
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import http from 'http';
|
|
22
|
+
import https from 'https';
|
|
23
|
+
import { execSync } from 'child_process';
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { fileURLToPath } from 'url';
|
|
27
|
+
import readline from 'readline';
|
|
28
|
+
import os from 'os';
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = path.dirname(__filename);
|
|
32
|
+
|
|
33
|
+
// Default config
|
|
34
|
+
const CONFIG = {
|
|
35
|
+
delay: 2,
|
|
36
|
+
iterations: Infinity,
|
|
37
|
+
sortBy: 'cpu',
|
|
38
|
+
reverse: false,
|
|
39
|
+
maxSessions: 20,
|
|
40
|
+
showSystem: true,
|
|
41
|
+
color: true
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// State
|
|
45
|
+
let paused = false;
|
|
46
|
+
let showingHelp = false;
|
|
47
|
+
let inputBuffer = '';
|
|
48
|
+
let awaitingInput = null; // 'delay' or 'iterations'
|
|
49
|
+
|
|
50
|
+
// Colors (disable on Windows or no-color)
|
|
51
|
+
const isWindows = process.platform === 'win32';
|
|
52
|
+
const noColor = process.env.NO_COLOR || isWindows;
|
|
53
|
+
|
|
54
|
+
const C = noColor ? {
|
|
55
|
+
reset: '', bright: '', dim: '', green: '', yellow: '', red: '', cyan: '', magenta: '', white: '', gray: ''
|
|
56
|
+
} : {
|
|
57
|
+
reset: '\x1b[0m',
|
|
58
|
+
bright: '\x1b[1m',
|
|
59
|
+
dim: '\x1b[2m',
|
|
60
|
+
green: '\x1b[32m',
|
|
61
|
+
yellow: '\x1b[33m',
|
|
62
|
+
red: '\x1b[31m',
|
|
63
|
+
cyan: '\x1b[36m',
|
|
64
|
+
magenta: '\x1b[35m',
|
|
65
|
+
white: '\x1b[37m',
|
|
66
|
+
gray: '\x1b[90m'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function getGatewayConfig() {
|
|
70
|
+
const configPaths = [
|
|
71
|
+
process.env.HOME + '/.openclaw/openclaw.json',
|
|
72
|
+
process.env.USERPROFILE + '/.openclaw/openclaw.json',
|
|
73
|
+
'/etc/openclaw/openclaw.json'
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
for (const configPath of configPaths) {
|
|
77
|
+
try {
|
|
78
|
+
if (fs.existsSync(configPath)) {
|
|
79
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
80
|
+
const config = JSON.parse(raw);
|
|
81
|
+
return {
|
|
82
|
+
port: config.gateway?.port || 18789,
|
|
83
|
+
token: config.gateway?.auth?.token,
|
|
84
|
+
host: config.gateway?.host || 'localhost'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
return { port: 18789, token: null, host: 'localhost' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatBytes(bytes) {
|
|
93
|
+
if (bytes === 0) return '0B';
|
|
94
|
+
const k = 1024;
|
|
95
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
96
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
97
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatDuration(seconds) {
|
|
101
|
+
if (!seconds || seconds < 0) return '--';
|
|
102
|
+
const days = Math.floor(seconds / 86400);
|
|
103
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
104
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
105
|
+
if (days > 0) return `${days}d ${hours}h`;
|
|
106
|
+
if (hours > 0) return `${hours}h ${mins}m`;
|
|
107
|
+
return `${mins}m`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatNumber(num) {
|
|
111
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
|
112
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
|
113
|
+
return num.toString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getSystemInfo() {
|
|
117
|
+
const info = {
|
|
118
|
+
os: 'Unknown',
|
|
119
|
+
arch: process.arch,
|
|
120
|
+
nodeVersion: process.version,
|
|
121
|
+
platform: process.platform,
|
|
122
|
+
cpuCount: os.cpus().length,
|
|
123
|
+
totalMem: os.totalmem(),
|
|
124
|
+
uptime: os.uptime(),
|
|
125
|
+
cpuUsage: 0,
|
|
126
|
+
freeMem: os.freemem(),
|
|
127
|
+
usedMem: os.totalmem() - os.freemem()
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Try to get OS info
|
|
131
|
+
try {
|
|
132
|
+
if (process.platform === 'darwin') {
|
|
133
|
+
const out = execSync('sw_vers -productVersion 2>/dev/null', { encoding: 'utf8', timeout: 2000 });
|
|
134
|
+
info.os = 'macOS ' + out.trim();
|
|
135
|
+
} else if (process.platform === 'linux') {
|
|
136
|
+
const out = execSync('cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d "\""', { encoding: 'utf8', timeout: 2000 });
|
|
137
|
+
info.os = out.trim() || 'Linux';
|
|
138
|
+
} else if (process.platform === 'win32') {
|
|
139
|
+
info.os = 'Windows';
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
info.os = process.platform;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return info;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Get current CPU usage by comparing idle times
|
|
149
|
+
let lastCpuInfo = null;
|
|
150
|
+
function getCpuUsage() {
|
|
151
|
+
const cpus = os.cpus();
|
|
152
|
+
let totalIdle = 0;
|
|
153
|
+
let totalTick = 0;
|
|
154
|
+
|
|
155
|
+
for (const cpu of cpus) {
|
|
156
|
+
for (const type in cpu.times) {
|
|
157
|
+
totalTick += cpu.times[type];
|
|
158
|
+
}
|
|
159
|
+
totalIdle += cpu.times.idle;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (lastCpuInfo) {
|
|
163
|
+
const idleDiff = totalIdle - lastCpuInfo.idle;
|
|
164
|
+
const totalDiff = totalTick - lastCpuInfo.total;
|
|
165
|
+
if (totalDiff > 0) {
|
|
166
|
+
const usage = 100 - (100 * idleDiff / totalDiff);
|
|
167
|
+
lastCpuInfo = { idle: totalIdle, total: totalTick };
|
|
168
|
+
return Math.round(usage);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lastCpuInfo = { idle: totalIdle, total: totalTick };
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getGatewayUptime() {
|
|
177
|
+
try {
|
|
178
|
+
if (process.platform === 'darwin') {
|
|
179
|
+
// Use ps to find openclaw-gateway process directly
|
|
180
|
+
const out = execSync('ps aux | grep "openclaw-gateway" | grep -v grep | head -1', { encoding: 'utf8', timeout: 2000 });
|
|
181
|
+
const match = out.trim().match(/^\S+\s+(\d+)/);
|
|
182
|
+
if (match) {
|
|
183
|
+
const pid = match[1];
|
|
184
|
+
const startOut = execSync(`ps -o lstart= -p ${pid} 2>/dev/null`, { encoding: 'utf8', timeout: 2000 });
|
|
185
|
+
const startTime = new Date(startOut.trim());
|
|
186
|
+
if (!isNaN(startTime.getTime())) {
|
|
187
|
+
return Math.floor((Date.now() - startTime.getTime()) / 1000);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else if (process.platform === 'linux') {
|
|
191
|
+
const out = execSync('pgrep -f openclaw-gateway 2>/dev/null | head -1', { encoding: 'utf8', timeout: 2000 });
|
|
192
|
+
const pid = parseInt(out.trim());
|
|
193
|
+
if (pid) {
|
|
194
|
+
const startOut = execSync(`ps -o lstart= -p ${pid} 2>/dev/null`, { encoding: 'utf8', timeout: 2000 });
|
|
195
|
+
const startTime = new Date(startOut.trim());
|
|
196
|
+
if (!isNaN(startTime.getTime())) {
|
|
197
|
+
return Math.floor((Date.now() - startTime.getTime()) / 1000);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else if (process.platform === 'win32') {
|
|
201
|
+
// Windows: use wmic to get creation date
|
|
202
|
+
const out = execSync('wmic process where "name=\'node.exe\' and commandline like \'%openclaw%\'" get ProcessId,CreationDate 2>/dev/null', { encoding: 'utf8', timeout: 3000 });
|
|
203
|
+
const lines = out.trim().split('\n').filter(l => l.trim());
|
|
204
|
+
if (lines.length > 1) {
|
|
205
|
+
// Parse the second line (first is headers)
|
|
206
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
207
|
+
if (parts.length >= 2) {
|
|
208
|
+
const pid = parseInt(parts[parts.length - 2]);
|
|
209
|
+
const createDate = parts[parts.length - 1];
|
|
210
|
+
// CreationDate format: 20260212170000.000000-000
|
|
211
|
+
const dateMatch = createDate.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
|
|
212
|
+
if (dateMatch) {
|
|
213
|
+
const startTime = new Date(
|
|
214
|
+
parseInt(dateMatch[1]), parseInt(dateMatch[2]) - 1, parseInt(dateMatch[3]),
|
|
215
|
+
parseInt(dateMatch[4]), parseInt(dateMatch[5]), parseInt(dateMatch[6])
|
|
216
|
+
);
|
|
217
|
+
if (!isNaN(startTime.getTime())) {
|
|
218
|
+
return Math.floor((Date.now() - startTime.getTime()) / 1000);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function fetchSessions(config) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const { port, token, host } = config;
|
|
231
|
+
const postData = JSON.stringify({ tool: 'sessions_list', args: { activeMinutes: 60, messageLimit: 1 } });
|
|
232
|
+
|
|
233
|
+
const headers = {
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
'Accept': 'application/json',
|
|
236
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (token) {
|
|
240
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const req = http.request({
|
|
244
|
+
hostname: host,
|
|
245
|
+
port,
|
|
246
|
+
path: '/tools/invoke',
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers,
|
|
249
|
+
timeout: 5000
|
|
250
|
+
}, (res) => {
|
|
251
|
+
let data = '';
|
|
252
|
+
res.on('data', chunk => data += chunk);
|
|
253
|
+
res.on('end', () => {
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(data);
|
|
256
|
+
if (parsed.ok && parsed.result) {
|
|
257
|
+
const result = parsed.result;
|
|
258
|
+
if (result.content && result.content[0]?.text) {
|
|
259
|
+
const innerResult = JSON.parse(result.content[0].text);
|
|
260
|
+
resolve(innerResult.sessions || []);
|
|
261
|
+
} else if (result.details?.sessions) {
|
|
262
|
+
resolve(result.details.sessions);
|
|
263
|
+
} else {
|
|
264
|
+
resolve([]);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
reject(new Error(parsed.error?.message || 'API error'));
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
reject(new Error('Invalid JSON: ' + e.message));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
req.on('error', (err) => reject(new Error('Request failed: ' + err.message)));
|
|
276
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
277
|
+
req.write(postData);
|
|
278
|
+
req.end();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Simple health check - just verifies gateway is reachable
|
|
283
|
+
async function checkGatewayHealth(config) {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
const { port, token, host } = config;
|
|
286
|
+
|
|
287
|
+
const req = http.request({
|
|
288
|
+
hostname: host,
|
|
289
|
+
port,
|
|
290
|
+
path: '/health',
|
|
291
|
+
method: 'GET',
|
|
292
|
+
timeout: 3000
|
|
293
|
+
}, (res) => {
|
|
294
|
+
resolve(res.statusCode === 200);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
req.on('error', () => resolve(false));
|
|
298
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
299
|
+
req.end();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function calculateCpuUsage(session, prevSession, elapsedMs) {
|
|
304
|
+
if (!session || !prevSession || elapsedMs < 500) return 0;
|
|
305
|
+
const currTokens = session.totalTokens || 0;
|
|
306
|
+
const prevTokens = prevSession.totalTokens || 0;
|
|
307
|
+
const diff = currTokens - prevTokens;
|
|
308
|
+
if (diff <= 0) return 0;
|
|
309
|
+
const tps = diff / (elapsedMs / 1000);
|
|
310
|
+
return Math.min(100, tps);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function sortSessions(sessions, sortBy, reverse) {
|
|
314
|
+
const sorted = [...sessions].sort((a, b) => {
|
|
315
|
+
let valA, valB;
|
|
316
|
+
|
|
317
|
+
switch (sortBy) {
|
|
318
|
+
case 'cpu':
|
|
319
|
+
valA = a._cpu || 0;
|
|
320
|
+
valB = b._cpu || 0;
|
|
321
|
+
break;
|
|
322
|
+
case 'mem':
|
|
323
|
+
case 'tokens':
|
|
324
|
+
valA = a.totalTokens || 0;
|
|
325
|
+
valB = b.totalTokens || 0;
|
|
326
|
+
break;
|
|
327
|
+
case 'idle':
|
|
328
|
+
valA = a.updatedAt || 0;
|
|
329
|
+
valB = b.updatedAt || 0;
|
|
330
|
+
break;
|
|
331
|
+
case 'name':
|
|
332
|
+
valA = (a.displayName || a.key || '').toLowerCase();
|
|
333
|
+
valB = (b.displayName || b.key || '').toLowerCase();
|
|
334
|
+
break;
|
|
335
|
+
default:
|
|
336
|
+
valA = a._cpu || 0;
|
|
337
|
+
valB = b._cpu || 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (typeof valA === 'string') {
|
|
341
|
+
return reverse ? valB.localeCompare(valA) : valA.localeCompare(valB);
|
|
342
|
+
}
|
|
343
|
+
return reverse ? valB - valA : valA - valB;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return sorted;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function clearScreen() {
|
|
350
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function moveToTop() {
|
|
354
|
+
process.stdout.write('\x1b[H');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function render(sysInfo, gwUptime, sessionCount, sessions, error, delay) {
|
|
358
|
+
const width = process.stdout.columns || 80;
|
|
359
|
+
|
|
360
|
+
// If showing help, render that instead
|
|
361
|
+
if (showingHelp) {
|
|
362
|
+
const helpLines = [
|
|
363
|
+
'',
|
|
364
|
+
' CLAWTOP - Keyboard Shortcuts',
|
|
365
|
+
'',
|
|
366
|
+
' q Quit',
|
|
367
|
+
' Space Pause/Resume updates',
|
|
368
|
+
' r Reverse sort order',
|
|
369
|
+
' s Cycle sort field (cpu → mem → idle → tokens → name)',
|
|
370
|
+
' d Change delay (prompts for seconds)',
|
|
371
|
+
' n Change iterations (prompts for number, 0 = infinite)',
|
|
372
|
+
' h Toggle this help',
|
|
373
|
+
'',
|
|
374
|
+
' Press any key to return...',
|
|
375
|
+
''
|
|
376
|
+
];
|
|
377
|
+
clearScreen();
|
|
378
|
+
console.log(C.cyan + C.bright + '┌─ CLAWTOP HELP ────────────────────────────────────────────────────────────┐' + C.reset);
|
|
379
|
+
helpLines.forEach(line => console.log(line));
|
|
380
|
+
console.log(C.cyan + '└───────────────────────────────────────────────────────────────────────────────┘' + C.reset);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// If awaiting input, show that
|
|
385
|
+
if (awaitingInput === 'delay') {
|
|
386
|
+
clearScreen();
|
|
387
|
+
console.log(C.cyan + '┌─ CLAWTOP ─────────────────────────────────────────────────────────────────────┐' + C.reset);
|
|
388
|
+
console.log(C.yellow + ' Enter delay in seconds (current: ' + CONFIG.delay + 's): ' + C.reset + inputBuffer);
|
|
389
|
+
console.log(C.gray + ' Press Enter to confirm, Esc to cancel' + C.reset);
|
|
390
|
+
console.log(C.cyan + '└───────────────────────────────────────────────────────────────────────────────┘' + C.reset);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (awaitingInput === 'iterations') {
|
|
395
|
+
clearScreen();
|
|
396
|
+
console.log(C.cyan + '┌─ CLAWTOP ─────────────────────────────────────────────────────────────────────┐' + C.reset);
|
|
397
|
+
const iterStr = CONFIG.iterations === Infinity ? 'infinite' : CONFIG.iterations;
|
|
398
|
+
console.log(C.yellow + ' Enter iterations (current: ' + iterStr + '): ' + C.reset + inputBuffer);
|
|
399
|
+
console.log(C.gray + ' Press Enter to confirm, Esc to cancel' + C.reset);
|
|
400
|
+
console.log(C.cyan + '└───────────────────────────────────────────────────────────────────────────────┘' + C.reset);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Normal render
|
|
405
|
+
clearScreen();
|
|
406
|
+
|
|
407
|
+
const statusLine = paused ? C.yellow + ' [PAUSED] ' + C.reset : '';
|
|
408
|
+
console.log(C.cyan + C.bright + '┌─ CLAWTOP ─────────────────────────────────────────────────────────────────────┐' + C.reset);
|
|
409
|
+
console.log(C.gray + ' OpenClaw session monitor' + statusLine + ' '.repeat(width - 45) + C.gray + `Refresh: ${delay}s` + C.reset);
|
|
410
|
+
console.log(C.cyan + '├──────────────────────────────────────────────────────────────────────────────┤' + C.reset);
|
|
411
|
+
|
|
412
|
+
if (CONFIG.showSystem) {
|
|
413
|
+
const cpuUsage = sysInfo.cpuUsage !== null ? sysInfo.cpuUsage : 0;
|
|
414
|
+
const memPercent = sysInfo.totalMem > 0 ? Math.round((sysInfo.usedMem / sysInfo.totalMem) * 100) : 0;
|
|
415
|
+
const sysLine = ` ${C.cyan}OS:${C.reset} ${sysInfo.os} ${C.cyan}CPU:${C.reset} ${cpuUsage}% ${C.cyan}Mem:${C.reset} ${formatBytes(sysInfo.usedMem)}/${formatBytes(sysInfo.totalMem)} (${memPercent}%) ${C.cyan}Uptime:${C.reset} ${formatDuration(sysInfo.uptime)}`;
|
|
416
|
+
console.log(sysLine.substring(0, width - 2));
|
|
417
|
+
|
|
418
|
+
const gwLine = ` ${C.magenta}Gateway:${C.reset} ${gwUptime ? formatDuration(gwUptime) : C.red + 'offline' + C.reset} ${C.magenta}Sessions:${C.reset} ${sessionCount} ${C.magenta}Node:${C.reset} ${sysInfo.nodeVersion}`;
|
|
419
|
+
console.log(gwLine.substring(0, width - 2));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log(C.cyan + '├──────────────────────────────────────────────────────────────────────────────┤' + C.reset);
|
|
423
|
+
|
|
424
|
+
const sortIndicator = (field) => CONFIG.sortBy === field ? (CONFIG.reverse ? '▼' : '▲') : ' ';
|
|
425
|
+
console.log(C.white + C.bright +
|
|
426
|
+
` ${sortIndicator('name')} NAME ${sortIndicator('cpu')} CPU ${sortIndicator('mem')} TOKENS ${sortIndicator('idle')} IDLE CHANNEL` +
|
|
427
|
+
C.reset);
|
|
428
|
+
console.log(C.cyan + '├──────────────────────────────────────────────────────────────────────────────┤' + C.reset);
|
|
429
|
+
|
|
430
|
+
if (error) {
|
|
431
|
+
console.log(C.red + ' Error: ' + error + C.reset);
|
|
432
|
+
console.log(C.gray + ` Gateway may be offline. Config: ${config.host}:${config.port}` + C.reset);
|
|
433
|
+
} else if (sessions.length === 0) {
|
|
434
|
+
console.log(C.gray + ' No active sessions' + C.reset);
|
|
435
|
+
} else {
|
|
436
|
+
sessions.forEach((s) => {
|
|
437
|
+
const name = (s.displayName || s.key || 'unknown').substring(0, 27).padEnd(27);
|
|
438
|
+
const cpu = s._cpu || 0;
|
|
439
|
+
const tokens = s.totalTokens || 0;
|
|
440
|
+
const idleMs = s.updatedAt ? Date.now() - s.updatedAt : 0;
|
|
441
|
+
let idleStr;
|
|
442
|
+
if (idleMs < 60000) idleStr = Math.round(idleMs / 1000) + 's';
|
|
443
|
+
else if (idleMs < 3600000) idleStr = Math.round(idleMs / 60000) + 'm';
|
|
444
|
+
else idleStr = Math.round(idleMs / 3600000) + 'h';
|
|
445
|
+
|
|
446
|
+
const channel = (s.channel || '-').substring(0, 10);
|
|
447
|
+
|
|
448
|
+
let nameColor = C.white;
|
|
449
|
+
let cpuColor = C.gray;
|
|
450
|
+
|
|
451
|
+
if (idleMs < 5 * 60 * 1000) {
|
|
452
|
+
nameColor = C.green;
|
|
453
|
+
cpuColor = cpu > 50 ? C.red : (cpu > 20 ? C.yellow : C.green);
|
|
454
|
+
} else if (idleMs < 30 * 60 * 1000) {
|
|
455
|
+
nameColor = C.yellow;
|
|
456
|
+
} else {
|
|
457
|
+
nameColor = C.gray;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const cpuStr = cpu > 0 ? cpu.toFixed(1) + '%' : '-';
|
|
461
|
+
const tokensStr = tokens > 0 ? formatNumber(tokens) : '-';
|
|
462
|
+
|
|
463
|
+
console.log(` ${nameColor}${name}${C.reset} ${cpuColor}${cpuStr.padStart(6)}${C.reset} ${tokensStr.padStart(9)} ${idleStr.padStart(7)} ${channel}`);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
console.log(C.cyan + '├──────────────────────────────────────────────────────────────────────────────┤' + C.reset);
|
|
468
|
+
console.log(C.gray + ` Sort: ${CONFIG.sortBy} (s) Reverse: ${CONFIG.reverse ? 'ON' : 'OFF'} (r) Delay: ${CONFIG.delay}s (d) Quit: q Help: h` + C.reset);
|
|
469
|
+
console.log(C.cyan + '└───────────────────────────────────────────────────────────────────────────────┘' + C.reset);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function main() {
|
|
473
|
+
const args = process.argv.slice(2);
|
|
474
|
+
for (let i = 0; i < args.length; i++) {
|
|
475
|
+
const arg = args[i];
|
|
476
|
+
if (arg === '-h' || arg === '--help') {
|
|
477
|
+
console.log(`
|
|
478
|
+
clawtop - A top-like utility for monitoring OpenClaw instances
|
|
479
|
+
|
|
480
|
+
Usage: node clawtop.js [options]
|
|
481
|
+
|
|
482
|
+
Options:
|
|
483
|
+
-n, --iterations N Number of iterations (default: infinite)
|
|
484
|
+
-d, --delay N Delay in seconds between updates (default: 2)
|
|
485
|
+
-s, --sort FIELD Sort by: cpu, mem, idle, tokens, name (default: cpu)
|
|
486
|
+
--no-color Disable colored output
|
|
487
|
+
--no-system Hide system info
|
|
488
|
+
-h, --help Show this help
|
|
489
|
+
|
|
490
|
+
Keyboard shortcuts (when running):
|
|
491
|
+
Space Pause/Resume updates
|
|
492
|
+
q Quit
|
|
493
|
+
r Reverse sort order
|
|
494
|
+
s Cycle sort field
|
|
495
|
+
d Change delay (prompts)
|
|
496
|
+
n Change iterations (prompts)
|
|
497
|
+
h Toggle help
|
|
498
|
+
|
|
499
|
+
Examples:
|
|
500
|
+
node clawtop.js
|
|
501
|
+
node clawtop.js -n 5
|
|
502
|
+
node clawtop.js -d 5 -s idle
|
|
503
|
+
`);
|
|
504
|
+
process.exit(0);
|
|
505
|
+
} else if (arg === '-n' || arg === '--iterations') {
|
|
506
|
+
CONFIG.iterations = parseInt(args[++i]) || Infinity;
|
|
507
|
+
} else if (arg === '-d' || arg === '--delay') {
|
|
508
|
+
CONFIG.delay = parseInt(args[++i]) || 2;
|
|
509
|
+
} else if (arg === '-s' || arg === '--sort') {
|
|
510
|
+
CONFIG.sortBy = args[++i] || 'cpu';
|
|
511
|
+
} else if (arg === '--no-color') {
|
|
512
|
+
Object.keys(C).forEach(k => C[k] = '');
|
|
513
|
+
} else if (arg === '--no-system') {
|
|
514
|
+
CONFIG.showSystem = false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const config = getGatewayConfig();
|
|
519
|
+
let iterations = 0;
|
|
520
|
+
let prevSessions = [];
|
|
521
|
+
let lastTime = Date.now();
|
|
522
|
+
let lastRenderTime = Date.now();
|
|
523
|
+
|
|
524
|
+
// Setup input handling
|
|
525
|
+
if (process.stdin.isTTY) {
|
|
526
|
+
readline.emitKeypressEvents(process.stdin);
|
|
527
|
+
process.stdin.setRawMode(true);
|
|
528
|
+
|
|
529
|
+
process.stdin.on('keypress', (str, key) => {
|
|
530
|
+
// Handle help toggle
|
|
531
|
+
if (key.name === 'h' && !awaitingInput) {
|
|
532
|
+
showingHelp = !showingHelp;
|
|
533
|
+
lastRenderTime = Date.now(); // Force render
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// If showing help, any key closes it
|
|
538
|
+
if (showingHelp) {
|
|
539
|
+
showingHelp = false;
|
|
540
|
+
lastRenderTime = Date.now();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Handle input modes
|
|
545
|
+
if (awaitingInput) {
|
|
546
|
+
if (key.name === 'escape') {
|
|
547
|
+
awaitingInput = null;
|
|
548
|
+
inputBuffer = '';
|
|
549
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
550
|
+
if (awaitingInput === 'delay') {
|
|
551
|
+
const newDelay = parseInt(inputBuffer);
|
|
552
|
+
if (newDelay > 0 && newDelay <= 3600) {
|
|
553
|
+
CONFIG.delay = newDelay;
|
|
554
|
+
}
|
|
555
|
+
} else if (awaitingInput === 'iterations') {
|
|
556
|
+
const newIter = parseInt(inputBuffer);
|
|
557
|
+
if (!isNaN(newIter)) {
|
|
558
|
+
CONFIG.iterations = newIter === 0 ? Infinity : newIter;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
awaitingInput = null;
|
|
562
|
+
inputBuffer = '';
|
|
563
|
+
} else if (key.name === 'backspace') {
|
|
564
|
+
inputBuffer = inputBuffer.slice(0, -1);
|
|
565
|
+
} else if (str && str.length === 1 && /[\d]/.test(str)) {
|
|
566
|
+
inputBuffer += str;
|
|
567
|
+
}
|
|
568
|
+
lastRenderTime = Date.now();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Regular keyboard shortcuts
|
|
573
|
+
if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
574
|
+
console.log('\n' + C.gray + 'Goodbye!' + C.reset);
|
|
575
|
+
process.exit(0);
|
|
576
|
+
} else if (key.name === 'space') {
|
|
577
|
+
paused = !paused;
|
|
578
|
+
} else if (key.name === 'r') {
|
|
579
|
+
CONFIG.reverse = !CONFIG.reverse;
|
|
580
|
+
} else if (key.name === 's') {
|
|
581
|
+
const fields = ['cpu', 'mem', 'idle', 'tokens', 'name'];
|
|
582
|
+
const idx = fields.indexOf(CONFIG.sortBy);
|
|
583
|
+
CONFIG.sortBy = fields[(idx + 1) % fields.length];
|
|
584
|
+
} else if (key.name === 'd') {
|
|
585
|
+
awaitingInput = 'delay';
|
|
586
|
+
inputBuffer = '';
|
|
587
|
+
} else if (key.name === 'n') {
|
|
588
|
+
awaitingInput = 'iterations';
|
|
589
|
+
inputBuffer = '';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
lastRenderTime = Date.now();
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Main loop
|
|
597
|
+
while (iterations < CONFIG.iterations) {
|
|
598
|
+
// Skip iteration if paused, but still render to show paused state
|
|
599
|
+
if (!paused && !showingHelp && !awaitingInput) {
|
|
600
|
+
const now = Date.now();
|
|
601
|
+
const elapsed = now - lastTime;
|
|
602
|
+
lastTime = now;
|
|
603
|
+
|
|
604
|
+
const sysInfo = getSystemInfo();
|
|
605
|
+
sysInfo.cpuUsage = getCpuUsage();
|
|
606
|
+
const gwUptime = getGatewayUptime();
|
|
607
|
+
|
|
608
|
+
let sessions = [];
|
|
609
|
+
let error = null;
|
|
610
|
+
try {
|
|
611
|
+
sessions = await fetchSessions(config);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
error = err.message;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
sessions = sessions.map(s => {
|
|
617
|
+
const prev = prevSessions.find(ps => ps.key === s.key);
|
|
618
|
+
s._cpu = calculateCpuUsage(s, prev, elapsed);
|
|
619
|
+
return s;
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
sessions = sortSessions(sessions, CONFIG.sortBy, CONFIG.reverse).slice(0, CONFIG.maxSessions);
|
|
623
|
+
prevSessions = sessions;
|
|
624
|
+
|
|
625
|
+
render(sysInfo, gwUptime, sessions.length, sessions, error, CONFIG.delay);
|
|
626
|
+
iterations++;
|
|
627
|
+
} else if (paused || showingHelp || awaitingInput) {
|
|
628
|
+
// Still render to show state
|
|
629
|
+
const sysInfo = getSystemInfo();
|
|
630
|
+
sysInfo.cpuUsage = getCpuUsage();
|
|
631
|
+
const gwUptime = getGatewayUptime();
|
|
632
|
+
render(sysInfo, gwUptime, prevSessions.length, prevSessions, null, CONFIG.delay);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Calculate sleep time - use shorter interval when paused/input
|
|
636
|
+
const sleepTime = (paused || showingHelp || awaitingInput) ? 500 : CONFIG.delay * 1000;
|
|
637
|
+
await new Promise(resolve => setTimeout(resolve, sleepTime));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
console.log(C.gray + '\nCompleted ' + CONFIG.iterations + ' iterations.' + C.reset);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
main().catch(err => {
|
|
644
|
+
console.error(C.red + 'Fatal error: ' + err.message + C.reset);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawps",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenClaw session utilities, in the style of the procps package",
|
|
5
|
+
"main": "clawps.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clawps": "./clawps.js",
|
|
8
|
+
"clawtop": "./clawtop.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"install": "node install.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["openclaw", "cli", "sessions", "procps", "utilities"],
|
|
14
|
+
"author": "Kevin Smith",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|