Initial commit: custom OpenClaw skills from docker-test

- workspace: capmetro-monitor, github-notifications, model-selector
- workspace-security: vt-monitor, monitor-unauthorized
- workspace-home: cron-manager, monitor-unauthorized
- extensions: vt-sentinel (VT-Sentinel plugin)

Includes sync.sh for pull/push, README, AGENTS.md, .gitignore.
This commit is contained in:
2026-02-16 15:32:44 +00:00
commit 3b7d6bb67c
37 changed files with 3358 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
---
name: vt-monitor
description: >-
Monitor VT-Sentinel activity from gateway logs. Parses scan events, uploads,
verdicts, quarantines, blocks, and failures. Returns structured JSON of all
VT-Sentinel activity since last check.
metadata:
openclaw:
emoji: "🛡️"
---
# VT-Sentinel Monitor
Parses the OpenClaw gateway log for all VT-Sentinel activity and returns structured reports.
## Available Tools
### `vt_monitor_check` — Check Recent Activity
Returns all VT-Sentinel events since last check (or last N minutes).
```
vt_monitor_check [minutes]
```
Arguments:
- `minutes` (optional, default: 60) — How far back to look
Output: JSON with categorized events (scans, uploads, verdicts, blocks, quarantines, failures).
### `vt_monitor_tail` — Live Tail
Returns the last N VT-Sentinel log entries.
```
vt_monitor_tail [count]
```
Arguments:
- `count` (optional, default: 50) — Number of recent entries

View File

@@ -0,0 +1,111 @@
#!/bin/bash
# Check VT-Sentinel activity — incremental (byte-offset tracking)
# Only reads NEW log lines since last check
set -e
LOG_FILE="/tmp/openclaw/vt-sentinel.log"
STATE_DIR="/home/node/.openclaw/workspace-security/memory"
OFFSET_FILE="$STATE_DIR/vt-log-offset"
mkdir -p "$STATE_DIR"
if [ ! -f "$LOG_FILE" ]; then
echo '{"error":"Gateway log not found"}'
exit 1
fi
# Current file size
FILE_SIZE=$(stat -c%s "$LOG_FILE")
# Last read offset (0 = first run, reads last 1MB as bootstrap)
LAST_OFFSET=0
if [ -f "$OFFSET_FILE" ]; then
LAST_OFFSET=$(cat "$OFFSET_FILE")
fi
# If file shrank (log rotation), reset
if [ "$LAST_OFFSET" -gt "$FILE_SIZE" ]; then
LAST_OFFSET=0
fi
# Dedicated log is small — no need for bootstrap limit
SKIP=$LAST_OFFSET
BYTES_NEW=$((FILE_SIZE - SKIP))
# Nothing new
if [ "$BYTES_NEW" -le 0 ]; then
echo "$FILE_SIZE" > "$OFFSET_FILE"
echo '{"totalEvents":0,"alerts":false,"summary":{"uploads":0,"upload_complete":0,"upload_failed":0,"cache_hits":0,"verdicts":{"clean":0,"malicious":0,"suspicious":0},"quarantined":0,"blocked":0},"events":[]}'
exit 0
fi
# Read only new bytes, grep VT-Sentinel
TMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT
tail -c +"$((SKIP + 1))" "$LOG_FILE" | grep '"subsystem.*gateway"' | grep '\[VT-Sentinel\]' | while IFS= read -r line; do
TIMESTAMP=$(echo "$line" | grep -oP '"date":"\K[^"]*' | head -1)
MESSAGE=$(echo "$line" | grep -oP '\[VT-Sentinel\] \K[^"\\]*' | head -1)
[ -z "$TIMESTAMP" ] || [ -z "$MESSAGE" ] && continue
CATEGORY="info"
case "$MESSAGE" in
*"Unknown"*"uploading"*) CATEGORY="upload" ;;
*"Uploaded for analysis"*) CATEGORY="upload_complete" ;;
*"Upload failed"*) CATEGORY="upload_failed" ;;
*"Cache hit"*) CATEGORY="cache_hit" ;;
*"MALICIOUS"*) CATEGORY="verdict_malicious" ;;
*"SUSPICIOUS"*) CATEGORY="verdict_suspicious" ;;
*"clean"*|*"BENIGN"*) CATEGORY="verdict_clean" ;;
*"quarantin"*) CATEGORY="quarantine" ;;
*"BLOCKED"*|*"blocked"*) CATEGORY="blocked" ;;
*"Watching:"*) CATEGORY="config" ;;
*"Plugin loaded"*|*"Service stopped"*|*"Auto-"*|*"Registered"*|*"Using"*) CATEGORY="lifecycle" ;;
esac
jq -cn --arg ts "$TIMESTAMP" --arg msg "$MESSAGE" --arg cat "$CATEGORY" \
'{"timestamp":$ts,"message":$msg,"category":$cat}'
done > "$TMPFILE"
# Save new offset
echo "$FILE_SIZE" > "$OFFSET_FILE"
TOTAL=$(wc -l < "$TMPFILE" | tr -d ' ')
[ -z "$TOTAL" ] && TOTAL=0
if [ "$TOTAL" -eq 0 ]; then
echo '{"totalEvents":0,"alerts":false,"summary":{"uploads":0,"upload_complete":0,"upload_failed":0,"cache_hits":0,"verdicts":{"clean":0,"malicious":0,"suspicious":0},"quarantined":0,"blocked":0},"events":[]}'
exit 0
fi
EVENTS_JSON=$(jq -s '.' "$TMPFILE")
count_cat() {
local c
c=$(grep -c "\"category\":\"$1\"" "$TMPFILE" 2>/dev/null || true)
echo "${c:-0}"
}
jq -n \
--argjson events "$EVENTS_JSON" \
--argjson total "$TOTAL" \
--argjson uploads "$(count_cat upload)" \
--argjson upload_complete "$(count_cat upload_complete)" \
--argjson upload_failed "$(count_cat upload_failed)" \
--argjson cache_hits "$(count_cat cache_hit)" \
--argjson clean "$(count_cat verdict_clean)" \
--argjson malicious "$(count_cat verdict_malicious)" \
--argjson suspicious "$(count_cat verdict_suspicious)" \
--argjson quarantined "$(count_cat quarantine)" \
--argjson blocked "$(count_cat blocked)" \
'{
totalEvents: $total,
summary: {
uploads: $uploads, upload_complete: $upload_complete, upload_failed: $upload_failed,
cache_hits: $cache_hits,
verdicts: {clean: $clean, malicious: $malicious, suspicious: $suspicious},
quarantined: $quarantined, blocked: $blocked
},
alerts: ($malicious > 0 or $suspicious > 0 or $quarantined > 0 or $blocked > 0),
events: $events
}'

