Skill: pors/clawdbot-release-check (version 1.0.1)

--- name: clawdbot-release-check description: Check for new clawdbot releases and notify once per new version. homepage: https://github.com/clawdbot/clawdbot metadata: {"clawdbot":{"emoji":"🔄","requires":{"bins":["curl","jq"]}}} --- # Clawdbot Release Check Checks for new clawdbot releases from GitHub and notifies you once per version. No nagging. ## Installation ```bash clawdhub install clawdbot-release-check ``` ## Quick Setup (with cron) ```bash # Add daily update check at 9am, notify via Telegram {baseDir}/scripts/setup.sh --telegram YOUR_TELEGRAM_ID # Custom hour (e.g., 8am) {baseDir}/scripts/setup.sh --hour 8 --telegram YOUR_TELEGRAM_ID # Remove cron job {baseDir}/scripts/setup.sh --uninstall ``` After setup, restart the gateway: ```bash launchctl kickstart -k gui/$(id -u)/com.clawdis.gateway ``` ## Manual Usage ```bash # Check for updates (silent if up-to-date or already notified) {baseDir}/scripts/check.sh # Show version info {baseDir}/scripts/check.sh --status # Force notification (bypass "already notified" state) {baseDir}/scripts/check.sh --force # Show highlights from ALL missed releases {baseDir}/scripts/check.sh --all-highlights # Clear state (will notify again on next check) {baseDir}/scripts/check.sh --reset # Help {baseDir}/scripts/check.sh --help ``` ## How It Works 1. Fetches latest release from `github.com/clawdbot/clawdbot/releases` 2. Compares with your installed version (from `package.json`) 3. If behind, shows highlights from release notes 4. Saves state to prevent repeat notifications ## Example Output ``` 🔄 **Clawdbot Update Available!** Current: `2.0.0-beta5` Latest: `2026.1.5-3` _(3 versions behind)_ **Highlights:** - Models: add image-specific model config - Agent tools: new `image` tool - Config: default model shorthands 🔗 https://github.com/clawdbot/clawdbot/releases/tag/v2026.1.5-3 To update: `cd /path/to/clawdis && git pull && pnpm install && pnpm build` ``` ## Files **State** — `~/.clawdbot/clawdbot-release-check-state.json`: ```json { "lastNotifiedVersion": "v2026.1.5-3", "lastCheckMs": 1704567890123 } ``` **Cache** — `~/.clawdbot/clawdbot-release-check-cache.json`: - Release data cached for 24 hours (saves API calls) - Highlights extracted once per release (saves tokens) - Use `--clear-cache` to force refresh ## Configuration Environment variables: - `CLAWDBOT_DIR` — Path to clawdbot source (auto-detected from `~/dev/clawdis`, `~/clawdbot`, or npm global) - `CACHE_MAX_AGE_HOURS` — Cache TTL in hours (default: 24)

path: scripts/check.sh
content: #!/usr/bin/env bash
set -uo pipefail

# Try to find clawdbot source directory
if [[ -n "${CLAWDBOT_DIR:-}" ]]; then
  CLAWDBOT_DIR="$CLAWDBOT_DIR"
elif [[ -f "${HOME}/dev/clawdis/package.json" ]]; then
  CLAWDBOT_DIR="${HOME}/dev/clawdis"
elif [[ -f "${HOME}/clawdbot/package.json" ]]; then
  CLAWDBOT_DIR="${HOME}/clawdbot"
else
  # Fallback: find via npm
  CLAWDBOT_DIR=$(npm root -g 2>/dev/null)/clawdbot
fi
STATE_DIR="${HOME}/.clawdbot"
STATE_FILE="${STATE_DIR}/clawdbot-release-check-state.json"
CACHE_FILE="${STATE_DIR}/clawdbot-release-check-cache.json"
RELEASES_URL="https://api.github.com/repos/clawdbot/clawdbot/releases"
CACHE_MAX_AGE_HOURS="${CACHE_MAX_AGE_HOURS:-24}"

