#!/bin/bash # Daily auto-update: OpenClaw, Claude Code, OS packages # Runs at 9:00 AM ET via systemd timer # Logs results to memory/updates/ for morning briefing set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" WORKSPACE="$(dirname "$SCRIPT_DIR")" LOG_DIR="$WORKSPACE/memory/updates" DATE=$(date +%Y-%m-%d) LOG="$LOG_DIR/$DATE.json" mkdir -p "$LOG_DIR" # Initialize log cat > "$LOG" <<'EOF' { "date": "DATE_PLACEHOLDER", "timestamp": "TS_PLACEHOLDER", "openclaw": {}, "claude_code": {}, "os": {}, "mission_control": {}, "caddy_pi": {}, "gateway_restarted": false } EOF sed -i "s/DATE_PLACEHOLDER/$DATE/" "$LOG" sed -i "s/TS_PLACEHOLDER/$(date -Iseconds)/" "$LOG" update_json() { local key="$1" value="$2" python3 << PYEOF import json with open("$LOG") as f: d = json.load(f) keys = "$key".split(".") obj = d for k in keys[:-1]: obj = obj[k] raw = '''$value''' # Try parsing as JSON first (handles strings, arrays, numbers, booleans) try: obj[keys[-1]] = json.loads(raw) except (json.JSONDecodeError, ValueError): obj[keys[-1]] = raw with open("$LOG", "w") as f: json.dump(d, f, indent=2) PYEOF } echo "=== Daily Update Check: $DATE ===" # --- OpenClaw --- echo "" echo "--- OpenClaw ---" OC_BEFORE=$(openclaw --version 2>/dev/null | head -1 || echo "unknown") OC_LATEST=$(npm show openclaw version 2>/dev/null || echo "unknown") echo "Current: $OC_BEFORE | Latest: $OC_LATEST" update_json "openclaw.before" "\"$OC_BEFORE\"" update_json "openclaw.latest" "\"$OC_LATEST\"" # Use LLM to decide if these refer to the same version — no fragile string parsing OC_SAME=$(claude --print --permission-mode bypassPermissions "Do these two strings refer to the same software version? Answer only 'yes' or 'no'. String 1: '$OC_BEFORE' String 2: '$OC_LATEST'" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') if [ "$OC_SAME" = "yes" ]; then update_json "openclaw.updated" "false" echo "Up to date: $OC_BEFORE" else echo "Update available, running npm update -g openclaw..." if npm update -g openclaw 2>&1; then OC_AFTER=$(openclaw --version 2>/dev/null | head -1 || echo "unknown") update_json "openclaw.after" "\"$OC_AFTER\"" update_json "openclaw.updated" "true" echo "Updated: $OC_BEFORE → $OC_AFTER" /home/johan/clawd/scripts/openclaw-post-update-patches.sh 2>&1 else update_json "openclaw.updated" "false" update_json "openclaw.error" "\"npm update failed\"" echo "Update failed" fi fi # --- Claude Code --- echo "" echo "--- Claude Code ---" CC_BEFORE=$(claude --version 2>/dev/null | sed 's/ (Claude Code)//' || echo "unknown") CC_LATEST=$(npm show @anthropic-ai/claude-code version 2>/dev/null || echo "unknown") echo "Current: $CC_BEFORE | Latest: $CC_LATEST" update_json "claude_code.before" "\"$CC_BEFORE\"" update_json "claude_code.latest" "\"$CC_LATEST\"" CC_SAME=$(claude --print --permission-mode bypassPermissions "Do these two strings refer to the same software version? Answer only 'yes' or 'no'. String 1: '$CC_BEFORE' String 2: '$CC_LATEST'" 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') if [ "$CC_SAME" = "yes" ]; then update_json "claude_code.updated" "false" echo "Up to date: $CC_BEFORE" else echo "Update available, running npm update -g @anthropic-ai/claude-code..." if npm update -g @anthropic-ai/claude-code 2>&1; then CC_AFTER=$(claude --version 2>/dev/null | sed 's/ (Claude Code)//' || echo "unknown") update_json "claude_code.after" "\"$CC_AFTER\"" update_json "claude_code.updated" "true" echo "Updated: $CC_BEFORE → $CC_AFTER" else update_json "claude_code.updated" "false" update_json "claude_code.error" "\"npm update failed\"" echo "Update failed" fi fi # --- OS Packages --- echo "" echo "--- OS Packages ---" # Capture upgradable list before updating sudo apt-get update -qq 2>/dev/null UPGRADABLE=$(apt list --upgradable 2>/dev/null | grep -v "^Listing" || true) PKG_COUNT=$(echo "$UPGRADABLE" | grep -c . || echo "0") update_json "os.available" "$PKG_COUNT" if [ "$PKG_COUNT" -gt 0 ]; then echo "$PKG_COUNT packages upgradable" # Capture package names and versions PKG_LIST=$(echo "$UPGRADABLE" | head -50 | python3 -c " import sys, json pkgs = [] for line in sys.stdin: line = line.strip() if not line: continue parts = line.split('/') if len(parts) >= 2: name = parts[0] rest = '/'.join(parts[1:]) # Extract version info ver_parts = rest.split(' ') new_ver = ver_parts[1] if len(ver_parts) > 1 else 'unknown' old_ver = ver_parts[-1].strip('[]') if '[' in rest else 'unknown' pkgs.append({'name': name, 'from': old_ver, 'to': new_ver}) print(json.dumps(pkgs)) " 2>/dev/null || echo "[]") update_json "os.packages" "$PKG_LIST" echo "Upgrading..." if sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq 2>&1 | tail -5; then update_json "os.updated" "true" echo "OS packages updated" # Check if reboot required if [ -f /var/run/reboot-required ]; then update_json "os.reboot_required" "true" echo "⚠️ Reboot required!" else update_json "os.reboot_required" "false" fi else update_json "os.updated" "false" update_json "os.error" "\"apt upgrade failed\"" fi else update_json "os.updated" "false" update_json "os.packages" "[]" echo "All packages up to date" fi # --- Mission Control --- echo "" echo "--- Mission Control ---" MC_DIR="/home/johan/mission-control" MC_BEFORE=$(cd "$MC_DIR" && git describe --tags --abbrev=0 2>/dev/null || git rev-parse --short HEAD) MC_LATEST=$(cd "$MC_DIR" && git fetch --tags -q origin 2>/dev/null && git describe --tags --abbrev=0 origin/main 2>/dev/null || echo "unknown") echo "Current: $MC_BEFORE | Latest: $MC_LATEST" update_json "mission_control.before" "\"$MC_BEFORE\"" update_json "mission_control.latest" "\"$MC_LATEST\"" if [ "$MC_BEFORE" = "$MC_LATEST" ]; then update_json "mission_control.updated" "false" echo "Up to date: $MC_BEFORE" else echo "Update available ($MC_BEFORE → $MC_LATEST), pulling and rebuilding..." if cd "$MC_DIR" && git pull --ff-only origin main 2>&1 && npm install --legacy-peer-deps 2>&1 | tail -3 && npm run build 2>&1 | tail -5; then MC_AFTER=$(cd "$MC_DIR" && git describe --tags --abbrev=0 2>/dev/null || git rev-parse --short HEAD) update_json "mission_control.after" "\"$MC_AFTER\"" update_json "mission_control.updated" "true" systemctl --user restart mission-control echo "Updated: $MC_BEFORE → $MC_AFTER, service restarted" else update_json "mission_control.updated" "false" update_json "mission_control.error" "\"pull or build failed\"" echo "Update failed — MC unchanged" fi fi # --- Gateway Restart (only if OpenClaw updated) --- echo "" OC_UPDATED=$(python3 -c "import json; print(json.load(open('$LOG'))['openclaw'].get('updated', False))") if [ "$OC_UPDATED" = "True" ]; then echo "OpenClaw was updated — restarting gateway..." systemctl --user restart openclaw-gateway update_json "gateway_restarted" "true" echo "Gateway restarted" else echo "No gateway restart needed" fi echo "" echo "=== Update complete. Log: $LOG ===" # --- Caddy Pi (192.168.0.2) --- echo "" echo "--- Caddy Pi ---" CADDY_RESULT=$(ssh -o ConnectTimeout=10 -o BatchMode=yes root@192.168.0.2 " apt-get update -qq 2>/dev/null UPGRADABLE=\$(apt list --upgradable 2>/dev/null | grep -v Listing | wc -l) if [ \"\$UPGRADABLE\" -gt 0 ]; then DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq 2>&1 | tail -3 echo \"upgraded:\$UPGRADABLE\" else echo \"upgraded:0\" fi # Commit and push any Caddyfile changes cd /etc/caddy if ! git diff --quiet HEAD Caddyfile 2>/dev/null; then git add Caddyfile && git commit -m \"auto: Caddyfile update \$(date +%Y-%m-%d)\" && git push echo 'caddyfile:committed' else echo 'caddyfile:unchanged' fi # Reboot if needed [ -f /var/run/reboot-required ] && echo 'reboot:required' || echo 'reboot:no' " 2>/dev/null || echo "ssh:failed") echo "Caddy Pi: $CADDY_RESULT" update_json "caddy_pi.result" "\"$CADDY_RESULT\""