ScuttleBot

scuttlebot / skills / gemini-relay / hooks / scuttlebot-after-agent.sh
Blame History Raw 123 lines
1
#!/bin/bash
2
# AfterAgent hook for Gemini agents. Posts final assistant replies to scuttlebot IRC.
3
4
SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
5
if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then
6
set -a
7
. "$SCUTTLEBOT_CONFIG_FILE"
8
set +a
9
fi
10
if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then
11
set -a
12
. "$SCUTTLEBOT_CHANNEL_STATE_FILE"
13
set +a
14
fi
15
16
SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}"
17
SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}"
18
SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}"
19
SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}"
20
SCUTTLEBOT_AFTER_AGENT_MAX_POSTS="${SCUTTLEBOT_AFTER_AGENT_MAX_POSTS:-6}"
21
SCUTTLEBOT_AFTER_AGENT_CHUNK_WIDTH="${SCUTTLEBOT_AFTER_AGENT_CHUNK_WIDTH:-360}"
22
23
normalize_channel() {
24
local channel="$1"
25
channel="${channel//[$' \t\r\n']/}"
26
channel="${channel#\#}"
27
printf '%s' "$channel"
28
}
29
30
relay_channels() {
31
local raw="${SCUTTLEBOT_CHANNELS:-$SCUTTLEBOT_CHANNEL}"
32
local IFS=','
33
local item channel seen=""
34
read -r -a items <<< "$raw"
35
for item in "${items[@]}"; do
36
channel=$(normalize_channel "$item")
37
[ -n "$channel" ] || continue
38
case ",$seen," in
39
*,"$channel",*) ;;
40
*)
41
seen="${seen:+$seen,}$channel"
42
printf '%s\n' "$channel"
43
;;
44
esac
45
done
46
}
47
48
sanitize() {
49
local input="$1"
50
if [ -z "$input" ]; then
51
input=$(cat)
52
fi
53
printf '%s' "$input" | tr -cs '[:alnum:]_-' '-'
54
}
55
56
post_line() {
57
local text="$1"
58
local payload
59
[ -z "$text" ] && return 0
60
payload="{\"text\": $(printf '%s' "$text" | jq -Rs .), \"nick\": \"$SCUTTLEBOT_NICK\"}"
61
for channel in $(relay_channels); do
62
curl -sf -X POST "$SCUTTLEBOT_URL/v1/channels/$channel/messages" \
63
--connect-timeout 1 \
64
--max-time 2 \
65
-H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
66
-H "Content-Type: application/json" \
67
-d "$payload" \
68
> /dev/null || true
69
done
70
}
71
72
normalize_response() {
73
printf '%s' "$1" \
74
| tr '\r\n\t' ' ' \
75
| tr -s '[:space:]' ' ' \
76
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
77
}
78
79
input=$(cat)
80
81
cwd=$(echo "$input" | jq -r '.cwd // empty')
82
if [ -z "$cwd" ]; then
83
cwd=$(pwd)
84
fi
85
base_name=$(sanitize "$(basename "$cwd")")
86
session_raw="${SCUTTLEBOT_SESSION_ID:-${GEMINI_SESSION_ID:-$PPID}}"
87
if [ -z "$session_raw" ] || [ "$session_raw" = "0" ]; then
88
session_raw=$(date +%s)
89
fi
90
session_suffix=$(printf '%s' "$session_raw" | sanitize | cut -c 1-8)
91
default_nick="gemini-${base_name}-${session_suffix}"
92
SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}"
93
94
[ "$SCUTTLEBOT_HOOKS_ENABLED" = "0" ] && { echo '{}'; exit 0; }
95
[ "$SCUTTLEBOT_HOOKS_ENABLED" = "false" ] && { echo '{}'; exit 0; }
96
[ -z "$SCUTTLEBOT_TOKEN" ] && { echo '{}'; exit 0; }
97
98
response=$(echo "$input" | jq -r '.prompt_response // empty')
99
[ -z "$response" ] && { echo '{}'; exit 0; }
100
101
response=$(normalize_response "$response")
102
[ -z "$response" ] && { echo '{}'; exit 0; }
103
104
posted=0
105
truncated=0
106
while IFS= read -r chunk || [ -n "$chunk" ]; do
107
chunk=$(printf '%s' "$chunk" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
108
[ -z "$chunk" ] && continue
109
if [ "$posted" -ge "$SCUTTLEBOT_AFTER_AGENT_MAX_POSTS" ]; then
110
truncated=1
111
break
112
fi
113
post_line "$chunk"
114
posted=$((posted + 1))
115
done < <(printf '%s\n' "$response" | fold -s -w "$SCUTTLEBOT_AFTER_AGENT_CHUNK_WIDTH")
116
117
if [ "$truncated" -eq 1 ]; then
118
post_line "[reply truncated]"
119
fi
120
121
echo '{}'
122
exit 0
123

Keyboard Shortcuts

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