usage() {
  cat <<'EOF'
Clawdbot Release Checker

Checks for new clawdbot releases and notifies once per version.

Usage: check.sh [OPTIONS]

Options:
  --status          Show current vs latest version (no notification)
  --force           Show full notification even if already notified
  --all-highlights  Show highlights from ALL missed releases
  --reset           Clear notification state (will notify again on next run)
  --clear-cache     Clear cached release data
  -h, --help        Show this help

Examples:
  check.sh                  # Silent if up-to-date or already notified
  check.sh --status         # Quick version check
  check.sh --force          # Force fetch and show notification
  check.sh --all-highlights # See highlights from all missed versions

Cache: Release data cached for 24h. Highlights extracted once per release.
EOF
  exit 0
}

force=false
status_only=false
all_highlights=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --force) force=true; shift ;;
    --status) status_only=true; shift ;;
    --all-highlights) all_highlights=true; force=true; shift ;;
    --reset) rm -f "$STATE_FILE"; echo "State cleared."; exit 0 ;;
    --clear-cache) rm -f "$CACHE_FILE"; echo "Cache cleared."; exit 0 ;;
    -h|--help) usage ;;
    *) shift ;;
  esac
done

# Ensure state directory exists
mkdir -p "$STATE_DIR"

# Get current installed version from package.json
current_version=$(jq -r '.version' "$CLAWDBOT_DIR/package.json" 2>/dev/null || echo "unknown")

# Check if cache is valid
use_cache=false
if [[ -f "$CACHE_FILE" && "$force" == "false" ]]; then
  cache_time=$(jq -r '.fetchedAtMs // 0' "$CACHE_FILE" 2>/dev/null)
  now_ms=$(date +%s)000
  cache_age_hours=$(( (now_ms - cache_time) / 1000 / 3600 ))
  if [[ $cache_age_hours -lt $CACHE_MAX_AGE_HOURS ]]; then
    use_cache=true
  fi
fi

# Fetch or use cache
if [[ "$use_cache" == "true" ]]; then
  releases_json=$(jq '.releases' "$CACHE_FILE" 2>/dev/null)
else
  # Fetch from GitHub
  releases_json=$(curl -sS "$RELEASES_URL" 2>/dev/null)
  if [[ -n "$releases_json" && "$releases_json" != "null" ]]; then
    # Save to cache
    now_ms=$(date +%s)000
    jq -n --argjson releases "$releases_json" --argjson time "$now_ms" \
      '{fetchedAtMs: $time, releases: $releases, highlights: {}}' > "$CACHE_FILE"
  fi
fi

if [[ -z "$releases_json" || "$releases_json" == "null" ]]; then
  exit 0
fi

# Get latest release info
latest_tag=$(echo "$releases_json" | jq -r '.[0].tag_name // empty')
release_url=$(echo "$releases_json" | jq -r '.[0].html_url // empty')

if [[ -z "$latest_tag" ]]; then
  exit 0
fi

latest_version="${latest_tag#v}"

# Function to get highlights for a release (with caching)
get_highlights() {
  local tag="$1"
  local body="$2"
  
  # Check cache for pre-computed highlights
  if [[ -f "$CACHE_FILE" ]]; then
    cached=$(jq -r --arg tag "$tag" '.highlights[$tag] // empty' "$CACHE_FILE" 2>/dev/null)
    if [[ -n "$cached" ]]; then
      echo "$cached"
      return
    fi
  fi
  
  # Extract highlights
  local hl=""
  if echo "$body" | grep -qi "Highlights"; then
    hl=$(echo "$body" | sed -n '/[Hh]ighlights/,/^##[^#]/p' | head -8 | sed '/^##[^#]/d' | sed 's/^### //' | sed 's/^## //')
  else
    hl=$(echo "$body" | grep -v "^$" | head -6 | sed 's/^### //' | sed 's/^## //')
  fi
  
  # Save to cache
  if [[ -n "$hl" && -f "$CACHE_FILE" ]]; then
    local escaped_hl
    escaped_hl=$(echo "$hl" | jq -Rs .)
    jq --arg tag "$tag" --argjson hl "$escaped_hl" '.highlights[$tag] = $hl' "$CACHE_FILE" > /tmp/cache-update.json
    mv /tmp/cache-update.json "$CACHE_FILE"
  fi
  
  echo "$hl"
}

