ScuttleBot

scuttlebot / skills / gemini-relay / scripts / install-gemini-relay.sh
Blame History Raw 380 lines
1
#!/usr/bin/env bash
2
# Install the tracked Gemini relay hooks plus binary launcher into a local setup.
3
4
set -euo pipefail
5
6
usage() {
7
cat <<'EOF'
8
Usage:
9
bash skills/gemini-relay/scripts/install-gemini-relay.sh [options]
10
11
Options:
12
--url URL Set SCUTTLEBOT_URL in the shared env file.
13
--token TOKEN Set SCUTTLEBOT_TOKEN in the shared env file.
14
--channel CHANNEL Set SCUTTLEBOT_CHANNEL in the shared env file.
15
--channels CSV Set SCUTTLEBOT_CHANNELS in the shared env file.
16
--transport MODE Set SCUTTLEBOT_TRANSPORT (http or irc). Default: http.
17
--irc-addr ADDR Set SCUTTLEBOT_IRC_ADDR. Default: 127.0.0.1:6667.
18
--irc-pass PASS Write SCUTTLEBOT_IRC_PASS for fixed-identity IRC mode.
19
--auto-register Remove SCUTTLEBOT_IRC_PASS so IRC mode auto-registers session nicks. Default.
20
--enabled Write SCUTTLEBOT_HOOKS_ENABLED=1. Default.
21
--disabled Write SCUTTLEBOT_HOOKS_ENABLED=0.
22
--config-file PATH Shared env file path. Default: ~/.config/scuttlebot-relay.env
23
--hooks-dir PATH Gemini hooks install dir. Default: ~/.gemini/hooks
24
--settings-json PATH Gemini settings JSON. Default: ~/.gemini/settings.json
25
--bin-dir PATH Launcher install dir. Default: ~/.local/bin
26
--help Show this help.
27
28
Environment defaults:
29
SCUTTLEBOT_URL
30
SCUTTLEBOT_TOKEN
31
SCUTTLEBOT_CHANNEL
32
SCUTTLEBOT_CHANNELS
33
SCUTTLEBOT_TRANSPORT
34
SCUTTLEBOT_IRC_ADDR
35
SCUTTLEBOT_IRC_PASS
36
SCUTTLEBOT_HOOKS_ENABLED
37
SCUTTLEBOT_INTERRUPT_ON_MESSAGE
38
SCUTTLEBOT_POLL_INTERVAL
39
SCUTTLEBOT_PRESENCE_HEARTBEAT
40
SCUTTLEBOT_CONFIG_FILE
41
GEMINI_HOOKS_DIR
42
GEMINI_SETTINGS_JSON
43
GEMINI_BIN_DIR
44
45
Examples:
46
bash skills/gemini-relay/scripts/install-gemini-relay.sh \
47
--url http://localhost:8080 \
48
--token "$(./run.sh token)" \
49
--channel general
50
51
SCUTTLEBOT_HOOKS_ENABLED=0 make install-gemini-relay
52
EOF
53
}
54
55
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
56
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
57
58
SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}"
59
SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}"
60
SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}"
61
SCUTTLEBOT_CHANNELS_VALUE="${SCUTTLEBOT_CHANNELS:-}"
62
SCUTTLEBOT_TRANSPORT_VALUE="${SCUTTLEBOT_TRANSPORT:-http}"
63
SCUTTLEBOT_IRC_ADDR_VALUE="${SCUTTLEBOT_IRC_ADDR:-127.0.0.1:6667}"
64
if [ -n "${SCUTTLEBOT_IRC_PASS:-}" ]; then
65
SCUTTLEBOT_IRC_PASS_MODE="fixed"
66
SCUTTLEBOT_IRC_PASS_VALUE="$SCUTTLEBOT_IRC_PASS"
67
else
68
SCUTTLEBOT_IRC_PASS_MODE="auto"
69
SCUTTLEBOT_IRC_PASS_VALUE=""
70
fi
71
SCUTTLEBOT_IRC_DELETE_ON_CLOSE_VALUE="${SCUTTLEBOT_IRC_DELETE_ON_CLOSE:-1}"
72
SCUTTLEBOT_HOOKS_ENABLED_VALUE="${SCUTTLEBOT_HOOKS_ENABLED:-1}"
73
SCUTTLEBOT_INTERRUPT_ON_MESSAGE_VALUE="${SCUTTLEBOT_INTERRUPT_ON_MESSAGE:-1}"
74
SCUTTLEBOT_POLL_INTERVAL_VALUE="${SCUTTLEBOT_POLL_INTERVAL:-2s}"
75
SCUTTLEBOT_PRESENCE_HEARTBEAT_VALUE="${SCUTTLEBOT_PRESENCE_HEARTBEAT:-60s}"
76
77
CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
78
HOOKS_DIR="${GEMINI_HOOKS_DIR:-$HOME/.gemini/hooks}"
79
SETTINGS_JSON="${GEMINI_SETTINGS_JSON:-$HOME/.gemini/settings.json}"
80
BIN_DIR="${GEMINI_BIN_DIR:-$HOME/.local/bin}"
81
82
while [ $# -gt 0 ]; do
83
case "$1" in
84
--url)
85
SCUTTLEBOT_URL_VALUE="${2:?missing value for --url}"
86
shift 2
87
;;
88
--token)
89
SCUTTLEBOT_TOKEN_VALUE="${2:?missing value for --token}"
90
shift 2
91
;;
92
--channel)
93
SCUTTLEBOT_CHANNEL_VALUE="${2:?missing value for --channel}"
94
shift 2
95
;;
96
--channels)
97
SCUTTLEBOT_CHANNELS_VALUE="${2:?missing value for --channels}"
98
shift 2
99
;;
100
--transport)
101
SCUTTLEBOT_TRANSPORT_VALUE="${2:?missing value for --transport}"
102
shift 2
103
;;
104
--irc-addr)
105
SCUTTLEBOT_IRC_ADDR_VALUE="${2:?missing value for --irc-addr}"
106
shift 2
107
;;
108
--irc-pass)
109
SCUTTLEBOT_IRC_PASS_MODE="fixed"
110
SCUTTLEBOT_IRC_PASS_VALUE="${2:?missing value for --irc-pass}"
111
shift 2
112
;;
113
--auto-register)
114
SCUTTLEBOT_IRC_PASS_MODE="auto"
115
SCUTTLEBOT_IRC_PASS_VALUE=""
116
shift
117
;;
118
--enabled)
119
SCUTTLEBOT_HOOKS_ENABLED_VALUE=1
120
shift
121
;;
122
--disabled)
123
SCUTTLEBOT_HOOKS_ENABLED_VALUE=0
124
shift
125
;;
126
--config-file)
127
CONFIG_FILE="${2:?missing value for --config-file}"
128
shift 2
129
;;
130
--hooks-dir)
131
HOOKS_DIR="${2:?missing value for --hooks-dir}"
132
shift 2
133
;;
134
--settings-json)
135
SETTINGS_JSON="${2:?missing value for --settings-json}"
136
shift 2
137
;;
138
--bin-dir)
139
BIN_DIR="${2:?missing value for --bin-dir}"
140
shift 2
141
;;
142
--help|-h)
143
usage
144
exit 0
145
;;
146
*)
147
printf 'install-gemini-relay: unknown argument %s\n' "$1" >&2
148
usage >&2
149
exit 2
150
;;
151
esac
152
done
153
154
need_cmd() {
155
if ! command -v "$1" >/dev/null 2>&1; then
156
printf 'install-gemini-relay: required command not found: %s\n' "$1" >&2
157
exit 1
158
fi
159
}
160
161
backup_file() {
162
local path="$1"
163
if [ -f "$path" ] && [ ! -f "${path}.bak" ]; then
164
cp "$path" "${path}.bak"
165
fi
166
}
167
168
ensure_parent_dir() {
169
mkdir -p "$(dirname "$1")"
170
}
171
172
normalize_channels() {
173
local primary="$1"
174
local raw="$2"
175
local IFS=','
176
local items=()
177
local extra_items=()
178
local item channel seen=""
179
180
if [ -n "$primary" ]; then
181
items+=("$primary")
182
fi
183
if [ -n "$raw" ]; then
184
read -r -a extra_items <<< "$raw"
185
items+=("${extra_items[@]}")
186
fi
187
188
for item in "${items[@]}"; do
189
channel="${item//[$' \t\r\n']/}"
190
channel="${channel#\#}"
191
[ -n "$channel" ] || continue
192
case ",$seen," in
193
*,"$channel",*) ;;
194
*) seen="${seen:+$seen,}$channel" ;;
195
esac
196
done
197
198
printf '%s' "$seen"
199
}
200
201
first_channel() {
202
local channels
203
channels=$(normalize_channels "" "$1")
204
printf '%s' "${channels%%,*}"
205
}
206
207
if [ -z "$SCUTTLEBOT_CHANNEL_VALUE" ] && [ -n "$SCUTTLEBOT_CHANNELS_VALUE" ]; then
208
SCUTTLEBOT_CHANNEL_VALUE="$(first_channel "$SCUTTLEBOT_CHANNELS_VALUE")"
209
fi
210
if [ -n "$SCUTTLEBOT_CHANNEL_VALUE" ]; then
211
SCUTTLEBOT_CHANNELS_VALUE="$(normalize_channels "$SCUTTLEBOT_CHANNEL_VALUE" "$SCUTTLEBOT_CHANNELS_VALUE")"
212
fi
213
214
upsert_env_var() {
215
local file="$1"
216
local key="$2"
217
local value="$3"
218
local escaped
219
escaped=$(printf '%q' "$value")
220
awk -v key="$key" -v value="$escaped" '
221
BEGIN { done = 0 }
222
$0 ~ "^(export[[:space:]]+)?" key "=" {
223
if (!done) {
224
print key "=" value
225
done = 1
226
}
227
next
228
}
229
{ print }
230
END {
231
if (!done) {
232
print key "=" value
233
}
234
}
235
' "$file" > "${file}.tmp"
236
mv "${file}.tmp" "$file"
237
}
238
239
remove_env_var() {
240
local file="$1"
241
local key="$2"
242
awk -v key="$key" '
243
$0 ~ "^(export[[:space:]]+)?" key "=" { next }
244
{ print }
245
' "$file" > "${file}.tmp"
246
mv "${file}.tmp" "$file"
247
}
248
249
need_cmd jq
250
need_cmd go
251
252
POST_CMD="$HOOKS_DIR/scuttlebot-post.sh"
253
CHECK_CMD="$HOOKS_DIR/scuttlebot-check.sh"
254
AFTER_AGENT_CMD="$HOOKS_DIR/scuttlebot-after-agent.sh"
255
LAUNCHER_DST="$BIN_DIR/gemini-relay"
256
257
mkdir -p "$HOOKS_DIR" "$BIN_DIR"
258
ensure_parent_dir "$SETTINGS_JSON"
259
ensure_parent_dir "$CONFIG_FILE"
260
261
backup_file "$POST_CMD"
262
backup_file "$CHECK_CMD"
263
backup_file "$AFTER_AGENT_CMD"
264
backup_file "$LAUNCHER_DST"
265
install -m 0755 "$REPO_ROOT/skills/gemini-relay/hooks/scuttlebot-post.sh" "$POST_CMD"
266
install -m 0755 "$REPO_ROOT/skills/gemini-relay/hooks/scuttlebot-check.sh" "$CHECK_CMD"
267
install -m 0755 "$REPO_ROOT/skills/gemini-relay/hooks/scuttlebot-after-agent.sh" "$AFTER_AGENT_CMD"
268
269
printf 'Building gemini-relay binary...\n'
270
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/gemini-relay.XXXXXX")
271
tmp_bin="$tmp_dir/gemini-relay"
272
cleanup_tmp_bin() {
273
rm -rf "$tmp_dir"
274
}
275
trap cleanup_tmp_bin EXIT
276
(cd "$REPO_ROOT" && go build -o "$tmp_bin" ./cmd/gemini-relay)
277
install -m 0755 "$tmp_bin" "$LAUNCHER_DST"
278
279
backup_file "$SETTINGS_JSON"
280
if [ -f "$SETTINGS_JSON" ]; then
281
jq --arg pre_matcher ".*" \
282
--arg pre_cmd "$CHECK_CMD" \
283
--arg post_matcher ".*" \
284
--arg post_cmd "$POST_CMD" \
285
--arg after_agent_matcher "*" \
286
--arg after_agent_cmd "$AFTER_AGENT_CMD" '
287
def ensure_matcher_entry(section; matcher; cmd):
288
.hooks = (.hooks // {})
289
| .hooks[section] = (.hooks[section] // [])
290
| if any(.hooks[section][]?; .matcher == matcher) then
291
.hooks[section] |= map(
292
if .matcher == matcher then
293
(.hooks = (.hooks // []))
294
| if any(.hooks[]?; .type == "command" and .command == cmd) then . else .hooks += [{"type":"command","command":cmd}] end
295
else
296
.
297
end
298
)
299
else
300
.hooks[section] += [{"matcher":matcher,"hooks":[{"type":"command","command":cmd}]}]
301
end;
302
ensure_matcher_entry("BeforeTool"; $pre_matcher; $pre_cmd)
303
| ensure_matcher_entry("AfterTool"; $post_matcher; $post_cmd)
304
| ensure_matcher_entry("AfterAgent"; $after_agent_matcher; $after_agent_cmd)
305
' "$SETTINGS_JSON" > "${SETTINGS_JSON}.tmp"
306
else
307
jq -n \
308
--arg pre_cmd "$CHECK_CMD" \
309
--arg post_cmd "$POST_CMD" \
310
--arg after_agent_cmd "$AFTER_AGENT_CMD" '
311
{
312
hooks: {
313
"BeforeTool": [
314
{
315
matcher: ".*",
316
hooks: [{type: "command", command: $pre_cmd}]
317
}
318
],
319
"AfterTool": [
320
{
321
matcher: ".*",
322
hooks: [{type: "command", command: $post_cmd}]
323
}
324
],
325
"AfterAgent": [
326
{
327
matcher: "*",
328
hooks: [{type: "command", command: $after_agent_cmd}]
329
}
330
]
331
}
332
}
333
' > "${SETTINGS_JSON}.tmp"
334
fi
335
mv "${SETTINGS_JSON}.tmp" "$SETTINGS_JSON"
336
337
backup_file "$CONFIG_FILE"
338
if [ ! -f "$CONFIG_FILE" ]; then
339
: > "$CONFIG_FILE"
340
fi
341
if [ -n "$SCUTTLEBOT_URL_VALUE" ]; then
342
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_URL "$SCUTTLEBOT_URL_VALUE"
343
fi
344
if [ -n "$SCUTTLEBOT_TOKEN_VALUE" ]; then
345
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_TOKEN "$SCUTTLEBOT_TOKEN_VALUE"
346
fi
347
if [ -n "$SCUTTLEBOT_CHANNEL_VALUE" ]; then
348
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_CHANNEL "${SCUTTLEBOT_CHANNEL_VALUE#\#}"
349
fi
350
if [ -n "$SCUTTLEBOT_CHANNELS_VALUE" ]; then
351
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_CHANNELS "$SCUTTLEBOT_CHANNELS_VALUE"
352
fi
353
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_TRANSPORT "$SCUTTLEBOT_TRANSPORT_VALUE"
354
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_ADDR "$SCUTTLEBOT_IRC_ADDR_VALUE"
355
if [ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ]; then
356
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_PASS "$SCUTTLEBOT_IRC_PASS_VALUE"
357
else
358
remove_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_PASS
359
fi
360
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_DELETE_ON_CLOSE "$SCUTTLEBOT_IRC_DELETE_ON_CLOSE_VALUE"
361
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_HOOKS_ENABLED "$SCUTTLEBOT_HOOKS_ENABLED_VALUE"
362
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_INTERRUPT_ON_MESSAGE "$SCUTTLEBOT_INTERRUPT_ON_MESSAGE_VALUE"
363
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_POLL_INTERVAL "$SCUTTLEBOT_POLL_INTERVAL_VALUE"
364
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_PRESENCE_HEARTBEAT "$SCUTTLEBOT_PRESENCE_HEARTBEAT_VALUE"
365
366
printf 'Installed Gemini relay files:\n'
367
printf ' hooks: %s\n' "$HOOKS_DIR"
368
printf ' settings: %s\n' "$SETTINGS_JSON"
369
printf ' launcher: %s\n' "$LAUNCHER_DST"
370
printf ' env: %s\n' "$CONFIG_FILE"
371
printf ' irc auth: %s\n' "$([ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ] && printf 'fixed-pass override' || printf 'auto-register')"
372
printf '\n'
373
printf 'Next steps:\n'
374
printf ' 1. Launch with: %s\n' "$LAUNCHER_DST"
375
printf ' 2. Watch IRC for: gemini-{repo}-{session}\n'
376
printf ' 3. Mention that nick to interrupt before the next action\n'
377
printf '\n'
378
printf 'Disable without uninstalling:\n'
379
printf ' SCUTTLEBOT_HOOKS_ENABLED=0 %s\n' "$LAUNCHER_DST"
380

Keyboard Shortcuts

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