View File

@@ -0,0 +1,136 @@
#!/bin/bash
# VT-Sentinel monitoring cron wrapper (incremental)
# Checks both VT-Sentinel file activity AND plugin updates
set -e
SCRIPT_DIR="$(dirname "$0")"
LOG_FILE="/tmp/openclaw/openclaw.log"
STATE_DIR="/home/node/.openclaw/workspace-security/memory"
SEEN_FILE="$STATE_DIR/seen-plugin-updates.txt"
mkdir -p "$STATE_DIR"
touch "$SEEN_FILE"
OUTPUT=""
# --- VT-Sentinel activity ---
REPORT=$("$SCRIPT_DIR/check.sh" 2>/dev/null)
TOTAL=$(echo "$REPORT" | jq '.totalEvents')
ALERTS=$(echo "$REPORT" | jq '.alerts')
ACTION_EVENTS=$(echo "$REPORT" | jq '[.summary.uploads, .summary.upload_complete, .summary.upload_failed, .summary.cache_hits, .summary.verdicts.clean, .summary.verdicts.malicious, .summary.verdicts.suspicious, .summary.quarantined, .summary.blocked] | add')
if [ "$ACTION_EVENTS" -gt 0 ]; then
OUTPUT+="🛡️ VT-Sentinel Activity Report\n===============================\n\n"
OUTPUT+="📊 Summary: ${ACTION_EVENTS} file events\n"
OUTPUT+=$(echo "$REPORT" | jq -r '
.summary |
(if .uploads > 0 then " ⬆️ Uploads initiated: \(.uploads)" else empty end),
(if .upload_complete > 0 then " ✅ Uploads completed: \(.upload_complete)" else empty end),
(if .upload_failed > 0 then " ❌ Upload failures: \(.upload_failed)" else empty end),
(if .cache_hits > 0 then " 💾 Cache hits: \(.cache_hits)" else empty end),
(if .verdicts.clean > 0 then " ✅ Clean verdicts: \(.verdicts.clean)" else empty end),
(if .verdicts.malicious > 0 then " 🚨 MALICIOUS: \(.verdicts.malicious)" else empty end),
(if .verdicts.suspicious > 0 then " ⚠️ SUSPICIOUS: \(.verdicts.suspicious)" else empty end),
(if .quarantined > 0 then " 📦 Quarantined: \(.quarantined)" else empty end),
(if .blocked > 0 then " 🚫 Blocked: \(.blocked)" else empty end)
')
OUTPUT+="\n\n📋 Event Details:\n"
OUTPUT+=$(echo "$REPORT" | jq -r '.events[] | select(.category != "lifecycle" and .category != "config") | " [\(.timestamp)] \(.category): \(.message)"')
if [ "$ALERTS" = "true" ]; then
OUTPUT+="\n\n⚠ SECURITY ALERT: Review malicious/suspicious/blocked events above!"
fi
fi
# --- VT scan thread creation for new uploads ---
PENDING_FILE="$STATE_DIR/pending-scans.json"
[ ! -f "$PENDING_FILE" ] || [ ! -s "$PENDING_FILE" ] && echo '[]' > "$PENDING_FILE"
SEEN_SCANS="$STATE_DIR/seen-scan-hashes.txt"
touch "$SEEN_SCANS"
UPLOADS=$(echo "$REPORT" | jq -r '[.events[] | select(.category == "upload")] | .[]' 2>/dev/null)
COMPLETES=$(echo "$REPORT" | jq -r '[.events[] | select(.category == "upload_complete")] | .[].message' 2>/dev/null)
if [ -n "$UPLOADS" ]; then
# Extract filenames + risk categories from upload events
echo "$REPORT" | jq -r '.events[] | select(.category == "upload") | .message' | while IFS= read -r msg; do
RISK_CAT=$(echo "$msg" | grep -oP 'Unknown \K[A-Z_]+' || echo "UNKNOWN")
FNAME=$(echo "$msg" | grep -oP 'file \K[^,]+' || echo "unknown")
[ -z "$FNAME" ] || [ "$FNAME" = "unknown" ] && continue
# Check if already tracked
grep -qF "$FNAME" "$SEEN_SCANS" 2>/dev/null && continue
# Find corresponding transaction ID from upload_complete events
HASH=""
if [ -n "$COMPLETES" ]; then
# Get first unmatched transaction ID, decode to extract hash
TXN_ID=$(echo "$COMPLETES" | grep -oP '\(\K[A-Za-z0-9+/=]+(?=\))' | head -1)
if [ -n "$TXN_ID" ]; then
HASH=$(echo "$TXN_ID" | base64 -d 2>/dev/null | cut -d: -f1)
fi
fi
# Create thread
RESULT=$("$SCRIPT_DIR/scan-thread.sh" "$FNAME" "$RISK_CAT" "${HASH:-pending}" 2>/dev/null)
THREAD_ID=$(echo "$RESULT" | jq -r '.threadId // empty')
if [ -n "$THREAD_ID" ] && [ -n "$HASH" ]; then
# Add to pending scans
PENDING=$(cat "$PENDING_FILE")
PENDING=$(echo "$PENDING" | jq --arg h "$HASH" --arg f "$FNAME" --arg t "$THREAD_ID" --arg r "$RISK_CAT" \
'. += [{"hash":$h,"filename":$f,"threadId":$t,"riskCategory":$r}]')
echo "$PENDING" > "$PENDING_FILE"
OUTPUT+="\n🛡 Created scan thread: [$RISK_CAT] $FNAME (hash: ${HASH:0:12}...)\n"
fi
echo "$FNAME" >> "$SEEN_SCANS"
done
fi
# --- Follow up on pending scans ---
PENDING_COUNT=$(jq 'length' "$PENDING_FILE" 2>/dev/null || echo 0)
if [ "$PENDING_COUNT" -gt 0 ]; then
FOLLOWUP=$("$SCRIPT_DIR/followup.sh" 2>/dev/null)
if [ -n "$FOLLOWUP" ]; then
OUTPUT+="\n$FOLLOWUP\n"
fi
fi
# --- Plugin update check (creates Discord forum threads) ---
# Filter: only plugins subsystem entries, strip ANSI + multibyte artifacts, deduplicate
PLUGIN_UPDATES=$(tail -c 5000000 "$LOG_FILE" 2>/dev/null \
| grep '"subsystem.*plugins"' \
| grep -oP 'Update available: [^"\\]+' \
| sed 's/\x1b\[[0-9;]*m//g; s/\[3[0-9]m//g' \
| tr -d '\r' \
| sort -u)
if [ -n "$PLUGIN_UPDATES" ]; then
while IFS= read -r line; do
[ -z "$line" ] && continue
grep -qF "$line" "$SEEN_FILE" 2>/dev/null && continue
# Parse: "Update available: 0.4.0 → 0.6.0. Run: openclaw plugins install plugin-name"
OLD_VER=$(echo "$line" | grep -oP 'Update available: \K[0-9.]+')
NEW_VER=$(echo "$line" | grep -oP '→ \K[0-9.]+')
INSTALL_CMD=$(echo "$line" | grep -oP 'Run: \K.*')
PLUGIN_NAME=$(echo "$INSTALL_CMD" | grep -oP 'install \K\S+')
if [ -n "$PLUGIN_NAME" ] && [ -n "$OLD_VER" ] && [ -n "$NEW_VER" ]; then
RESULT=$("$SCRIPT_DIR/plugin-update-thread.sh" "$PLUGIN_NAME" "$OLD_VER" "$NEW_VER" "$INSTALL_CMD" 2>/dev/null)
if echo "$RESULT" | jq -e '.ok == true' >/dev/null 2>&1; then
OUTPUT+="\n📦 Created thread for plugin update: ${PLUGIN_NAME} ${OLD_VER}${NEW_VER}\n"
else
OUTPUT+="\n📦 Plugin update: ${PLUGIN_NAME} ${OLD_VER}${NEW_VER} (thread creation failed)\n"
fi
fi
echo "$line" >> "$SEEN_FILE"
done <<< "$PLUGIN_UPDATES"
fi
# --- Output ---
if [ -z "$OUTPUT" ]; then
echo "NO_REPLY"
else
echo -e "$OUTPUT"
fi

View File

@@ -0,0 +1,106 @@
#!/bin/bash
# Follow up on pending VT scans — poll VT API, update Discord threads
# Usage: followup.sh
set -e
STATE_DIR="/home/node/.openclaw/workspace-security/memory"
PENDING_FILE="$STATE_DIR/pending-scans.json"
CHANNEL_ID="1470849667737714851"
# Initialize if missing
if [ ! -f "$PENDING_FILE" ] || [ ! -s "$PENDING_FILE" ]; then
echo '[]' > "$PENDING_FILE"
exit 0
fi
PENDING=$(cat "$PENDING_FILE")
COUNT=$(echo "$PENDING" | jq 'length')
[ "$COUNT" -eq 0 ] && exit 0
TOKEN=$(printenv DISCORD_BOT_TOKEN)
VT_KEY=$(printenv VIRUSTOTAL_API_KEY)
if [ -z "$TOKEN" ] || [ -z "$VT_KEY" ]; then
echo "Missing DISCORD_BOT_TOKEN or VIRUSTOTAL_API_KEY" >&2
exit 1
fi
UPDATED="[]"
RESOLVED=0
for i in $(seq 0 $((COUNT - 1))); do
ENTRY=$(echo "$PENDING" | jq ".[$i]")
HASH=$(echo "$ENTRY" | jq -r '.hash')
THREAD_ID=$(echo "$ENTRY" | jq -r '.threadId')
FILENAME=$(echo "$ENTRY" | jq -r '.filename')
RISK_CAT=$(echo "$ENTRY" | jq -r '.riskCategory')
# Query VT API
VT_RESULT=$(curl -s --max-time 10 -H "x-apikey: $VT_KEY" \
"https://www.virustotal.com/api/v3/files/$HASH" 2>/dev/null)
STATS=$(echo "$VT_RESULT" | jq '.data.attributes.last_analysis_stats // empty' 2>/dev/null)
if [ -z "$STATS" ] || [ "$STATS" = "null" ]; then
# Still pending or not found — keep in queue
UPDATED=$(echo "$UPDATED" | jq --argjson entry "$ENTRY" '. += [$entry]')
continue
fi
# Extract verdict
MALICIOUS=$(echo "$STATS" | jq '.malicious // 0')
SUSPICIOUS=$(echo "$STATS" | jq '.suspicious // 0')
UNDETECTED=$(echo "$STATS" | jq '.undetected // 0')
TOTAL=$((MALICIOUS + SUSPICIOUS + UNDETECTED))
TYPE_DESC=$(echo "$VT_RESULT" | jq -r '.data.attributes.type_description // "Unknown"')
SHA256=$(echo "$VT_RESULT" | jq -r '.data.attributes.sha256 // "unknown"')
VT_LINK="https://www.virustotal.com/gui/file/$SHA256"
# Determine verdict
if [ "$MALICIOUS" -gt 0 ]; then
VERDICT="🚨 MALICIOUS"
EMOJI="🚨"
VERDICT_SHORT="MALICIOUS ($MALICIOUS/$TOTAL)"
elif [ "$SUSPICIOUS" -gt 0 ]; then
VERDICT="⚠️ SUSPICIOUS"
EMOJI="⚠️"
VERDICT_SHORT="SUSPICIOUS ($SUSPICIOUS/$TOTAL)"
else
VERDICT="✅ CLEAN"
EMOJI="✅"
VERDICT_SHORT="CLEAN (0/$TOTAL)"
fi
# Post verdict to thread
MSG=$(printf '%s **Analysis Complete — %s**\n\n**File:** `%s`\n**Type:** %s\n**SHA-256:** `%s`\n\n**Results:**\n• Malicious: %s\n• Suspicious: %s\n• Undetected: %s engines\n\n**VT Link:** %s\n\n**Verdict:** %s' \
"$EMOJI" "$VERDICT_SHORT" "$FILENAME" "$TYPE_DESC" "$SHA256" \
"$MALICIOUS" "$SUSPICIOUS" "$UNDETECTED" "$VT_LINK" "$VERDICT")
curl -s -X POST \
-H "Authorization: Bot $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg content "$MSG" '{content: $content}')" \
"https://discord.com/api/v10/channels/$THREAD_ID/messages" > /dev/null
# Update thread title
NEW_TITLE=$(printf '[%s] %s — %s' "$RISK_CAT" "$FILENAME" "$VERDICT")
curl -s -X PATCH \
-H "Authorization: Bot $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg name "$NEW_TITLE" '{name: $name}')" \
"https://discord.com/api/v10/channels/$THREAD_ID" > /dev/null
RESOLVED=$((RESOLVED + 1))
echo "✅ Resolved: $FILENAME$VERDICT_SHORT"
# VT rate limit: 4 req/min on free tier, be conservative
sleep 1
done
# Save remaining pending scans
echo "$UPDATED" > "$PENDING_FILE"
if [ "$RESOLVED" -gt 0 ]; then
REMAINING=$(echo "$UPDATED" | jq 'length')
echo "Resolved $RESOLVED scan(s), $REMAINING still pending"
fi

View File

@@ -0,0 +1,51 @@
#!/bin/bash
# VT-Sentinel log splitter — extracts sentinel entries into dedicated log
# Usage: bash log-splitter.sh [start|stop|status]
set -eo pipefail
GATEWAY_LOG="/tmp/openclaw/openclaw.log"
VT_LOG="/tmp/openclaw/vt-sentinel.log"
PID_FILE="/tmp/openclaw/vt-log-splitter.pid"
PATTERN="VT-Sentinel\|vt-sentinel\|vtsentinel"
start() {
# Kill existing
stop 2>/dev/null || true
# Start tail from current end of file
nohup tail -F "$GATEWAY_LOG" 2>/dev/null \
| grep --line-buffered -i "$PATTERN" \
>> "$VT_LOG" 2>/dev/null &
echo $! > "$PID_FILE"
echo "Started (PID $(cat $PID_FILE)), writing to $VT_LOG"
}
stop() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
# Kill the tail pipeline (parent + children)
pkill -P "$PID" 2>/dev/null || true
kill "$PID" 2>/dev/null || true
rm -f "$PID_FILE"
echo "Stopped"
else
echo "Not running"
fi
}
status() {
if [ -f "$PID_FILE" ] && kill -0 "$(cat $PID_FILE)" 2>/dev/null; then
echo "Running (PID $(cat $PID_FILE))"
[ -f "$VT_LOG" ] && echo "Log size: $(du -h "$VT_LOG" | cut -f1), $(wc -l < "$VT_LOG") lines"
else
echo "Not running"
rm -f "$PID_FILE" 2>/dev/null
fi
}
case "${1:-start}" in
start) start ;;
stop) stop ;;
status) status ;;
*) echo "Usage: $0 [start|stop|status]" ;;
esac

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Create Discord forum thread for a plugin update notification
# Usage: plugin-update-thread.sh "plugin-name" "old_version" "new_version" "install_cmd"
set -e
PLUGIN="$1"
OLD_VER="$2"
NEW_VER="$3"
INSTALL_CMD="$4"
CHANNEL_ID="1470849667737714851"
TOKEN=$(printenv DISCORD_BOT_TOKEN)
if [ -z "$TOKEN" ]; then
echo '{"error":"DISCORD_BOT_TOKEN not set"}'
exit 1
fi
# Build content with real newlines using printf
CONTENT=$(printf '📦 **Plugin Update Available**\n\n**Plugin:** `%s`\n**Current:** %s\n**Available:** %s\n\n**Install:**\n```\n%s\n```\n\n---\n*Detected by VT-Sentinel Monitor*' \
"$PLUGIN" "$OLD_VER" "$NEW_VER" "$INSTALL_CMD")
THREAD_NAME=$(printf '📦 %s — %s → %s' "$PLUGIN" "$OLD_VER" "$NEW_VER")
# Use jq to properly encode the content with real newlines
PAYLOAD=$(jq -n \
--arg name "$THREAD_NAME" \
--arg content "$CONTENT" \
'{name: $name, message: {content: $content}, auto_archive_duration: 1440}')
RESULT=$(curl -s -X POST \
-H "Authorization: Bot $TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://discord.com/api/v10/channels/${CHANNEL_ID}/threads")
THREAD_ID=$(echo "$RESULT" | jq -r '.id // empty')
if [ -n "$THREAD_ID" ]; then
echo "{\"ok\":true,\"threadId\":\"$THREAD_ID\",\"plugin\":\"$PLUGIN\"}"
else
echo "{\"ok\":false,\"error\":$(echo "$RESULT" | jq -c '.')}"
fi