if [[ "$status_only" == "true" ]]; then
  echo "Current: $current_version"
  echo "Latest:  $latest_version (tag: $latest_tag)"
  echo "Release: $release_url"
  if [[ -f "$STATE_FILE" ]]; then
    last_notified=$(jq -r '.lastNotifiedVersion // "none"' "$STATE_FILE" 2>/dev/null)
    echo "Last notified: $last_notified"
  fi
  if [[ -f "$CACHE_FILE" ]]; then
    cache_time=$(jq -r '.fetchedAtMs // 0' "$CACHE_FILE" 2>/dev/null)
    cached_count=$(jq '.highlights | keys | length' "$CACHE_FILE" 2>/dev/null)
    echo "Cache: $cached_count highlights cached"
  fi
  exit 0
fi

# Compare versions (semver-style: major.minor.patch)
version_gt() {
  # Returns 0 (true) if $1 > $2
  local IFS='.'
  read -ra v1 <<< "$1"
  read -ra v2 <<< "$2"
  for i in 0 1 2; do
    local n1=${v1[i]:-0}
    local n2=${v2[i]:-0}
    if (( n1 > n2 )); then return 0; fi
    if (( n1 < n2 )); then return 1; fi
  done
  return 1  # Equal
}

# Exit silently if current >= latest
if [[ "$current_version" == "$latest_version" ]]; then
  exit 0
fi

# Exit silently if running ahead of latest (dev build)
if version_gt "$current_version" "$latest_version"; then
  exit 0
fi

# We're behind - check if already notified
if [[ "$force" == "false" && -f "$STATE_FILE" ]]; then
  last_notified=$(jq -r '.lastNotifiedVersion // ""' "$STATE_FILE" 2>/dev/null)
  if [[ "$last_notified" == "$latest_tag" ]]; then
    exit 0
  fi
fi

# Count how many releases behind
versions_behind=0
found_current=false
while IFS= read -r tag; do
  if [[ "${tag#v}" == "$current_version" ]]; then
    found_current=true
    break
  fi
  ((versions_behind++))
done <<< "$(echo "$releases_json" | jq -r '.[].tag_name')"

if [[ "$found_current" == "false" ]]; then
  versions_behind="several"
fi

# Build notification message
cat <<EOF
🔄 **Clawdbot Update Available!**

