clawmonitor 1.0.0 → 1.1.1

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.0",
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.1",
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,449 +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
- declare -A SESSION_LABELS
263
-
264
- cache_labels() {
265
- local files="$1"
266
- for f in $files; do
267
- local key
268
- key=$(basename "$f")
269
- SESSION_LABELS["$key"]=$(get_session_label "$f")
270
- done
271
- }
272
-
273
- # Extract tool calls from a single session JSONL
274
- extract_tool_calls() {
275
- local file="$1"
276
- local limit="${2:-0}"
277
- local filename
278
- filename=$(basename "$file")
279
-
280
- local filter='select(.type=="message" and .message.role=="assistant") |
281
- {ts: (.timestamp // .message.timestamp), file: "'"${filename}"'", content: [.message.content[] | select(.type=="toolCall")]} |
282
- select(.content | length > 0)'
283
-
284
- if [[ "$limit" -gt 0 ]]; then
285
- jq -c "$filter" "$file" 2>/dev/null | tail -"$limit"
286
- else
287
- jq -c "$filter" "$file" 2>/dev/null
288
- fi
289
- }
290
-
291
- # Format a single tool call entry
292
- format_tc() {
293
- local json="$1"
294
- local ts filename label
295
- ts=$(echo "$json" | jq -r '.ts // "???"')
296
- filename=$(echo "$json" | jq -r '.file // "?"')
297
- label="${SESSION_LABELS[$filename]:-${filename}}"
298
-
299
- local time_str
300
- time_str=$(format_timestamp "$ts")
301
-
302
- echo "$json" | jq -c '.content[]' 2>/dev/null | while IFS= read -r tc; do
303
- local tool_name tool_id
304
- tool_name=$(echo "$tc" | jq -r '.name // "?"')
305
- tool_id=$(echo "$tc" | jq -r '.id // "?"' | cut -c1-12)
306
-
307
- if $COMPACT; then
308
- local brief_args
309
- brief_args=$(echo "$tc" | jq -r '
310
- .arguments | to_entries[:2] |
311
- map("\(.key)=\(.value | tostring | .[0:80])") | join(" ")
312
- ' 2>/dev/null)
313
- printf "${GRN}%-13s${RST} ${BLU}%-28s${RST} ${CYN}%-9s${RST} %s\n" \
314
- "$tool_name" "${label:0:28}" "$time_str" "$brief_args"
315
- else
316
- printf "\n${BLD}${YLW}⏱ %s${RST} ${BLU}%s${RST} ${MAG}%s${RST}\n" \
317
- "$time_str" "${label}" "$tool_id"
318
- printf " ${GRN}▶ %s${RST}\n" "$tool_name"
319
-
320
- echo "$tc" | jq -c '.arguments | to_entries[]' 2>/dev/null | while IFS= read -r entry; do
321
- local key val
322
- key=$(echo "$entry" | jq -r '.key')
323
- val=$(echo "$entry" | jq -r '.value | tostring | .[0:300]')
324
- [[ ${#val} -ge 300 ]] && val="${val}…"
325
- printf " ${CYN}%-14s${RST} %s\n" "$key" "$val"
326
- done
327
- fi
328
- done
329
- }
330
-
331
- # === Main ===
332
-
333
- sessions=$(find_sessions | sort)
334
- session_count=$(echo "$sessions" | grep -c . 2>/dev/null || echo 0)
335
-
336
- if [[ "$session_count" -eq 0 ]]; then
337
- echo -e "${RED}No sessions found under ${AGENTS_DIR}${RST}"
338
- echo "Try --all or wait for activity."
339
- exit 1
340
- fi
341
-
342
- cache_labels "$sessions"
343
-
344
- echo -e "${BLD}🔧 OpenClaw Tool Call Monitor${RST}"
345
- echo -e "${DIM}Watching ${session_count} session(s) on ${OS} — Ctrl+C to stop${RST}"
346
- echo ""
347
-
348
- # List all sessions
349
- echo -e "${BLD}📋 Active sessions:${RST}"
350
- for f in $sessions; do
351
- filename=$(basename "$f")
352
- label="${SESSION_LABELS[$filename]}"
353
- mod_time=$(get_mod_time "$f")
354
- printf " ${DIM}%-9s${RST} ${BLU}%-35s${RST} ${DIM}(last: %s)${RST}\n" \
355
- "$(echo "$filename" | cut -c1-8)" "${label:0:35}" "$mod_time"
356
- done
357
-
358
- if $COMPACT; then
359
- printf "\n${DIM}%-13s %-28s %-9s %s${RST}\n" "TOOL" "SESSION" "TIME" "ARGS"
360
- echo "-------------------------------------------------------------------------"
361
- fi
362
-
363
- # Default: show last 10 history entries
364
- if ! $SHOW_HISTORY; then
365
- SHOW_HISTORY=true
366
- fi
367
-
368
- # Show history sorted by time
369
- if $SHOW_HISTORY; then
370
- echo -e "\n${BLD}📜 Recent tool calls (sorted by time):${RST}"
371
- for f in $sessions; do
372
- extract_tool_calls "$f" 0
373
- done | jq -s 'sort_by(.ts)' | jq -c '.[]' > /tmp/openclaw-tc-history.jsonl
374
-
375
- if [[ "$HISTORY_N" -gt 0 ]]; then
376
- tail -"$HISTORY_N" /tmp/openclaw-tc-history.jsonl | while IFS= read -r line; do
377
- format_tc "$line"
378
- done
379
- else
380
- cat /tmp/openclaw-tc-history.jsonl | while IFS= read -r line; do
381
- format_tc "$line"
382
- done
383
- fi
384
- rm -f /tmp/openclaw-tc-history.jsonl
385
-
386
- echo -e "\n${BLD}🔴 Live monitoring:${RST}"
387
- if $COMPACT; then
388
- echo "-------------------------------------------------------------------------"
389
- fi
390
- fi
391
-
392
- # Live monitoring with glob pattern for auto-tracking new sessions
393
- SESSIONS_GLOB="${AGENTS_DIR}/*/sessions/*.jsonl"
394
- shopt -s nullglob
395
- live_session_files=("$AGENTS_DIR"/*/sessions/*.jsonl)
396
- shopt -u nullglob
397
-
398
- tail -F -n 0 $SESSIONS_GLOB 2>/dev/null | {
399
- current_file=""
400
- if [[ "${#live_session_files[@]}" -eq 1 ]]; then
401
- current_file="${live_session_files[0]}"
402
- fi
403
- while IFS= read -r line; do
404
- # Detect tail -F file switch header
405
- if [[ "$line" == "==>"* ]]; then
406
- current_file=$(echo "$line" | sed 's/==> \(.*\) <==/\1/')
407
- continue
408
- fi
409
-
410
- # Quick filter
411
- echo "$line" | grep -q '"toolCall"' || continue
412
- echo "$line" | grep -q '"assistant"' || continue
413
-
414
- filename=$(basename "${current_file}")
415
- # Dynamic label lookup for new sessions
416
- if [[ -z "${SESSION_LABELS[$filename]:-}" ]]; then
417
- SESSION_LABELS["$filename"]=$(get_session_label "${current_file}")
418
- fi
419
- label="${SESSION_LABELS[$filename]}"
420
-
421
- ts=$(echo "$line" | jq -r '.timestamp // .message.timestamp // "???"' 2>/dev/null)
422
- time_str=$(format_timestamp "$ts")
423
-
424
- echo "$line" | jq -c '.message.content[] | select(.type=="toolCall")' 2>/dev/null | while IFS= read -r tc; do
425
- tool_name=$(echo "$tc" | jq -r '.name // "?"')
426
- tool_id=$(echo "$tc" | jq -r '.id // "?"' | cut -c1-12)
427
-
428
- if $COMPACT; then
429
- brief_args=$(echo "$tc" | jq -r '
430
- .arguments | to_entries[:2] |
431
- map("\(.key)=\(.value | tostring | .[0:80])") | join(" ")
432
- ' 2>/dev/null)
433
- printf "${GRN}%-13s${RST} ${BLU}%-28s${RST} ${CYN}%-9s${RST} %s\n" \
434
- "$tool_name" "${label:0:28}" "$time_str" "$brief_args"
435
- else
436
- printf "\n${BLD}${YLW}⏱ %s${RST} ${BLU}%s${RST} ${MAG}%s${RST}\n" \
437
- "$time_str" "${label}" "$tool_id"
438
- printf " ${GRN}▶ %s${RST}\n" "$tool_name"
439
-
440
- echo "$tc" | jq -c '.arguments | to_entries[]' 2>/dev/null | while IFS= read -r entry; do
441
- key=$(echo "$entry" | jq -r '.key')
442
- val=$(echo "$entry" | jq -r '.value | tostring | .[0:300]')
443
- [[ ${#val} -ge 300 ]] && val="${val}…"
444
- printf " ${CYN}%-14s${RST} %s\n" "$key" "$val"
445
- done
446
- fi
447
- done
448
- done
449
- }