ScuttleBot

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

Keyboard Shortcuts

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