Current: \`$current_version\`
Latest:  \`$latest_version\`
EOF

if [[ "$versions_behind" != "0" && "$versions_behind" != "1" ]]; then
  echo ""
  echo "_($versions_behind versions behind)_"
fi

if [[ "$all_highlights" == "true" ]]; then
  echo ""
  echo "**Highlights from all missed releases:**"
  
  echo "$releases_json" | jq -c '.[]' 2>/dev/null | while read -r release; do
    tag=$(echo "$release" | jq -r '.tag_name')
    body=$(echo "$release" | jq -r '.body // ""')
    tag_version="${tag#v}"
    
    if [[ "$tag_version" == "$current_version" ]]; then
      break
    fi
    
    hl=$(get_highlights "$tag" "$body")
    if [[ -n "$hl" && "$hl" != "null" ]]; then
      echo ""
      echo "📦 **$tag**"
      echo "$hl"
    fi
  done
else
  release_body=$(echo "$releases_json" | jq -r '.[0].body // empty')
  if [[ -n "$release_body" ]]; then
    highlights=$(get_highlights "$latest_tag" "$release_body")
    if [[ -n "$highlights" ]]; then
      # Truncate if too long
      if [[ ${#highlights} -gt 500 ]]; then
        highlights="${highlights:0:500}..."
      fi
      cat <<EOF

**Highlights:**
$highlights
EOF
    fi
  fi
fi

echo ""
echo "🔗 $release_url"
echo ""

# Show appropriate update command
if [[ -d "$CLAWDBOT_DIR/.git" ]]; then
  echo "To update: \`cd $CLAWDBOT_DIR && git pull && pnpm install && pnpm build\`"
else
  echo "To update: \`npm update -g clawdbot\`"
fi

# Save state
now_ms=$(date +%s)000
jq -n --arg v "$latest_tag" --arg t "$now_ms" \
  '{lastNotifiedVersion: $v, lastCheckMs: ($t | tonumber)}' > "$STATE_FILE"


path: scripts/setup.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Default values
CRON_HOUR="${UPDATE_CHECK_HOUR:-9}"
TELEGRAM_TO="${TELEGRAM_TO:-}"
CHANNEL="${CHANNEL:-telegram}"

usage() {
  cat <<'EOF'
Setup Clawdbot Release Check cron job

Usage: setup.sh [OPTIONS]

Options:
  --hour <0-23>       Hour to run daily check (default: 9)
  --telegram <id>     Telegram user/group ID to notify
  --channel <name>    Channel: telegram|whatsapp|discord (default: telegram)
  --uninstall         Remove the cron job
  -h, --help          Show this help

Examples:
  setup.sh --telegram 88230176              # Daily at 9am to Telegram
  setup.sh --hour 8 --telegram 88230176     # Daily at 8am
  setup.sh --uninstall                      # Remove cron job
EOF
  exit 0
}

uninstall=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --hour) CRON_HOUR="$2"; shift 2 ;;
    --telegram) TELEGRAM_TO="$2"; CHANNEL="telegram"; shift 2 ;;
    --channel) CHANNEL="$2"; shift 2 ;;
    --uninstall) uninstall=true; shift ;;
    -h|--help) usage ;;
    *) echo "Unknown: $1"; usage ;;
  esac
done

CRON_FILE="${HOME}/.clawdbot/cron/jobs.json"
JOB_NAME="Clawdbot Release Check"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

if [[ "$uninstall" == "true" ]]; then
  if [[ -f "$CRON_FILE" ]]; then
    jq --arg name "$JOB_NAME" '.jobs = [.jobs[] | select(.name != $name)]' "$CRON_FILE" > /tmp/cron-updated.json
    mv /tmp/cron-updated.json "$CRON_FILE"
    echo "✓ Removed '$JOB_NAME' cron job"
  else
    echo "No cron file found"
  fi
  exit 0
fi

if [[ -z "$TELEGRAM_TO" && "$CHANNEL" == "telegram" ]]; then
  echo "Error: --telegram <id> required"
  echo "Run with --help for usage"
  exit 1
fi

# Check if job already exists
if [[ -f "$CRON_FILE" ]] && jq -e --arg name "$JOB_NAME" '.jobs[] | select(.name == $name)' "$CRON_FILE" >/dev/null 2>&1; then
  echo "Job '$JOB_NAME' already exists. Use --uninstall first to replace."
  exit 1
fi

# Ensure cron directory exists
mkdir -p "$(dirname "$CRON_FILE")"

# Create jobs file if it doesn't exist
if [[ ! -f "$CRON_FILE" ]]; then
  echo '{"version":1,"jobs":[]}' > "$CRON_FILE"
fi

# Generate job ID
JOB_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
NOW_MS=$(date +%s)000

# Add the job
jq --arg id "$JOB_ID" \
   --arg name "$JOB_NAME" \
   --arg hour "$CRON_HOUR" \
   --arg channel "$CHANNEL" \
   --arg to "$TELEGRAM_TO" \
   --arg script "$SCRIPT_DIR/check.sh" \
   --argjson now "$NOW_MS" \
   '.jobs += [{
     "id": $id,
     "name": $name,
     "enabled": true,
     "createdAtMs": $now,
     "updatedAtMs": $now,
     "schedule": {
       "kind": "cron",
       "expr": ("0 " + $hour + " * * *")
     },
     "sessionTarget": "isolated",
     "wakeMode": "now",
     "payload": {
       "kind": "agentTurn",
       "message": ("UPDATE_CHECK: Run " + $script + " and if there is output, send it to the user."),
       "deliver": true,
       "channel": $channel,
       "to": $to
     },
     "isolation": {
       "postToMainPrefix": "System"
     },
     "state": {}
   }]' "$CRON_FILE" > /tmp/cron-updated.json

mv /tmp/cron-updated.json "$CRON_FILE"

echo "✓ Added '$JOB_NAME' cron job"
echo "  Schedule: Daily at ${CRON_HOUR}:00"
echo "  Channel: $CHANNEL → $TELEGRAM_TO"
echo ""
echo "Restart gateway to apply: launchctl kickstart -k gui/\$(id -u)/com.clawdis.gateway"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)