|
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
|
|