clawmonitor 1.0.1 → 1.1.2
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 +10 -40
- package/README.zh-CN.md +22 -51
- package/README.zh-TW.md +21 -50
- package/bin/clawmonitor.js +733 -0
- package/package.json +4 -6
- package/bin/clawmonitor.sh +0 -466
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// clawmonitor — Real-time OpenClaw tool call monitor
|
|
3
|
+
// Zero dependencies. Node.js >= 20.
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
// === Args ===
|
|
10
|
+
const argv = process.argv.slice(2);
|
|
11
|
+
const opts = { compact: argv.includes('--compact'), full: argv.includes('--full'), history: 10 };
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
if (argv[i] === '--history') { opts.history = parseInt(argv[i + 1]) || 10; i++; }
|
|
14
|
+
else if (argv[i] === '--help' || argv[i] === '-h') {
|
|
15
|
+
console.log(`clawmonitor — Real-time OpenClaw tool call monitor
|
|
16
|
+
|
|
17
|
+
Usage: clawmonitor [options]
|
|
18
|
+
|
|
19
|
+
--compact Compact one-line output
|
|
20
|
+
--history N Show last N history entries (default: 10)
|
|
21
|
+
--full Show full input/output (no truncation)
|
|
22
|
+
--help Show this help
|
|
23
|
+
|
|
24
|
+
Environment:
|
|
25
|
+
OPENCLAW_HOME Custom OpenClaw data directory
|
|
26
|
+
NO_COLOR Disable colored output`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// === Theme ===
|
|
32
|
+
const NC = process.env.NO_COLOR || !process.stdout.isTTY;
|
|
33
|
+
const T = NC ? {
|
|
34
|
+
s: (v) => v,
|
|
35
|
+
r: (s) => s, g: (s) => s, y: (s) => s, b: (s) => s,
|
|
36
|
+
m: (s) => s, c: (s) => s, w: (s) => s, x: (s) => s,
|
|
37
|
+
d: (s) => s, bd: (s) => s,
|
|
38
|
+
ok: (s) => s, err: (s) => s, tag: (s) => s,
|
|
39
|
+
badge: (name) => ` ${name} `,
|
|
40
|
+
} : {
|
|
41
|
+
s: (v) => v,
|
|
42
|
+
// Foregrounds (call as functions)
|
|
43
|
+
r: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
44
|
+
g: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
45
|
+
y: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
46
|
+
b: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
47
|
+
m: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
48
|
+
c: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
49
|
+
w: (s) => `\x1b[37m${s}\x1b[0m`,
|
|
50
|
+
x: (s) => `\x1b[90m${s}\x1b[0m`, // gray
|
|
51
|
+
d: (s) => `\x1b[2m${s}\x1b[0m`, // dim
|
|
52
|
+
bd: (s) => `\x1b[1m${s}\x1b[0m`, // bold
|
|
53
|
+
// Badges
|
|
54
|
+
// Badges — no emoji, pure ASCII for predictable width
|
|
55
|
+
ok: NC ? (s) => s : (s) => `\x1b[42m\x1b[30m ${s} \x1b[0m`,
|
|
56
|
+
err: NC ? (s) => s : (s) => `\x1b[41m\x1b[97m ${s} \x1b[0m`,
|
|
57
|
+
tag: NC ? (s) => `<${s}>` : (s) => `\x1b[48;5;237m\x1b[37m<${s}>\x1b[0m`,
|
|
58
|
+
badge: NC
|
|
59
|
+
? (name) => ` ${name} `
|
|
60
|
+
: (name) => {
|
|
61
|
+
const map = {
|
|
62
|
+
exec: '\x1b[48;5;22m', read: '\x1b[48;5;24m', write: '\x1b[48;5;52m',
|
|
63
|
+
edit: '\x1b[48;5;58m', web_search: '\x1b[48;5;28m', web_fetch: '\x1b[48;5;25m',
|
|
64
|
+
browser: '\x1b[48;5;23m', message: '\x1b[48;5;55m', tts: '\x1b[48;5;54m',
|
|
65
|
+
memory_search: '\x1b[48;5;26m', gateway: '\x1b[48;5;60m',
|
|
66
|
+
};
|
|
67
|
+
return `${map[name] || '\x1b[48;5;237m'}\x1b[37m ${name} \x1b[0m`;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// === OpenClaw dir ===
|
|
72
|
+
function findDir() {
|
|
73
|
+
for (const d of [
|
|
74
|
+
process.env.OPENCLAW_HOME && path.join(process.env.OPENCLAW_HOME, 'agents'),
|
|
75
|
+
path.join(os.homedir(), '.openclaw', 'agents'),
|
|
76
|
+
process.env.XDG_DATA_HOME && path.join(process.env.XDG_DATA_HOME, 'openclaw', 'agents'),
|
|
77
|
+
process.platform === 'win32' && process.env.APPDATA && path.join(process.env.APPDATA, 'openclaw', 'agents'),
|
|
78
|
+
].filter(Boolean)) {
|
|
79
|
+
if (fs.existsSync(d)) return d;
|
|
80
|
+
}
|
|
81
|
+
console.error(`Cannot find OpenClaw data directory. Set OPENCLAW_HOME.`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DIR = findDir();
|
|
86
|
+
const TIME = 0;
|
|
87
|
+
|
|
88
|
+
// === Sessions ===
|
|
89
|
+
function findSessions() {
|
|
90
|
+
const now = Date.now(), out = [];
|
|
91
|
+
if (!fs.existsSync(DIR)) return out;
|
|
92
|
+
for (const ag of fs.readdirSync(DIR)) {
|
|
93
|
+
const sd = path.join(DIR, ag, 'sessions');
|
|
94
|
+
if (!fs.existsSync(sd)) continue;
|
|
95
|
+
for (const f of fs.readdirSync(sd)) {
|
|
96
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
97
|
+
const fp = path.join(sd, f);
|
|
98
|
+
try {
|
|
99
|
+
const st = fs.statSync(fp);
|
|
100
|
+
if (TIME > 0 && (now - st.mtimeMs) > TIME * 6e4) continue;
|
|
101
|
+
out.push(fp);
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out.sort();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getLabel(fp) {
|
|
109
|
+
const ag = path.relative(DIR, fp).split(path.sep)[0];
|
|
110
|
+
const bn = path.basename(fp, '.jsonl');
|
|
111
|
+
try {
|
|
112
|
+
for (const ln of fs.readFileSync(fp, 'utf8').split('\n').slice(0, 80)) {
|
|
113
|
+
if (!ln.trim()) continue;
|
|
114
|
+
try {
|
|
115
|
+
const o = JSON.parse(ln);
|
|
116
|
+
if (o.type === 'message' && o.message?.role === 'user') {
|
|
117
|
+
for (const t of (o.message.content?.filter(c => c.type === 'text') || [])) {
|
|
118
|
+
const m = t.text?.match(/"conversation_label"\s*:\s*"([^"]+)"/);
|
|
119
|
+
if (m) {
|
|
120
|
+
const l = m[1], tp = l.match(/topic:(\d+)/)?.[1], gn = l.split(' ')[0];
|
|
121
|
+
if (tp) return `${ag}/${gn}/t${tp}`;
|
|
122
|
+
if (gn) return `${ag}/${gn}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
return `${ag}/DM/${bn.replace(/-topic-\d+$/, '').replace(/-([0-9a-f]{4}).*/, '..*$1')}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// === Extract & Pair ===
|
|
133
|
+
function extract(fp) {
|
|
134
|
+
const out = [], fn = path.basename(fp);
|
|
135
|
+
let data;
|
|
136
|
+
try { data = fs.readFileSync(fp, 'utf8'); } catch { return out; }
|
|
137
|
+
for (const ln of data.split('\n')) {
|
|
138
|
+
if (!ln.trim()) continue;
|
|
139
|
+
let o; try { o = JSON.parse(ln); } catch { continue; }
|
|
140
|
+
if (o.type !== 'message' || !o.timestamp) continue;
|
|
141
|
+
if (o.message.role === 'assistant') {
|
|
142
|
+
for (const tc of (o.message.content || []).filter(c => c.type === 'toolCall'))
|
|
143
|
+
out.push({ ts: o.timestamp, file: fn, role: 'call', id: tc.id, name: tc.name, args: tc.arguments || {} });
|
|
144
|
+
} else if (o.message.role === 'toolResult') {
|
|
145
|
+
const t = o.message.content?.[0]?.text || '';
|
|
146
|
+
out.push({ ts: o.timestamp, file: fn, role: 'result', id: o.message.toolCallId, name: o.message.toolName, result: typeof t === 'string' ? t : JSON.stringify(t) });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function pair(entries) {
|
|
153
|
+
entries.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
154
|
+
const p = {}, out = [];
|
|
155
|
+
for (const e of entries) {
|
|
156
|
+
if (e.role === 'call') p[e.id] = e;
|
|
157
|
+
else if (e.role === 'result' && p[e.id]) {
|
|
158
|
+
out.push({ ...p[e.id], result: e.result, rts: e.ts });
|
|
159
|
+
delete p[e.id];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const k of Object.keys(p)) out.push(p[k]);
|
|
163
|
+
out.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// === Helpers ===
|
|
168
|
+
const fmtT = (ts) => {
|
|
169
|
+
if (!ts || ts === 'null') return '??:??:??';
|
|
170
|
+
const d = new Date(ts);
|
|
171
|
+
return isNaN(d) ? ts.substring(11, 19) : d.toLocaleTimeString('en-GB', { hour12: false });
|
|
172
|
+
};
|
|
173
|
+
const fmtDur = (a, b) => {
|
|
174
|
+
const ms = new Date(b) - new Date(a);
|
|
175
|
+
if (isNaN(ms)) return null;
|
|
176
|
+
if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m${Math.floor((ms % 6e4) / 1e3)}s`;
|
|
177
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(1)}s`;
|
|
178
|
+
return `${ms}ms`;
|
|
179
|
+
};
|
|
180
|
+
const fmtSz = (b) => b >= 1048576 ? `${(b / 1048576).toFixed(1)}MB` : b >= 1024 ? `${Math.floor(b / 1024)}KB` : `${b}B`;
|
|
181
|
+
const trunc = (v, n) => {
|
|
182
|
+
if (v == null) return '';
|
|
183
|
+
let s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
184
|
+
const lines = s.split('\n');
|
|
185
|
+
if (lines.length > 1) {
|
|
186
|
+
const shown = lines.slice(0, 3).map(l => l.trim()).filter(l => l);
|
|
187
|
+
if (lines.length > 3) return truncTo(shown.join(' '), n - 15) + ` … (+${lines.length - 3} lines)`;
|
|
188
|
+
return truncTo(shown.join(' '), n);
|
|
189
|
+
}
|
|
190
|
+
s = s.replace(/\s+/g, ' ').trim();
|
|
191
|
+
return truncTo(s, n);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const durColor = (d) => {
|
|
195
|
+
if (!d) return '';
|
|
196
|
+
const ms = d.endsWith('ms') ? +d.slice(0, -2) : d.includes('m') ? (+d.match(/(\d+)m/)[1] * 6e4 + (+d.match(/(\d+)s/)?.[1] || 0) * 1e3) : +d.slice(0, -1) * 1e3;
|
|
197
|
+
if (ms < 100) return T.g(d);
|
|
198
|
+
if (ms < 1000) return T.c(d);
|
|
199
|
+
if (ms < 5000) return T.y(d);
|
|
200
|
+
return T.r(d);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// === Format Entry ===
|
|
204
|
+
// Wrap a multi-line arg value into display lines that fit the border.
|
|
205
|
+
// Returns array of {key, value} lines. Max 3 lines per arg.
|
|
206
|
+
// Pad a string to exactly N terminal columns (right-pad with spaces)
|
|
207
|
+
const padTo = (s, n) => {
|
|
208
|
+
const w = strWidth(s);
|
|
209
|
+
if (w >= n) return s;
|
|
210
|
+
return s + ' '.repeat(n - w);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
214
|
+
|
|
215
|
+
// === Width measurement (based on string-width by sindresorhus) ===
|
|
216
|
+
// Uses Intl.Segmenter (Node 16+) for grapheme clustering
|
|
217
|
+
// + Unicode East Asian Width for CJK
|
|
218
|
+
// + RGI Emoji detection for emoji width
|
|
219
|
+
// Zero dependencies — all built-in JS APIs
|
|
220
|
+
|
|
221
|
+
const segmenter = new Intl.Segmenter();
|
|
222
|
+
const reZeroWidth = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+$/v;
|
|
223
|
+
const reEmoji = /^\p{RGI_Emoji}$/v;
|
|
224
|
+
const reExtPicto = /\p{Extended_Pictographic}/gv;
|
|
225
|
+
|
|
226
|
+
const charWidth = (segment) => {
|
|
227
|
+
const cp = segment.codePointAt(0);
|
|
228
|
+
// Zero-width clusters
|
|
229
|
+
if (reZeroWidth.test(segment)) return 0;
|
|
230
|
+
// RGI Emoji → 2 cols
|
|
231
|
+
if (reEmoji.test(segment)) return 2;
|
|
232
|
+
// Keycap sequences
|
|
233
|
+
if (/^[\d#*]\u20E3$/.test(segment)) return 2;
|
|
234
|
+
// ZWJ sequences with 2+ Extended_Pictographic
|
|
235
|
+
if (segment.includes('\u200D') && segment.length <= 50) {
|
|
236
|
+
const p = segment.match(reExtPicto);
|
|
237
|
+
if (p && p.length >= 2) return 2;
|
|
238
|
+
}
|
|
239
|
+
// East Asian Width: Wide/Fullwidth ranges → 2 cols
|
|
240
|
+
if ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3400 && cp <= 0x4DBF) ||
|
|
241
|
+
(cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0xFF01 && cp <= 0xFFEF) ||
|
|
242
|
+
(cp >= 0x3000 && cp <= 0x303F) || (cp >= 0xFE30 && cp <= 0xFE6F) ||
|
|
243
|
+
(cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0x3040 && cp <= 0x30FF) ||
|
|
244
|
+
(cp >= 0x3100 && cp <= 0x312F) || (cp >= 0x3130 && cp <= 0x318F) ||
|
|
245
|
+
(cp >= 0x2E80 && cp <= 0x2EFF) || (cp >= 0x2F00 && cp <= 0x2FDF) ||
|
|
246
|
+
(cp >= 0x20000 && cp <= 0x3FFFF)) return 2;
|
|
247
|
+
return 1;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const strWidth = (s) => {
|
|
251
|
+
const plain = stripAnsi(s);
|
|
252
|
+
if (!plain) return 0;
|
|
253
|
+
// Fast path: pure ASCII
|
|
254
|
+
if (/^[\x20-\x7E]*$/.test(plain)) return plain.length;
|
|
255
|
+
let w = 0;
|
|
256
|
+
for (const {segment} of segmenter.segment(plain)) w += charWidth(segment);
|
|
257
|
+
return w;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// truncTo returns the truncated string. Also sets truncTo.consumed for caller.
|
|
261
|
+
let _truncConsumed = 0;
|
|
262
|
+
const truncTo = (s, maxCols) => {
|
|
263
|
+
_truncConsumed = 0;
|
|
264
|
+
if (!s) return s;
|
|
265
|
+
// Fast path: pure ASCII
|
|
266
|
+
if (/^[\x20-\x7E]*$/.test(s)) {
|
|
267
|
+
if (s.length <= maxCols) { _truncConsumed = s.length; return s; }
|
|
268
|
+
_truncConsumed = maxCols;
|
|
269
|
+
return s.slice(0, maxCols) + '\u2026';
|
|
270
|
+
}
|
|
271
|
+
let w = 0, chars = [];
|
|
272
|
+
for (const {segment} of segmenter.segment(s)) {
|
|
273
|
+
const cw = charWidth(segment);
|
|
274
|
+
if (cw === 0) { _truncConsumed += segment.length; continue; }
|
|
275
|
+
if (w + cw > maxCols) break;
|
|
276
|
+
w += cw;
|
|
277
|
+
_truncConsumed += segment.length;
|
|
278
|
+
chars.push(segment);
|
|
279
|
+
}
|
|
280
|
+
const result = chars.join('');
|
|
281
|
+
if (_truncConsumed < s.length) {
|
|
282
|
+
return result + '\u2026';
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Visible length of a string (ignoring ANSI codes)
|
|
288
|
+
function wrapArg(key, val, maxCols, keyW) {
|
|
289
|
+
const pad = keyW + 2; // key + space after key
|
|
290
|
+
const maxValW = Math.max(20, maxCols - pad - 3); // 3 for "│ "
|
|
291
|
+
const indent = ' '.repeat(pad + 2); // "│ " + key padding
|
|
292
|
+
|
|
293
|
+
let s = val == null ? '' : (typeof val === 'string' ? val : JSON.stringify(val));
|
|
294
|
+
// Flatten newlines to spaces
|
|
295
|
+
s = s.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
296
|
+
|
|
297
|
+
const isJson = s.startsWith('{') || s.startsWith('[');
|
|
298
|
+
|
|
299
|
+
if (isJson) {
|
|
300
|
+
// Tokenize → wrap at token boundaries → color
|
|
301
|
+
const tokens = highlightJson(s);
|
|
302
|
+
const tokenLines = wrapTokens(tokens, maxValW, opts.full ? 999 : 3);
|
|
303
|
+
const out = [];
|
|
304
|
+
for (let i = 0; i < tokenLines.length; i++) {
|
|
305
|
+
out.push({ key, indent: i === 0 ? null : indent, val: renderTokenLines([tokenLines[i]])[0] });
|
|
306
|
+
}
|
|
307
|
+
if (out.length === 0) out.push({ key, indent: null, val: '' });
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Non-JSON: plain text wrapping
|
|
312
|
+
const out = [];
|
|
313
|
+
let text = s;
|
|
314
|
+
let lineIdx = 0;
|
|
315
|
+
while (text.length > 0 && lineIdx < (opts.full ? 999 : 3)) {
|
|
316
|
+
const maxL = opts.full ? 999 : 3;
|
|
317
|
+
const isLast = lineIdx === maxL - 1;
|
|
318
|
+
const budget = isLast ? maxValW - 1 : maxValW;
|
|
319
|
+
const chunk = truncTo(text, budget);
|
|
320
|
+
out.push({ key, indent: lineIdx === 0 ? null : indent, val: chunk });
|
|
321
|
+
text = text.slice(_truncConsumed);
|
|
322
|
+
lineIdx++;
|
|
323
|
+
if (!text) break;
|
|
324
|
+
if (isLast && text) {
|
|
325
|
+
out[out.length - 1].val = truncTo(chunk, budget - 1) + '\u2026';
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (out.length === 0) out.push({ key, indent: null, val: '' });
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// JSON syntax highlighting
|
|
334
|
+
function highlightJson(s) {
|
|
335
|
+
// Tokenize JSON, return array of {type, text}
|
|
336
|
+
const tokens = [];
|
|
337
|
+
let i = 0;
|
|
338
|
+
while (i < s.length) {
|
|
339
|
+
const ch = s[i];
|
|
340
|
+
if (ch === '"') {
|
|
341
|
+
let j = i + 1;
|
|
342
|
+
while (j < s.length && s[j] !== '"') { if (s[j] === '\\') j++; j++; }
|
|
343
|
+
const str = s.slice(i, j < s.length ? j + 1 : j);
|
|
344
|
+
// Key if followed by :
|
|
345
|
+
let k = j + 1; while (k < s.length && s[k] === ' ') k++;
|
|
346
|
+
tokens.push({ type: s[k] === ':' ? 'key' : 'str', text: str });
|
|
347
|
+
i = j + 1;
|
|
348
|
+
} else if (ch === ':' || ch === ',' || ch === '{' || ch === '}' || ch === '[' || ch === ']') {
|
|
349
|
+
tokens.push({ type: 'punct', text: ch });
|
|
350
|
+
i++;
|
|
351
|
+
} else if (ch === ' ' || ch === '\t') {
|
|
352
|
+
let j = i; while (j < s.length && (s[j] === ' ' || s[j] === '\t')) j++;
|
|
353
|
+
tokens.push({ type: 'ws', text: s.slice(i, j) });
|
|
354
|
+
i = j;
|
|
355
|
+
} else if (ch === 't' && s.slice(i, i + 4) === 'true') {
|
|
356
|
+
tokens.push({ type: 'bool', text: 'true' }); i += 4;
|
|
357
|
+
} else if (ch === 'f' && s.slice(i, i + 5) === 'false') {
|
|
358
|
+
tokens.push({ type: 'bool', text: 'false' }); i += 5;
|
|
359
|
+
} else if (ch === 'n' && s.slice(i, i + 4) === 'null') {
|
|
360
|
+
tokens.push({ type: 'null', text: 'null' }); i += 4;
|
|
361
|
+
} else if (ch >= '0' && ch <= '9' || ch === '-') {
|
|
362
|
+
let j = i; while (j < s.length && /[0-9.eE+\-]/.test(s[j])) j++;
|
|
363
|
+
tokens.push({ type: 'num', text: s.slice(i, j) }); i = j;
|
|
364
|
+
} else {
|
|
365
|
+
tokens.push({ type: 'plain', text: ch }); i++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return tokens;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Apply ANSI color to a token
|
|
372
|
+
const colorToken = (t) => {
|
|
373
|
+
const colors = {
|
|
374
|
+
key: '\x1b[36m', // cyan
|
|
375
|
+
str: '\x1b[32m', // green
|
|
376
|
+
num: '\x1b[33m', // yellow
|
|
377
|
+
bool: '\x1b[33m', // yellow
|
|
378
|
+
null: '\x1b[90m', // dim
|
|
379
|
+
punct: '', // no color
|
|
380
|
+
ws: '', // no color
|
|
381
|
+
plain: '', // no color
|
|
382
|
+
};
|
|
383
|
+
const c = colors[t.type] || '';
|
|
384
|
+
return c ? `${c}${t.text}\x1b[0m` : t.text;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Wrap tokens into lines, respecting token boundaries
|
|
388
|
+
function wrapTokens(tokens, maxCols, maxLines) {
|
|
389
|
+
const lines = [];
|
|
390
|
+
let lineTokens = [];
|
|
391
|
+
let lineW = 0;
|
|
392
|
+
let linesUsed = 0;
|
|
393
|
+
|
|
394
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
395
|
+
const t = tokens[ti];
|
|
396
|
+
if (lineTokens.length === 0 && t.type === 'ws') continue;
|
|
397
|
+
|
|
398
|
+
let tw = strWidth(t.text);
|
|
399
|
+
const remaining = maxCols - lineW;
|
|
400
|
+
|
|
401
|
+
// Token fits entirely
|
|
402
|
+
if (lineW + tw <= maxCols) {
|
|
403
|
+
lineTokens.push(t);
|
|
404
|
+
lineW += tw;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Token doesn't fit — split it to fill remaining space
|
|
409
|
+
if (lineTokens.length > 0 && remaining > 5) {
|
|
410
|
+
// Split token: put first part on current line
|
|
411
|
+
const head = truncTo(t.text, remaining);
|
|
412
|
+
lineTokens.push({ ...t, text: head });
|
|
413
|
+
lines.push(lineTokens);
|
|
414
|
+
linesUsed++;
|
|
415
|
+
lineTokens = [];
|
|
416
|
+
lineW = 0;
|
|
417
|
+
|
|
418
|
+
// Put rest on next line(s)
|
|
419
|
+
let rest = t.text.slice(_truncConsumed);
|
|
420
|
+
if (rest && linesUsed < maxLines) {
|
|
421
|
+
while (rest && linesUsed < maxLines) {
|
|
422
|
+
const isLast = linesUsed === maxLines - 1;
|
|
423
|
+
const budget = isLast ? maxCols - 1 : maxCols;
|
|
424
|
+
const chunk = truncTo(rest, budget);
|
|
425
|
+
lines.push([{ ...t, text: chunk }]);
|
|
426
|
+
linesUsed++;
|
|
427
|
+
rest = rest.slice(_truncConsumed);
|
|
428
|
+
if (chunk.endsWith('\u2026')) break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
// Flush current line, start fresh
|
|
433
|
+
if (lineTokens.length > 0) {
|
|
434
|
+
lines.push(lineTokens);
|
|
435
|
+
linesUsed++;
|
|
436
|
+
lineTokens = [];
|
|
437
|
+
lineW = 0;
|
|
438
|
+
}
|
|
439
|
+
if (linesUsed >= maxLines) break;
|
|
440
|
+
|
|
441
|
+
// Split token onto new line(s)
|
|
442
|
+
let rest = t.text;
|
|
443
|
+
while (rest && linesUsed < maxLines) {
|
|
444
|
+
const isLast = linesUsed === maxLines - 1;
|
|
445
|
+
const budget = isLast ? maxCols - 1 : maxCols;
|
|
446
|
+
const chunk = truncTo(rest, budget);
|
|
447
|
+
lines.push([{ ...t, text: chunk }]);
|
|
448
|
+
linesUsed++;
|
|
449
|
+
rest = rest.slice(_truncConsumed);
|
|
450
|
+
if (chunk.endsWith('\u2026')) break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (lineTokens.length > 0 && linesUsed < maxLines) lines.push(lineTokens);
|
|
455
|
+
|
|
456
|
+
while (lines.length > maxLines) lines.pop();
|
|
457
|
+
if (lines.length >= maxLines) {
|
|
458
|
+
const last = lines[lines.length - 1];
|
|
459
|
+
const lastStr = last.map(t => t.text).join('');
|
|
460
|
+
if (!lastStr.endsWith('\u2026')) {
|
|
461
|
+
const truncated = truncTo(lastStr, maxCols - 1);
|
|
462
|
+
lines[lines.length - 1] = [{ type: 'plain', text: truncated + '\u2026' }];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return lines;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Render token lines as colored strings
|
|
470
|
+
const renderTokenLines = (tokenLines) => tokenLines.map(line => line.map(colorToken).join(''));
|
|
471
|
+
|
|
472
|
+
// Wrap result text into up to N lines, each fitting within maxCols
|
|
473
|
+
function wrapResult(text, maxCols, maxLines) {
|
|
474
|
+
const flat = text.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
475
|
+
const lines = [];
|
|
476
|
+
let remaining = flat;
|
|
477
|
+
while (remaining && lines.length < maxLines) {
|
|
478
|
+
const isLast = lines.length === maxLines - 1;
|
|
479
|
+
const budget = isLast ? maxCols - 1 : maxCols;
|
|
480
|
+
const chunk = truncTo(remaining, budget);
|
|
481
|
+
lines.push(chunk);
|
|
482
|
+
remaining = remaining.slice(_truncConsumed);
|
|
483
|
+
if (!remaining) break;
|
|
484
|
+
if (isLast) {
|
|
485
|
+
// Force … on last line if more text remains
|
|
486
|
+
if (!lines[lines.length - 1].endsWith('\u2026'))
|
|
487
|
+
lines[lines.length - 1] = truncTo(chunk, budget - 1) + '\u2026';
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return lines;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function fmt(e, label) {
|
|
495
|
+
const t = fmtT(e.ts), id = e.id?.slice(0, 10) || '?';
|
|
496
|
+
|
|
497
|
+
if (opts.compact) {
|
|
498
|
+
const args = Object.entries(e.args || {}).slice(0, 2).map(([k, v]) => T.x(`${k}=`) + trunc(v, 50)).join(' ');
|
|
499
|
+
let line = `${T.x(t)} ${T.badge(e.name)} ${T.b(trunc(label, 20))} ${args}`;
|
|
500
|
+
if (e.result != null) {
|
|
501
|
+
const sz = fmtSz(Buffer.byteLength(e.result, 'utf8'));
|
|
502
|
+
const dur = e.rts ? durColor(fmtDur(e.ts, e.rts)) : '';
|
|
503
|
+
const meta = [dur, T.x(sz)].filter(Boolean).join(T.x(' · '));
|
|
504
|
+
line += `\n ${T.x('↳')} ${meta} ${T.d(trunc(e.result, 70))}`;
|
|
505
|
+
}
|
|
506
|
+
return line;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Card layout — full width
|
|
510
|
+
const W = process.stdout.columns || 80;
|
|
511
|
+
const lines = [];
|
|
512
|
+
|
|
513
|
+
// Top border
|
|
514
|
+
lines.push(T.x('┌' + '─'.repeat(W - 2) + '┐'));
|
|
515
|
+
|
|
516
|
+
// Row 1: time · session · tool badge · id
|
|
517
|
+
const badge = T.badge(e.name);
|
|
518
|
+
const row1 = `${T.x('│')} ${T.x(t)} ${T.b(trunc(label, W - 38))} ${badge} ${T.x(id)}`;
|
|
519
|
+
lines.push(padTo(row1, W - 1) + T.x('│'));
|
|
520
|
+
|
|
521
|
+
// Args
|
|
522
|
+
const args = Object.entries(e.args || {}).slice(0, 4);
|
|
523
|
+
const keyW = args.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
524
|
+
for (const [k, v] of args) {
|
|
525
|
+
for (const wl of wrapArg(k, v, W - 4, keyW)) {
|
|
526
|
+
const valPart = wl.indent ? wl.indent + wl.val : T.c(wl.key.padEnd(keyW)) + ' ' + wl.val;
|
|
527
|
+
lines.push(padTo(`${T.x('│')} ${valPart}`, W - 1) + T.x('│'));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Result
|
|
532
|
+
if (e.result != null) {
|
|
533
|
+
const isErr = e.result.includes('"status": "error"') || e.result.includes('"error"');
|
|
534
|
+
const icon = isErr ? T.err('✗') : T.ok('✓');
|
|
535
|
+
const dur = e.rts ? durColor(fmtDur(e.ts, e.rts)) : '';
|
|
536
|
+
const sz = T.x(fmtSz(Buffer.byteLength(e.result, 'utf8')));
|
|
537
|
+
const meta = [dur, sz].filter(Boolean).join(T.x(' · '));
|
|
538
|
+
lines.push(padTo(`${T.x('│')} ${icon} ${T.b(e.name || '?')} ${meta}`, W - 1) + T.x('│'));
|
|
539
|
+
const rText = (typeof e.result === 'string' ? e.result : JSON.stringify(e.result)).trim();
|
|
540
|
+
const flat = rText.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
541
|
+
const isJson = flat.startsWith('{') || flat.startsWith('[');
|
|
542
|
+
if (isJson) {
|
|
543
|
+
const tokens = highlightJson(flat);
|
|
544
|
+
const tokenLines = wrapTokens(tokens, W - 10, opts.full ? 999 : 2);
|
|
545
|
+
for (const tl of renderTokenLines(tokenLines)) {
|
|
546
|
+
lines.push(padTo(`${T.x('│')} ${tl}`, W - 1) + T.x('│'));
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
const rLines = wrapResult(flat, W - 10, opts.full ? 999 : 2);
|
|
550
|
+
for (const rl of rLines) {
|
|
551
|
+
lines.push(padTo(`${T.x('│')} ${T.d(rl)}`, W - 1) + T.x('│'));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Bottom border
|
|
557
|
+
lines.push(T.x('└' + '─'.repeat(W - 2) + '┘'));
|
|
558
|
+
return lines.join('\n');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// === Live output helpers ===
|
|
562
|
+
function fmtLiveCall(tc, label) {
|
|
563
|
+
const t = fmtT(tc.timestamp);
|
|
564
|
+
const id = tc.id?.slice(0, 10) || '?';
|
|
565
|
+
const W = process.stdout.columns || 80;
|
|
566
|
+
|
|
567
|
+
if (opts.compact) {
|
|
568
|
+
const args = Object.entries(tc.arguments || {}).slice(0, 2).map(([k, v]) => T.x(`${k}=`) + trunc(v, 50)).join(' ');
|
|
569
|
+
console.log(`${T.x(t)} ${T.badge(tc.name)} ${T.b(trunc(label, 20))} ${args}`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
console.log(T.x('┌' + '─'.repeat(W - 2) + '┐'));
|
|
574
|
+
const badge = T.badge(tc.name);
|
|
575
|
+
console.log(padTo(`${T.x('│')} ${T.x(t)} ${T.b(trunc(label, W - 38))} ${badge} ${T.x(id)}`, W - 1) + T.x('│'));
|
|
576
|
+
const args = Object.entries(tc.arguments || {}).slice(0, 4);
|
|
577
|
+
const keyW = args.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
578
|
+
for (const [k, v] of args) {
|
|
579
|
+
for (const wl of wrapArg(k, v, W - 4, keyW)) {
|
|
580
|
+
const valPart = wl.indent ? wl.indent + wl.val : T.c(wl.key.padEnd(keyW)) + ' ' + wl.val;
|
|
581
|
+
console.log(padTo(`${T.x('│')} ${valPart}`, W - 1) + T.x('│'));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function fmtLiveResult(msg, ts) {
|
|
587
|
+
const callId = msg.toolCallId;
|
|
588
|
+
const id = callId?.slice(0, 10) || '?';
|
|
589
|
+
const name = msg.toolName || '?';
|
|
590
|
+
const rStr = typeof (msg.content?.[0]?.text || '') === 'string' ? (msg.content?.[0]?.text || '') : JSON.stringify(msg.content?.[0]?.text || '');
|
|
591
|
+
const isErr = rStr.includes('"status": "error"') || rStr.includes('"error"');
|
|
592
|
+
const icon = isErr ? T.err('✗') : T.ok('✓');
|
|
593
|
+
const sz = T.x(fmtSz(Buffer.byteLength(rStr, 'utf8')));
|
|
594
|
+
let dur = '';
|
|
595
|
+
if (pending[callId]) {
|
|
596
|
+
dur = durColor(fmtDur(pending[callId], ts));
|
|
597
|
+
delete pending[callId];
|
|
598
|
+
}
|
|
599
|
+
const meta = [dur, sz].filter(Boolean).join(T.x(' · '));
|
|
600
|
+
|
|
601
|
+
if (opts.compact) {
|
|
602
|
+
console.log(` ${T.x('↳')} ${meta} ${T.d(trunc(rStr, 70))}`);
|
|
603
|
+
} else {
|
|
604
|
+
const W = process.stdout.columns || 80;
|
|
605
|
+
console.log(padTo(`${T.x('│')} ${icon} ${T.b(name)} ${meta}`, W - 1) + T.x('│'));
|
|
606
|
+
const flat = rStr.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
607
|
+
const isJson = flat.startsWith('{') || flat.startsWith('[');
|
|
608
|
+
if (isJson) {
|
|
609
|
+
const tokens = highlightJson(flat);
|
|
610
|
+
const tokenLines = wrapTokens(tokens, W - 10, opts.full ? 999 : 2);
|
|
611
|
+
for (const tl of renderTokenLines(tokenLines)) {
|
|
612
|
+
console.log(padTo(`${T.x('│')} ${tl}`, W - 1) + T.x('│'));
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
const rLines = wrapResult(flat, W - 10, opts.full ? 999 : 2);
|
|
616
|
+
for (const rl of rLines) {
|
|
617
|
+
console.log(padTo(`${T.x('│')} ${T.d(rl)}`, W - 1) + T.x('│'));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
console.log(T.x('└' + '─'.repeat(W - 2) + '┘'));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// === Main ===
|
|
625
|
+
const sessions = findSessions();
|
|
626
|
+
if (!sessions.length) {
|
|
627
|
+
console.error(`No sessions found under ${DIR}. Try --all.`);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const labels = {};
|
|
632
|
+
for (const f of sessions) labels[path.basename(f)] = getLabel(f);
|
|
633
|
+
|
|
634
|
+
// Header
|
|
635
|
+
console.log('');
|
|
636
|
+
console.log(` ${T.bd('clawmonitor')} ${T.x(`${sessions.length} sessions · ${os.type()} · Ctrl+C to stop`)}`);
|
|
637
|
+
|
|
638
|
+
// Sessions (compact inline)
|
|
639
|
+
console.log('');
|
|
640
|
+
const sessLine = sessions.map(f => {
|
|
641
|
+
const fn = path.basename(f);
|
|
642
|
+
return T.tag(labels[fn]?.split('/').pop() || fn.slice(0, 8));
|
|
643
|
+
}).join(' ');
|
|
644
|
+
console.log(` ${sessLine}`);
|
|
645
|
+
|
|
646
|
+
// History
|
|
647
|
+
console.log('');
|
|
648
|
+
let all = [];
|
|
649
|
+
for (const f of sessions) all.push(...extract(f));
|
|
650
|
+
const paired = pair(all).slice(-opts.history);
|
|
651
|
+
for (const e of paired) console.log(fmt(e, labels[e.file] || e.file));
|
|
652
|
+
|
|
653
|
+
// Live
|
|
654
|
+
console.log('');
|
|
655
|
+
console.log(` ${T.bd('Live')}`);
|
|
656
|
+
|
|
657
|
+
// === Polling ===
|
|
658
|
+
const pending = {};
|
|
659
|
+
const watched = new Set();
|
|
660
|
+
const pos = {};
|
|
661
|
+
|
|
662
|
+
function getFiles() {
|
|
663
|
+
const out = [];
|
|
664
|
+
if (!fs.existsSync(DIR)) return out;
|
|
665
|
+
for (const ag of fs.readdirSync(DIR)) {
|
|
666
|
+
const sd = path.join(DIR, ag, 'sessions');
|
|
667
|
+
if (!fs.existsSync(sd)) continue;
|
|
668
|
+
for (const f of fs.readdirSync(sd))
|
|
669
|
+
if (f.endsWith('.jsonl')) out.push(path.join(sd, f));
|
|
670
|
+
}
|
|
671
|
+
return out;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Init at end of files
|
|
675
|
+
for (const f of getFiles()) {
|
|
676
|
+
watched.add(f);
|
|
677
|
+
try { pos[f] = fs.statSync(f).size; if (!labels[path.basename(f)]) labels[path.basename(f)] = getLabel(f); } catch {}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function poll() {
|
|
681
|
+
for (const f of getFiles()) {
|
|
682
|
+
if (!watched.has(f)) {
|
|
683
|
+
watched.add(f); pos[f] = 0;
|
|
684
|
+
try { labels[path.basename(f)] = getLabel(f); } catch {}
|
|
685
|
+
}
|
|
686
|
+
let p = pos[f] || 0;
|
|
687
|
+
let data;
|
|
688
|
+
try {
|
|
689
|
+
const st = fs.statSync(f);
|
|
690
|
+
if (st.size < p) { p = 0; }
|
|
691
|
+
if (st.size === p) continue;
|
|
692
|
+
// Skip stale data — if delta > 1MB, re-seek to last 100KB
|
|
693
|
+
if (st.size - p > 1048576) { p = Math.max(0, st.size - 102400); }
|
|
694
|
+
const fd = fs.openSync(f, 'r');
|
|
695
|
+
const buf = Buffer.alloc(st.size - p);
|
|
696
|
+
fs.readSync(fd, buf, 0, buf.length, p);
|
|
697
|
+
fs.closeSync(fd);
|
|
698
|
+
data = buf.toString('utf8');
|
|
699
|
+
pos[f] = st.size;
|
|
700
|
+
} catch { continue; }
|
|
701
|
+
|
|
702
|
+
for (const ln of data.split('\n')) {
|
|
703
|
+
if (!ln.trim()) continue;
|
|
704
|
+
let o; try { o = JSON.parse(ln); } catch { continue; }
|
|
705
|
+
if (o.type !== 'message' || !o.timestamp) continue;
|
|
706
|
+
const ts = o.timestamp;
|
|
707
|
+
const fn = path.basename(f);
|
|
708
|
+
const label = labels[fn] || fn;
|
|
709
|
+
|
|
710
|
+
if (o.message.role === 'assistant') {
|
|
711
|
+
for (const tc of (o.message.content || []).filter(c => c.type === 'toolCall')) {
|
|
712
|
+
pending[tc.id] = ts;
|
|
713
|
+
fmtLiveCall({ ...tc, timestamp: ts }, label);
|
|
714
|
+
}
|
|
715
|
+
} else if (o.message.role === 'toolResult') {
|
|
716
|
+
fmtLiveResult(o.message, ts);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
setInterval(poll, 200);
|
|
723
|
+
|
|
724
|
+
// Also re-scan for new files every 10s
|
|
725
|
+
setInterval(() => {
|
|
726
|
+
for (const f of getFiles()) {
|
|
727
|
+
if (!watched.has(f)) {
|
|
728
|
+
watched.add(f); pos[f] = 0;
|
|
729
|
+
try { labels[path.basename(f)] = getLabel(f); } catch {}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}, 10000);
|
|
733
|
+
process.on('SIGINT', () => { console.log(`\n ${T.x('Stopped.')}`); process.exit(0); });
|