View File

@@ -0,0 +1,35 @@
#!/bin/bash
# Create a Discord forum thread for a VT file scan event
# Usage: scan-thread.sh "filename" "risk_category" ["hash"]
set -e
FILENAME="$1"
RISK_CAT="$2"
HASH="${3:-unknown}"
CHANNEL_ID="1470849667737714851"
TOKEN=$(printenv DISCORD_BOT_TOKEN)
[ -z "$TOKEN" ] && echo '{"ok":false,"error":"no token"}' && exit 1
CONTENT=$(printf '🛡️ **VT-Sentinel File Scan**\n\n**File:** `%s`\n**Category:** %s\n**Status:** ⏳ PENDING — uploaded to VirusTotal for analysis\n**Hash:** `%s`\n\n---\n*Will update when verdict is available.*' \
"$FILENAME" "$RISK_CAT" "$HASH")
THREAD_NAME=$(printf '[%s] %s — ⏳ PENDING' "$RISK_CAT" "$FILENAME")
PAYLOAD=$(jq -n \
--arg name "$THREAD_NAME" \
--arg content "$CONTENT" \
'{name: $name, message: {content: $content}, auto_archive_duration: 1440}')
RESULT=$(curl -s -X POST \
-H "Authorization: Bot $TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://discord.com/api/v10/channels/${CHANNEL_ID}/threads")
THREAD_ID=$(echo "$RESULT" | jq -r '.id // empty')
if [ -n "$THREAD_ID" ]; then
echo "{\"ok\":true,\"threadId\":\"$THREAD_ID\"}"
else
echo "{\"ok\":false,\"error\":$(echo "$RESULT" | jq -c '.')}"
fi

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Tail recent VT-Sentinel log entries (last N from recent log tail)
# Usage: tail.sh [count]
set -e
COUNT="${1:-50}"
LOG_FILE="/tmp/openclaw/openclaw.log"
if [ ! -f "$LOG_FILE" ]; then
echo '{"error":"Gateway log not found"}'
exit 1
fi
# Read last 5MB, grep VT-Sentinel, take last N
tail -c 5000000 "$LOG_FILE" | grep '"subsystem.*gateway"' | grep '\[VT-Sentinel\]' | tail -n "$COUNT" | while IFS= read -r line; do
TIMESTAMP=$(echo "$line" | grep -oP '"date":"\K[^"]*' | head -1)
MESSAGE=$(echo "$line" | grep -oP '\[VT-Sentinel\] \K[^"\\]*' | head -1)
[ -n "$TIMESTAMP" ] && [ -n "$MESSAGE" ] && \
jq -cn --arg ts "$TIMESTAMP" --arg msg "$MESSAGE" '{"timestamp":$ts,"message":$msg}'
done | jq -s '{count:length, entries:.}'