ScuttleBot

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