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/package.json CHANGED
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "name": "clawmonitor",
3
- "version": "1.0.1",
4
- "description": "Real-time OpenClaw tool call monitor watch all agent sessions with readable names and sorted output",
5
- "bin": {
6
- "clawmonitor": "./bin/clawmonitor.sh"
7
- },
3
+ "version": "1.1.2",
4
+ "description": "Real-time OpenClaw tool call monitor with TUI, JSON highlighting, and zero dependencies",
5
+ "bin": "./bin/clawmonitor.js",
8
6
  "scripts": {
9
- "test": "bash -n bin/clawmonitor.sh"
7
+ "test": "node --check bin/clawmonitor.js"
10
8
  },
11
9
  "keywords": [
12
10
  "openclaw",
@@ -1,466 +0,0 @@
1
- #!/usr/bin/env bash
2
- # clawmonitor — Real-time OpenClaw tool call monitor
3
- #
4
- # Usage:
5
- # clawmonitor # Default: last 30 min sessions + 10 history
6
- # clawmonitor --all # Monitor all sessions
7
- # clawmonitor --compact # Compact output
8
- # clawmonitor --history N # Show N recent tool calls
9
- #
10
- # npx clawmonitor # Run without installing
11
-
12
- set -euo pipefail
13
-
14
- # === Platform detection ===
15
- OS="$(uname -s 2>/dev/null || echo "unknown")"
16
- ARCH="$(uname -m 2>/dev/null || echo "unknown")"
17
-
18
- IS_WINDOWS=false
19
- if [[ "$OS" == "MINGW"* || "$OS" == "MSYS"* || "$OS" == "CYGWIN"* ]]; then
20
- IS_WINDOWS=true
21
- fi
22
-
23
- # === Detect OpenClaw data directory ===
24
- if [[ -n "${OPENCLAW_HOME:-}" ]]; then
25
- AGENTS_DIR="$OPENCLAW_HOME/agents"
26
- elif [[ -d "$HOME/.openclaw/agents" ]]; then
27
- AGENTS_DIR="$HOME/.openclaw/agents"
28
- elif [[ -n "${XDG_DATA_HOME:-}" ]] && [[ -d "$XDG_DATA_HOME/openclaw/agents" ]]; then
29
- AGENTS_DIR="$XDG_DATA_HOME/openclaw/agents"
30
- elif [[ "$IS_WINDOWS" == "true" ]] && [[ -d "$APPDATA/openclaw/agents" ]]; then
31
- AGENTS_DIR="$APPDATA/openclaw/agents"
32
- else
33
- echo "❌ Cannot find OpenClaw data directory."
34
- echo ""
35
- echo " Tried:"
36
- echo " - \$OPENCLAW_HOME/agents"
37
- echo " - ~/.openclaw/agents"
38
- echo " - \$XDG_DATA_HOME/openclaw/agents"
39
- if [[ "$IS_WINDOWS" == "true" ]]; then
40
- echo " - %APPDATA%/openclaw/agents"
41
- fi
42
- echo ""
43
- echo " Set OPENCLAW_HOME to your OpenClaw data directory."
44
- exit 1
45
- fi
46
-
47
- # === Check dependencies ===
48
- MISSING=()
49
- for cmd in jq tail date; do
50
- if ! command -v "$cmd" &>/dev/null; then
51
- MISSING+=("$cmd")
52
- fi
53
- done
54
-
55
- if [[ ${#MISSING[@]} -gt 0 ]]; then
56
- echo "❌ Missing required commands: ${MISSING[*]}"
57
- echo ""
58
- echo " Install them:"
59
- echo ""
60
- if [[ "$IS_WINDOWS" == "true" ]]; then
61
- echo " 🪟 Windows (Git Bash / MSYS2):"
62
- echo " pacman -S jq"
63
- echo ""
64
- echo " Or use WSL:"
65
- echo " sudo apt install jq"
66
- elif [[ "$OS" == "Darwin" ]]; then
67
- echo " 🍎 macOS:"
68
- echo " brew install jq"
69
- echo ""
70
- echo " No Homebrew? Install it first:"
71
- echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
72
- else
73
- echo " 🐧 Linux:"
74
- echo " sudo apt install jq # Debian/Ubuntu"
75
- echo " sudo dnf install jq # Fedora"
76
- echo " sudo pacman -S jq # Arch"
77
- echo " sudo apk add jq # Alpine"
78
- fi
79
- echo ""
80
- echo " jq is the only external dependency. tail and date are usually pre-installed."
81
- exit 1
82
- fi
83
-
84
- # === Cross-platform helpers ===
85
-
86
- # Get file modification time (HH:MM:SS)
87
- get_mod_time() {
88
- local file="$1"
89
- if [[ "$OS" == "Darwin" ]]; then
90
- stat -f '%Sm' -t '%H:%M:%S' "$file" 2>/dev/null || echo "???"
91
- elif [[ "$IS_WINDOWS" == "true" ]]; then
92
- stat -c '%y' "$file" 2>/dev/null | cut -c12-19 || echo "???"
93
- else
94
- stat -c '%y' "$file" 2>/dev/null | cut -c12-19 || echo "???"
95
- fi
96
- }
97
-
98
- # Format ISO timestamp → HH:MM:SS
99
- format_timestamp() {
100
- local ts="$1"
101
- if [[ -z "$ts" || "$ts" == "null" || "$ts" == "???" ]]; then
102
- echo "???"
103
- return
104
- fi
105
-
106
- if [[ "$OS" == "Darwin" ]]; then
107
- if date -j -f "%Y-%m-%dT%H:%M:%S" "$(echo "$ts" | cut -c1-19)" "+%H:%M:%S" 2>/dev/null; then
108
- return
109
- fi
110
- echo "$ts" | cut -c12-19
111
- else
112
- date -d "$ts" "+%H:%M:%S" 2>/dev/null || echo "$ts" | cut -c12-19
113
- fi
114
- }
115
-
116
- # Portable grep (no -P on macOS)
117
- grep_p() {
118
- if [[ "$OS" == "Darwin" ]]; then
119
- grep -E "$@"
120
- else
121
- grep -P "$@"
122
- fi
123
- }
124
-
125
- # === Argument parsing ===
126
- COMPACT=false
127
- SHOW_HISTORY=false
128
- HISTORY_N=10
129
- TIME_RANGE=30 # minutes, 0 = all
130
-
131
- while [[ $# -gt 0 ]]; do
132
- case "$1" in
133
- --compact)
134
- COMPACT=true
135
- shift
136
- ;;
137
- --history)
138
- SHOW_HISTORY=true
139
- if [[ $# -gt 1 ]] && [[ "$2" =~ ^[0-9]+$ ]]; then
140
- HISTORY_N="$2"
141
- shift 2
142
- else
143
- shift
144
- fi
145
- ;;
146
- --all)
147
- TIME_RANGE=0
148
- shift
149
- ;;
150
- --help|-h)
151
- echo "clawmonitor — Real-time OpenClaw tool call monitor"
152
- echo ""
153
- echo "Usage: clawmonitor [options]"
154
- echo ""
155
- echo " --all Monitor all sessions (no time filter)"
156
- echo " --compact Show only tool name + brief args"
157
- echo " --history Show recent history before live tail"
158
- echo " --history N Show last N tool calls from history (default: 10)"
159
- echo " --help Show this help"
160
- echo ""
161
- echo " Monitors all agents under ~/.openclaw/agents/"
162
- echo " Supports Linux, macOS, and Windows (Git Bash / WSL)"
163
- echo ""
164
- echo " Environment variables:"
165
- echo " OPENCLAW_HOME Custom OpenClaw data directory"
166
- echo " NO_COLOR Disable colored output"
167
- exit 0
168
- ;;
169
- [0-9]*)
170
- if $SHOW_HISTORY; then
171
- HISTORY_N="$1"
172
- shift
173
- else
174
- echo "Unknown argument: $1"
175
- exit 1
176
- fi
177
- ;;
178
- *)
179
- echo "Unknown argument: $1"
180
- exit 1
181
- ;;
182
- esac
183
- done
184
-
185
- # === Color setup ===
186
- if [[ -t 1 ]] && [[ "${NO_COLOR:-}" == "" ]]; then
187
- RST='\033[0m' BLD='\033[1m' DIM='\033[2m'
188
- CYN='\033[36m' GRN='\033[32m' YLW='\033[33m' MAG='\033[35m' RED='\033[31m'
189
- BLU='\033[34m' WHT='\033[37m'
190
- else
191
- RST='' BLD='' DIM=''
192
- CYN='' GRN='' YLW='' MAG='' RED=''
193
- BLU='' WHT=''
194
- fi
195
-
196
- # === Session management ===
197
-
198
- find_sessions() {
199
- local find_cmd=(find "$AGENTS_DIR" -path "*/sessions/*.jsonl" -type f)
200
- if [[ "$TIME_RANGE" -eq 0 ]]; then
201
- "${find_cmd[@]}"
202
- else
203
- local now_epoch
204
- now_epoch=$(date +%s)
205
-
206
- "${find_cmd[@]}" | while IFS= read -r file; do
207
- local mod_epoch
208
- if [[ "$OS" == "Darwin" ]]; then
209
- mod_epoch=$(stat -f '%m' "$file" 2>/dev/null || echo 0)
210
- else
211
- mod_epoch=$(stat -c '%Y' "$file" 2>/dev/null || echo 0)
212
- fi
213
-
214
- if [[ "$mod_epoch" -gt 0 ]] && (( now_epoch - mod_epoch <= TIME_RANGE * 60 )); then
215
- echo "$file"
216
- fi
217
- done
218
- fi
219
- }
220
-
221
- # Extract readable session name from conversation_label
222
- get_session_label() {
223
- local file="$1"
224
- local agent_name
225
- agent_name=$(echo "$file" | sed "s|$AGENTS_DIR/||;s|/sessions/.*||")
226
-
227
- local filename
228
- filename=$(basename "$file" .jsonl)
229
-
230
- local conv_label
231
- conv_label=$(jq -r 'select(.type=="message" and .message.role=="user") |
232
- .message.content[] | select(.type=="text") | .text' "$file" 2>/dev/null | \
233
- head -20 | \
234
- grep_p -o '"conversation_label"[[:space:]]*:[[:space:]]*"[^"]*"' 2>/dev/null | head -1 | \
235
- sed 's/"conversation_label" : "//;s/"conversation_label": "//;s/"$//')
236
-
237
- if [[ -n "$conv_label" ]]; then
238
- local group topic group_name
239
- group=$(echo "$conv_label" | grep_p -o 'id:[^ ]+' 2>/dev/null | sed 's/id://')
240
- topic=$(echo "$conv_label" | grep_p -o 'topic:[0-9]+' 2>/dev/null | sed 's/topic:/t/')
241
- group_name=$(echo "$conv_label" | grep_p -o '^[^ ]+' 2>/dev/null)
242
-
243
- if [[ -n "$topic" ]]; then
244
- echo "${agent_name}/${group_name}/${topic}"
245
- elif [[ -n "$group_name" ]]; then
246
- echo "${agent_name}/${group_name}"
247
- else
248
- echo "${agent_name}/${conv_label}"
249
- fi
250
- else
251
- local short_id topic_suffix
252
- short_id=$(echo "$filename" | sed 's/-topic-[0-9]*$//;s/-\([0-9a-f]\{4\}\).*/..*\1/')
253
- topic_suffix=$(echo "$filename" | grep_p -o 'topic-[0-9]+' 2>/dev/null || true)
254
- if [[ -n "$topic_suffix" ]]; then
255
- echo "${agent_name}/DM/${topic_suffix}"
256
- else
257
- echo "${agent_name}/DM/${short_id}"
258
- fi
259
- fi
260
- }
261
-
262
- # Session label store (portable: works on bash 3.2+ / macOS)
263
- # Uses a temp file as key\0value pairs instead of associative arrays
264
- LABEL_STORE=$(mktemp /tmp/clawmonitor-labels.XXXXXX)
265
- trap 'rm -f "$LABEL_STORE"' EXIT
266
-
267
- _label_set() {
268
- local key="$1" val="$2"
269
- grep -v "^${key}"$'\t' "$LABEL_STORE" > "${LABEL_STORE}.tmp" 2>/dev/null || true
270
- printf '%s\t%s\n' "$key" "$val" >> "${LABEL_STORE}.tmp"
271
- mv "${LABEL_STORE}.tmp" "$LABEL_STORE"
272
- }
273
-
274
- _label_get() {
275
- local key="$1"
276
- local result
277
- result=$(grep "^${key}"$'\t' "$LABEL_STORE" 2>/dev/null | head -1 | cut -f2-)
278
- echo "${result:-$2}"
279
- }
280
-
281
- cache_labels() {
282
- local files="$1"
283
- for f in $files; do
284
- local key
285
- key=$(basename "$f")
286
- _label_set "$key" "$(get_session_label "$f")"
287
- done
288
- }
289
-
290
- # Extract tool calls from a single session JSONL
291
- extract_tool_calls() {
292
- local file="$1"
293
- local limit="${2:-0}"
294
- local filename
295
- filename=$(basename "$file")
296
-
297
- local filter='select(.type=="message" and .message.role=="assistant") |
298
- {ts: (.timestamp // .message.timestamp), file: "'"${filename}"'", content: [.message.content[] | select(.type=="toolCall")]} |
299
- select(.content | length > 0)'
300
-
301
- if [[ "$limit" -gt 0 ]]; then
302
- jq -c "$filter" "$file" 2>/dev/null | tail -"$limit"
303
- else
304
- jq -c "$filter" "$file" 2>/dev/null
305
- fi
306
- }
307
-
308
- # Format a single tool call entry
309
- format_tc() {
310
- local json="$1"
311
- local ts filename label
312
- ts=$(echo "$json" | jq -r '.ts // "???"')
313
- filename=$(echo "$json" | jq -r '.file // "?"')
314
- label=$(_label_get "$filename" "$filename")
315
-
316
- local time_str
317
- time_str=$(format_timestamp "$ts")
318
-
319
- echo "$json" | jq -c '.content[]' 2>/dev/null | while IFS= read -r tc; do
320
- local tool_name tool_id
321
- tool_name=$(echo "$tc" | jq -r '.name // "?"')
322
- tool_id=$(echo "$tc" | jq -r '.id // "?"' | cut -c1-12)
323
-
324
- if $COMPACT; then
325
- local brief_args
326
- brief_args=$(echo "$tc" | jq -r '
327
- .arguments | to_entries[:2] |
328
- map("\(.key)=\(.value | tostring | .[0:80])") | join(" ")
329
- ' 2>/dev/null)
330
- printf "${GRN}%-13s${RST} ${BLU}%-28s${RST} ${CYN}%-9s${RST} %s\n" \
331
- "$tool_name" "${label:0:28}" "$time_str" "$brief_args"
332
- else
333
- printf "\n${BLD}${YLW}⏱ %s${RST} ${BLU}%s${RST} ${MAG}%s${RST}\n" \
334
- "$time_str" "${label}" "$tool_id"
335
- printf " ${GRN}▶ %s${RST}\n" "$tool_name"
336
-
337
- echo "$tc" | jq -c '.arguments | to_entries[]' 2>/dev/null | while IFS= read -r entry; do
338
- local key val
339
- key=$(echo "$entry" | jq -r '.key')
340
- val=$(echo "$entry" | jq -r '.value | tostring | .[0:300]')
341
- [[ ${#val} -ge 300 ]] && val="${val}…"
342
- printf " ${CYN}%-14s${RST} %s\n" "$key" "$val"
343
- done
344
- fi
345
- done
346
- }
347
-
348
- # === Main ===
349
-
350
- sessions=$(find_sessions | sort)
351
- session_count=$(echo "$sessions" | grep -c . 2>/dev/null || echo 0)
352
-
353
- if [[ "$session_count" -eq 0 ]]; then
354
- echo -e "${RED}No sessions found under ${AGENTS_DIR}${RST}"
355
- echo "Try --all or wait for activity."
356
- exit 1
357
- fi
358
-
359
- cache_labels "$sessions"
360
-
361
- echo -e "${BLD}🔧 OpenClaw Tool Call Monitor${RST}"
362
- echo -e "${DIM}Watching ${session_count} session(s) on ${OS} — Ctrl+C to stop${RST}"
363
- echo ""
364
-
365
- # List all sessions
366
- echo -e "${BLD}📋 Active sessions:${RST}"
367
- for f in $sessions; do
368
- filename=$(basename "$f")
369
- label=$(_label_get "$filename" "$filename")
370
- mod_time=$(get_mod_time "$f")
371
- printf " ${DIM}%-9s${RST} ${BLU}%-35s${RST} ${DIM}(last: %s)${RST}\n" \
372
- "$(echo "$filename" | cut -c1-8)" "${label:0:35}" "$mod_time"
373
- done
374
-
375
- if $COMPACT; then
376
- printf "\n${DIM}%-13s %-28s %-9s %s${RST}\n" "TOOL" "SESSION" "TIME" "ARGS"
377
- echo "-------------------------------------------------------------------------"
378
- fi
379
-
380
- # Default: show last 10 history entries
381
- if ! $SHOW_HISTORY; then
382
- SHOW_HISTORY=true
383
- fi
384
-
385
- # Show history sorted by time
386
- if $SHOW_HISTORY; then
387
- echo -e "\n${BLD}📜 Recent tool calls (sorted by time):${RST}"
388
- for f in $sessions; do
389
- extract_tool_calls "$f" 0
390
- done | jq -s 'sort_by(.ts)' | jq -c '.[]' > /tmp/openclaw-tc-history.jsonl
391
-
392
- if [[ "$HISTORY_N" -gt 0 ]]; then
393
- tail -"$HISTORY_N" /tmp/openclaw-tc-history.jsonl | while IFS= read -r line; do
394
- format_tc "$line"
395
- done
396
- else
397
- cat /tmp/openclaw-tc-history.jsonl | while IFS= read -r line; do
398
- format_tc "$line"
399
- done
400
- fi
401
- rm -f /tmp/openclaw-tc-history.jsonl
402
-
403
- echo -e "\n${BLD}🔴 Live monitoring:${RST}"
404
- if $COMPACT; then
405
- echo "-------------------------------------------------------------------------"
406
- fi
407
- fi
408
-
409
- # Live monitoring with glob pattern for auto-tracking new sessions
410
- SESSIONS_GLOB="${AGENTS_DIR}/*/sessions/*.jsonl"
411
- shopt -s nullglob
412
- live_session_files=("$AGENTS_DIR"/*/sessions/*.jsonl)
413
- shopt -u nullglob
414
-
415
- tail -F -n 0 $SESSIONS_GLOB 2>/dev/null | {
416
- current_file=""
417
- if [[ "${#live_session_files[@]}" -eq 1 ]]; then
418
- current_file="${live_session_files[0]}"
419
- fi
420
- while IFS= read -r line; do
421
- # Detect tail -F file switch header
422
- if [[ "$line" == "==>"* ]]; then
423
- current_file=$(echo "$line" | sed 's/==> \(.*\) <==/\1/')
424
- continue
425
- fi
426
-
427
- # Quick filter
428
- echo "$line" | grep -q '"toolCall"' || continue
429
- echo "$line" | grep -q '"assistant"' || continue
430
-
431
- filename=$(basename "${current_file}")
432
- # Dynamic label lookup for new sessions
433
- if [[ -z "$(_label_get "$filename" "")" ]]; then
434
- _label_set "$filename" "$(get_session_label "${current_file}")"
435
- fi
436
- label=$(_label_get "$filename" "$filename")
437
-
438
- ts=$(echo "$line" | jq -r '.timestamp // .message.timestamp // "???"' 2>/dev/null)
439
- time_str=$(format_timestamp "$ts")
440
-
441
- echo "$line" | jq -c '.message.content[] | select(.type=="toolCall")' 2>/dev/null | while IFS= read -r tc; do
442
- tool_name=$(echo "$tc" | jq -r '.name // "?"')
443
- tool_id=$(echo "$tc" | jq -r '.id // "?"' | cut -c1-12)
444
-
445
- if $COMPACT; then
446
- brief_args=$(echo "$tc" | jq -r '
447
- .arguments | to_entries[:2] |
448
- map("\(.key)=\(.value | tostring | .[0:80])") | join(" ")
449
- ' 2>/dev/null)
450
- printf "${GRN}%-13s${RST} ${BLU}%-28s${RST} ${CYN}%-9s${RST} %s\n" \
451
- "$tool_name" "${label:0:28}" "$time_str" "$brief_args"
452
- else
453
- printf "\n${BLD}${YLW}⏱ %s${RST} ${BLU}%s${RST} ${MAG}%s${RST}\n" \
454
- "$time_str" "${label}" "$tool_id"
455
- printf " ${GRN}▶ %s${RST}\n" "$tool_name"
456
-
457
- echo "$tc" | jq -c '.arguments | to_entries[]' 2>/dev/null | while IFS= read -r entry; do
458
- key=$(echo "$entry" | jq -r '.key')
459
- val=$(echo "$entry" | jq -r '.value | tostring | .[0:300]')
460
- [[ ${#val} -ge 300 ]] && val="${val}…"
461
- printf " ${CYN}%-14s${RST} %s\n" "$key" "$val"
462
- done
463
- fi
464
- done
465
- done
466
- }