ScuttleBot

scuttlebot / skills / scuttlebot-relay / hooks / scuttlebot-check.sh
Source Blame History 139 lines
016a29f… lmata 1 #!/bin/bash
016a29f… lmata 2 # PreToolUse hook — checks IRC for human instructions before each tool call.
016a29f… lmata 3 # Only messages that explicitly mention this session nick are surfaced back into
016a29f… lmata 4 # the agent loop as a blocking instruction.
016a29f… lmata 5
016a29f… lmata 6 SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
016a29f… lmata 7 if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then
016a29f… lmata 8 set -a
016a29f… lmata 9 . "$SCUTTLEBOT_CONFIG_FILE"
016a29f… lmata 10 set +a
016a29f… lmata 11 fi
1d3caa2… lmata 12 if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then
1d3caa2… lmata 13 set -a
1d3caa2… lmata 14 . "$SCUTTLEBOT_CHANNEL_STATE_FILE"
1d3caa2… lmata 15 set +a
1d3caa2… lmata 16 fi
016a29f… lmata 17
016a29f… lmata 18 SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}"
016a29f… lmata 19 SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}"
016a29f… lmata 20 SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}"
016a29f… lmata 21 SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}"
016a29f… lmata 22
016a29f… lmata 23 input=$(cat)
016a29f… lmata 24 session_id=$(echo "$input" | jq -r '.session_id // empty' 2>/dev/null | head -c 8)
016a29f… lmata 25
016a29f… lmata 26 sanitize() {
016a29f… lmata 27 printf '%s' "$1" | tr -cs '[:alnum:]_-' '-'
016a29f… lmata 28 }
016a29f… lmata 29
1d3caa2… lmata 30 normalize_channel() {
1d3caa2… lmata 31 local channel="$1"
1d3caa2… lmata 32 channel="${channel//[$' \t\r\n']/}"
1d3caa2… lmata 33 channel="${channel#\#}"
1d3caa2… lmata 34 printf '%s' "$channel"
1d3caa2… lmata 35 }
1d3caa2… lmata 36
1d3caa2… lmata 37 relay_channels() {
1d3caa2… lmata 38 local raw="${SCUTTLEBOT_CHANNELS:-$SCUTTLEBOT_CHANNEL}"
1d3caa2… lmata 39 local IFS=','
1d3caa2… lmata 40 local item channel seen=""
1d3caa2… lmata 41 read -r -a items <<< "$raw"
1d3caa2… lmata 42 for item in "${items[@]}"; do
1d3caa2… lmata 43 channel=$(normalize_channel "$item")
1d3caa2… lmata 44 [ -n "$channel" ] || continue
1d3caa2… lmata 45 case ",$seen," in
1d3caa2… lmata 46 *,"$channel",*) ;;
1d3caa2… lmata 47 *)
1d3caa2… lmata 48 seen="${seen:+$seen,}$channel"
1d3caa2… lmata 49 printf '%s\n' "$channel"
1d3caa2… lmata 50 ;;
1d3caa2… lmata 51 esac
1d3caa2… lmata 52 done
1d3caa2… lmata 53 }
1d3caa2… lmata 54
1d3caa2… lmata 55 contains_mention() {
1d3caa2… lmata 56 local text="$1"
1d3caa2… lmata 57 [[ "$text" =~ (^|[^[:alnum:]_./\\-])$SCUTTLEBOT_NICK($|[^[:alnum:]_./\\-]) ]]
1d3caa2… lmata 58 }
1d3caa2… lmata 59
87e6978… lmata 60 epoch_millis() {
87e6978… lmata 61 local at="$1" ts_secs ts_frac ts_clean frac
87e6978… lmata 62 ts_frac=$(printf '%s' "$at" | grep -oE '\.[0-9]+' | head -1)
87e6978… lmata 63 ts_clean=$(printf '%s' "$at" | sed 's/\.[0-9]*//' | sed 's/\([+-][0-9][0-9]\):\([0-9][0-9]\)$/\1\2/')
87e6978… lmata 64 ts_secs=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$ts_clean" "+%s" 2>/dev/null || \
87e6978… lmata 65 date -d "$ts_clean" "+%s" 2>/dev/null)
87e6978… lmata 66 [ -n "$ts_secs" ] || return
87e6978… lmata 67 if [ -n "$ts_frac" ]; then
87e6978… lmata 68 frac="${ts_frac#.}000"
87e6978… lmata 69 printf '%s%s' "$ts_secs" "${frac:0:3}"
87e6978… lmata 70 else
87e6978… lmata 71 printf '%s000' "$ts_secs"
87e6978… lmata 72 fi
87e6978… lmata 73 }
87e6978… lmata 74
87e6978… lmata 75 now_millis() {
87e6978… lmata 76 python3 -c "import time; print(int(time.time()*1000))" 2>/dev/null || \
87e6978… lmata 77 date +%s%3N 2>/dev/null || \
87e6978… lmata 78 printf '%s000' "$(date +%s)"
1d3caa2… lmata 79 }
1d3caa2… lmata 80
016a29f… lmata 81 cwd=$(echo "$input" | jq -r '.cwd // empty' 2>/dev/null)
016a29f… lmata 82 if [ -z "$cwd" ]; then cwd=$(pwd); fi
016a29f… lmata 83 base_name=$(sanitize "$(basename "$cwd")")
016a29f… lmata 84 session_suffix="${session_id:-$PPID}"
016a29f… lmata 85 default_nick="claude-${base_name}-${session_suffix}"
016a29f… lmata 86 SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}"
016a29f… lmata 87
016a29f… lmata 88 [ "$SCUTTLEBOT_HOOKS_ENABLED" = "0" ] && exit 0
016a29f… lmata 89 [ "$SCUTTLEBOT_HOOKS_ENABLED" = "false" ] && exit 0
016a29f… lmata 90 [ -z "$SCUTTLEBOT_TOKEN" ] && exit 0
016a29f… lmata 91
1d3caa2… lmata 92 state_key=$(printf '%s' "$SCUTTLEBOT_NICK|$(pwd)" | cksum | awk '{print $1}')
016a29f… lmata 93 LAST_CHECK_FILE="/tmp/.scuttlebot-last-check-$state_key"
016a29f… lmata 94
016a29f… lmata 95 last_check=0
016a29f… lmata 96 if [ -f "$LAST_CHECK_FILE" ]; then
016a29f… lmata 97 last_check=$(cat "$LAST_CHECK_FILE")
87e6978… lmata 98 # Migrate from second-precision to millisecond-precision on first upgrade.
87e6978… lmata 99 if [ "$last_check" -lt 100000000000 ] 2>/dev/null; then
87e6978… lmata 100 last_check=$((last_check * 1000))
87e6978… lmata 101 fi
016a29f… lmata 102 fi
87e6978… lmata 103 now=$(now_millis)
016a29f… lmata 104 echo "$now" > "$LAST_CHECK_FILE"
016a29f… lmata 105
016a29f… lmata 106 BOTS='["bridge","oracle","sentinel","steward","scribe","warden","snitch","herald","scroll","systembot","auditbot","claude"]'
016a29f… lmata 107
016a29f… lmata 108 instruction=$(
1d3caa2… lmata 109 for channel in $(relay_channels); do
1d3caa2… lmata 110 messages=$(curl -sf \
1d3caa2… lmata 111 --connect-timeout 1 \
1d3caa2… lmata 112 --max-time 2 \
1d3caa2… lmata 113 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
1d3caa2… lmata 114 "$SCUTTLEBOT_URL/v1/channels/$channel/messages" 2>/dev/null) || continue
1d3caa2… lmata 115 [ -n "$messages" ] || continue
1d3caa2… lmata 116 echo "$messages" | jq -r --argjson bots "$BOTS" --arg self "$SCUTTLEBOT_NICK" --arg channel "$channel" '
1d3caa2… lmata 117 .messages[]
1d3caa2… lmata 118 | select(.nick as $n |
1d3caa2… lmata 119 ($bots | index($n) | not) and
1d3caa2… lmata 120 ($n | startswith("claude-") | not) and
1d3caa2… lmata 121 ($n | startswith("codex-") | not) and
1d3caa2… lmata 122 ($n | startswith("gemini-") | not) and
1d3caa2… lmata 123 $n != $self
1d3caa2… lmata 124 )
1d3caa2… lmata 125 | "\(.at)\t\($channel)\t\(.nick)\t\(.text)"
1d3caa2… lmata 126 ' 2>/dev/null
1d3caa2… lmata 127 done | while IFS=$'\t' read -r at channel nick text; do
87e6978… lmata 128 ts=$(epoch_millis "$at")
016a29f… lmata 129 [ -n "$ts" ] || continue
016a29f… lmata 130 [ "$ts" -gt "$last_check" ] || continue
016a29f… lmata 131 contains_mention "$text" || continue
1d3caa2… lmata 132 printf '%s\t[#%s] %s: %s\n' "$ts" "$channel" "$nick" "$text"
1d3caa2… lmata 133 done | sort -n | tail -1 | cut -f2-
016a29f… lmata 134 )
016a29f… lmata 135
016a29f… lmata 136 [ -z "$instruction" ] && exit 0
016a29f… lmata 137
016a29f… lmata 138 echo "{\"decision\": \"block\", \"reason\": \"[IRC instruction from operator] $instruction\"}"
016a29f… lmata 139 exit 0

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button