ScuttleBot

scuttlebot / skills / openai-relay / scripts / install-codex-relay.sh
Blame History Raw 434 lines
1
#!/usr/bin/env bash
2
# Install the tracked Codex relay hooks plus the compiled broker into a local Codex setup.
3
4
set -euo pipefail
5
6
usage() {
7
cat <<'EOF'
8
Usage:
9
bash skills/openai-relay/scripts/install-codex-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 Codex hooks install dir. Default: ~/.codex/hooks
24
--hooks-json PATH Codex hooks config JSON. Default: ~/.codex/hooks.json
25
--codex-config PATH Codex config TOML. Default: ~/.codex/config.toml
26
--bin-dir PATH Launcher install dir. Default: ~/.local/bin
27
--help Show this help.
28
29
Environment defaults:
30
SCUTTLEBOT_URL
31
SCUTTLEBOT_TOKEN
32
SCUTTLEBOT_CHANNEL
33
SCUTTLEBOT_CHANNELS
34
SCUTTLEBOT_TRANSPORT
35
SCUTTLEBOT_IRC_ADDR
36
SCUTTLEBOT_IRC_PASS
37
SCUTTLEBOT_HOOKS_ENABLED
38
SCUTTLEBOT_INTERRUPT_ON_MESSAGE
39
SCUTTLEBOT_POLL_INTERVAL
40
SCUTTLEBOT_PRESENCE_HEARTBEAT
41
SCUTTLEBOT_CONFIG_FILE
42
CODEX_HOOKS_DIR
43
CODEX_HOOKS_JSON
44
CODEX_CONFIG_TOML
45
CODEX_BIN_DIR
46
47
Examples:
48
bash skills/openai-relay/scripts/install-codex-relay.sh \
49
--url http://localhost:8080 \
50
--token "$(./run.sh token)" \
51
--channel general
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="${CODEX_HOOKS_DIR:-$HOME/.codex/hooks}"
79
HOOKS_JSON="${CODEX_HOOKS_JSON:-$HOME/.codex/hooks.json}"
80
CODEX_CONFIG="${CODEX_CONFIG_TOML:-$HOME/.codex/config.toml}"
81
BIN_DIR="${CODEX_BIN_DIR:-$HOME/.local/bin}"
82
83
while [ $# -gt 0 ]; do
84
case "$1" in
85
--url)
86
SCUTTLEBOT_URL_VALUE="${2:?missing value for --url}"
87
shift 2
88
;;
89
--token)
90
SCUTTLEBOT_TOKEN_VALUE="${2:?missing value for --token}"
91
shift 2
92
;;
93
--channel)
94
SCUTTLEBOT_CHANNEL_VALUE="${2:?missing value for --channel}"
95
shift 2
96
;;
97
--channels)
98
SCUTTLEBOT_CHANNELS_VALUE="${2:?missing value for --channels}"
99
shift 2
100
;;
101
--transport)
102
SCUTTLEBOT_TRANSPORT_VALUE="${2:?missing value for --transport}"
103
shift 2
104
;;
105
--irc-addr)
106
SCUTTLEBOT_IRC_ADDR_VALUE="${2:?missing value for --irc-addr}"
107
shift 2
108
;;
109
--irc-pass)
110
SCUTTLEBOT_IRC_PASS_MODE="fixed"
111
SCUTTLEBOT_IRC_PASS_VALUE="${2:?missing value for --irc-pass}"
112
shift 2
113
;;
114
--auto-register)
115
SCUTTLEBOT_IRC_PASS_MODE="auto"
116
SCUTTLEBOT_IRC_PASS_VALUE=""
117
shift
118
;;
119
--enabled)
120
SCUTTLEBOT_HOOKS_ENABLED_VALUE=1
121
shift
122
;;
123
--disabled)
124
SCUTTLEBOT_HOOKS_ENABLED_VALUE=0
125
shift
126
;;
127
--config-file)
128
CONFIG_FILE="${2:?missing value for --config-file}"
129
shift 2
130
;;
131
--hooks-dir)
132
HOOKS_DIR="${2:?missing value for --hooks-dir}"
133
shift 2
134
;;
135
--hooks-json)
136
HOOKS_JSON="${2:?missing value for --hooks-json}"
137
shift 2
138
;;
139
--codex-config)
140
CODEX_CONFIG="${2:?missing value for --codex-config}"
141
shift 2
142
;;
143
--bin-dir)
144
BIN_DIR="${2:?missing value for --bin-dir}"
145
shift 2
146
;;
147
--help|-h)
148
usage
149
exit 0
150
;;
151
*)
152
printf 'install-codex-relay: unknown argument %s\n' "$1" >&2
153
usage >&2
154
exit 2
155
;;
156
esac
157
done
158
159
need_cmd() {
160
if ! command -v "$1" >/dev/null 2>&1; then
161
printf 'install-codex-relay: required command not found: %s\n' "$1" >&2
162
exit 1
163
fi
164
}
165
166
backup_file() {
167
local path="$1"
168
if [ -f "$path" ] && [ ! -f "${path}.bak" ]; then
169
cp "$path" "${path}.bak"
170
fi
171
}
172
173
ensure_parent_dir() {
174
mkdir -p "$(dirname "$1")"
175
}
176
177
normalize_channels() {
178
local primary="$1"
179
local raw="$2"
180
local IFS=','
181
local items=()
182
local extra_items=()
183
local item channel seen=""
184
185
if [ -n "$primary" ]; then
186
items+=("$primary")
187
fi
188
if [ -n "$raw" ]; then
189
read -r -a extra_items <<< "$raw"
190
items+=("${extra_items[@]}")
191
fi
192
193
for item in "${items[@]}"; do
194
channel="${item//[$' \t\r\n']/}"
195
channel="${channel#\#}"
196
[ -n "$channel" ] || continue
197
case ",$seen," in
198
*,"$channel",*) ;;
199
*) seen="${seen:+$seen,}$channel" ;;
200
esac
201
done
202
203
printf '%s' "$seen"
204
}
205
206
first_channel() {
207
local channels
208
channels=$(normalize_channels "" "$1")
209
printf '%s' "${channels%%,*}"
210
}
211
212
if [ -z "$SCUTTLEBOT_CHANNEL_VALUE" ] && [ -n "$SCUTTLEBOT_CHANNELS_VALUE" ]; then
213
SCUTTLEBOT_CHANNEL_VALUE="$(first_channel "$SCUTTLEBOT_CHANNELS_VALUE")"
214
fi
215
if [ -n "$SCUTTLEBOT_CHANNEL_VALUE" ]; then
216
SCUTTLEBOT_CHANNELS_VALUE="$(normalize_channels "$SCUTTLEBOT_CHANNEL_VALUE" "$SCUTTLEBOT_CHANNELS_VALUE")"
217
fi
218
219
upsert_env_var() {
220
local file="$1"
221
local key="$2"
222
local value="$3"
223
local escaped
224
escaped=$(printf '%q' "$value")
225
awk -v key="$key" -v value="$escaped" '
226
BEGIN { done = 0 }
227
$0 ~ "^(export[[:space:]]+)?" key "=" {
228
if (!done) {
229
print key "=" value
230
done = 1
231
}
232
next
233
}
234
{ print }
235
END {
236
if (!done) {
237
print key "=" value
238
}
239
}
240
' "$file" > "${file}.tmp"
241
mv "${file}.tmp" "$file"
242
}
243
244
remove_env_var() {
245
local file="$1"
246
local key="$2"
247
awk -v key="$key" '
248
$0 ~ "^(export[[:space:]]+)?" key "=" { next }
249
{ print }
250
' "$file" > "${file}.tmp"
251
mv "${file}.tmp" "$file"
252
}
253
254
ensure_codex_hooks_feature() {
255
local file="$1"
256
local tmp="${file}.tmp"
257
if [ ! -f "$file" ]; then
258
cat > "$tmp" <<'EOF'
259
[features]
260
codex_hooks = true
261
EOF
262
mv "$tmp" "$file"
263
return
264
fi
265
266
awk '
267
BEGIN {
268
in_features = 0
269
features_seen = 0
270
codex_hooks_set = 0
271
}
272
/^\[features\][[:space:]]*$/ {
273
print
274
in_features = 1
275
features_seen = 1
276
next
277
}
278
/^\[/ {
279
if (in_features && !codex_hooks_set) {
280
print "codex_hooks = true"
281
codex_hooks_set = 1
282
}
283
in_features = 0
284
print
285
next
286
}
287
{
288
if (in_features && $0 ~ /^[[:space:]]*codex_hooks[[:space:]]*=/) {
289
if (!codex_hooks_set) {
290
print "codex_hooks = true"
291
codex_hooks_set = 1
292
}
293
next
294
}
295
print
296
}
297
END {
298
if (!features_seen) {
299
if (NR > 0) {
300
print ""
301
}
302
print "[features]"
303
print "codex_hooks = true"
304
} else if (in_features && !codex_hooks_set) {
305
print "codex_hooks = true"
306
}
307
}
308
' "$file" > "$tmp"
309
mv "$tmp" "$file"
310
}
311
312
need_cmd go
313
need_cmd jq
314
315
POST_CMD="$HOOKS_DIR/scuttlebot-post.sh"
316
CHECK_CMD="$HOOKS_DIR/scuttlebot-check.sh"
317
LAUNCHER_DST="$BIN_DIR/codex-relay"
318
319
mkdir -p "$HOOKS_DIR" "$BIN_DIR"
320
ensure_parent_dir "$HOOKS_JSON"
321
ensure_parent_dir "$CODEX_CONFIG"
322
ensure_parent_dir "$CONFIG_FILE"
323
324
backup_file "$POST_CMD"
325
backup_file "$CHECK_CMD"
326
backup_file "$LAUNCHER_DST"
327
install -m 0755 "$REPO_ROOT/skills/openai-relay/hooks/scuttlebot-post.sh" "$POST_CMD"
328
install -m 0755 "$REPO_ROOT/skills/openai-relay/hooks/scuttlebot-check.sh" "$CHECK_CMD"
329
(cd "$REPO_ROOT" && go build -o "$LAUNCHER_DST" ./cmd/codex-relay)
330
331
backup_file "$HOOKS_JSON"
332
if [ -f "$HOOKS_JSON" ]; then
333
jq --arg pre_matcher "Bash|Edit|Write" \
334
--arg pre_cmd "$CHECK_CMD" \
335
--arg post_matcher "Bash|Read|Edit|Write|Glob|Grep|Agent" \
336
--arg post_cmd "$POST_CMD" '
337
def ensure_command(matcher; cmd):
338
.hooks = (.hooks // {})
339
| .hooks[matcher] = (.hooks[matcher] // [])
340
| if any(.hooks[matcher][]?; .type == "command" and .command == cmd) then
341
.
342
else
343
.hooks[matcher] += [{"type":"command","command":cmd}]
344
end;
345
def ensure_matcher_entry(section; matcher; cmd):
346
.hooks = (.hooks // {})
347
| .hooks[section] = (.hooks[section] // [])
348
| if any(.hooks[section][]?; .matcher == matcher) then
349
.hooks[section] |= map(
350
if .matcher == matcher then
351
(.hooks = (.hooks // []))
352
| if any(.hooks[]?; .type == "command" and .command == cmd) then . else .hooks += [{"type":"command","command":cmd}] end
353
else
354
.
355
end
356
)
357
else
358
.hooks[section] += [{"matcher":matcher,"hooks":[{"type":"command","command":cmd}]}]
359
end;
360
ensure_matcher_entry("pre-tool-use"; $pre_matcher; $pre_cmd)
361
| ensure_matcher_entry("post-tool-use"; $post_matcher; $post_cmd)
362
' "$HOOKS_JSON" > "${HOOKS_JSON}.tmp"
363
else
364
jq -n \
365
--arg pre_cmd "$CHECK_CMD" \
366
--arg post_cmd "$POST_CMD" '
367
{
368
hooks: {
369
"pre-tool-use": [
370
{
371
matcher: "Bash|Edit|Write",
372
hooks: [{type: "command", command: $pre_cmd}]
373
}
374
],
375
"post-tool-use": [
376
{
377
matcher: "Bash|Read|Edit|Write|Glob|Grep|Agent",
378
hooks: [{type: "command", command: $post_cmd}]
379
}
380
]
381
}
382
}
383
' > "${HOOKS_JSON}.tmp"
384
fi
385
mv "${HOOKS_JSON}.tmp" "$HOOKS_JSON"
386
387
backup_file "$CODEX_CONFIG"
388
ensure_codex_hooks_feature "$CODEX_CONFIG"
389
390
backup_file "$CONFIG_FILE"
391
if [ ! -f "$CONFIG_FILE" ]; then
392
: > "$CONFIG_FILE"
393
fi
394
if [ -n "$SCUTTLEBOT_URL_VALUE" ]; then
395
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_URL "$SCUTTLEBOT_URL_VALUE"
396
fi
397
if [ -n "$SCUTTLEBOT_TOKEN_VALUE" ]; then
398
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_TOKEN "$SCUTTLEBOT_TOKEN_VALUE"
399
fi
400
if [ -n "$SCUTTLEBOT_CHANNEL_VALUE" ]; then
401
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_CHANNEL "${SCUTTLEBOT_CHANNEL_VALUE#\#}"
402
fi
403
if [ -n "$SCUTTLEBOT_CHANNELS_VALUE" ]; then
404
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_CHANNELS "$SCUTTLEBOT_CHANNELS_VALUE"
405
fi
406
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_TRANSPORT "$SCUTTLEBOT_TRANSPORT_VALUE"
407
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_ADDR "$SCUTTLEBOT_IRC_ADDR_VALUE"
408
if [ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ]; then
409
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_PASS "$SCUTTLEBOT_IRC_PASS_VALUE"
410
else
411
remove_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_PASS
412
fi
413
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_DELETE_ON_CLOSE "$SCUTTLEBOT_IRC_DELETE_ON_CLOSE_VALUE"
414
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_HOOKS_ENABLED "$SCUTTLEBOT_HOOKS_ENABLED_VALUE"
415
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_INTERRUPT_ON_MESSAGE "$SCUTTLEBOT_INTERRUPT_ON_MESSAGE_VALUE"
416
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_POLL_INTERVAL "$SCUTTLEBOT_POLL_INTERVAL_VALUE"
417
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_PRESENCE_HEARTBEAT "$SCUTTLEBOT_PRESENCE_HEARTBEAT_VALUE"
418
419
printf 'Installed Codex relay files:\n'
420
printf ' hooks: %s\n' "$HOOKS_DIR"
421
printf ' hooks.json: %s\n' "$HOOKS_JSON"
422
printf ' config: %s\n' "$CODEX_CONFIG"
423
printf ' broker: %s\n' "$LAUNCHER_DST"
424
printf ' env: %s\n' "$CONFIG_FILE"
425
printf ' irc auth: %s\n' "$([ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ] && printf 'fixed-pass override' || printf 'auto-register')"
426
printf '\n'
427
printf 'Next steps:\n'
428
printf ' 1. Launch with: %s\n' "$LAUNCHER_DST"
429
printf ' 2. Watch IRC for: codex-{repo}-{session}\n'
430
printf ' 3. Mention that nick to interrupt before the next action\n'
431
printf '\n'
432
printf 'Disable without uninstalling:\n'
433
printf ' SCUTTLEBOT_HOOKS_ENABLED=0 %s\n' "$LAUNCHER_DST"
434

Keyboard Shortcuts

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