ScuttleBot

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

Keyboard Shortcuts

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