ScuttleBot

scuttlebot / skills / scuttlebot-relay / scripts / install-claude-relay.sh
Blame History Raw 361 lines
1
#!/usr/bin/env bash
2
# Install the tracked Claude 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/scuttlebot-relay/scripts/install-claude-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: irc.
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 Claude hooks install dir. Default: ~/.claude/hooks
24
--settings-json PATH Claude settings JSON. Default: ~/.claude/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_IRC_DELETE_ON_CLOSE
37
SCUTTLEBOT_HOOKS_ENABLED
38
SCUTTLEBOT_INTERRUPT_ON_MESSAGE
39
SCUTTLEBOT_POLL_INTERVAL
40
SCUTTLEBOT_PRESENCE_HEARTBEAT
41
SCUTTLEBOT_CONFIG_FILE
42
CLAUDE_HOOKS_DIR
43
CLAUDE_SETTINGS_JSON
44
CLAUDE_BIN_DIR
45
46
Examples:
47
bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \
48
--url http://localhost:8080 \
49
--token "$(./run.sh token)" \
50
--channel general
51
52
SCUTTLEBOT_HOOKS_ENABLED=0 make install-claude-relay
53
EOF
54
}
55
56
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
57
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
58
59
SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}"
60
SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}"
61
SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}"
62
SCUTTLEBOT_CHANNELS_VALUE="${SCUTTLEBOT_CHANNELS:-}"
63
SCUTTLEBOT_TRANSPORT_VALUE="${SCUTTLEBOT_TRANSPORT:-irc}"
64
SCUTTLEBOT_IRC_ADDR_VALUE="${SCUTTLEBOT_IRC_ADDR:-127.0.0.1:6667}"
65
if [ -n "${SCUTTLEBOT_IRC_PASS:-}" ]; then
66
SCUTTLEBOT_IRC_PASS_MODE="fixed"
67
SCUTTLEBOT_IRC_PASS_VALUE="$SCUTTLEBOT_IRC_PASS"
68
else
69
SCUTTLEBOT_IRC_PASS_MODE="auto"
70
SCUTTLEBOT_IRC_PASS_VALUE=""
71
fi
72
SCUTTLEBOT_IRC_DELETE_ON_CLOSE_VALUE="${SCUTTLEBOT_IRC_DELETE_ON_CLOSE:-1}"
73
SCUTTLEBOT_HOOKS_ENABLED_VALUE="${SCUTTLEBOT_HOOKS_ENABLED:-1}"
74
SCUTTLEBOT_INTERRUPT_ON_MESSAGE_VALUE="${SCUTTLEBOT_INTERRUPT_ON_MESSAGE:-1}"
75
SCUTTLEBOT_POLL_INTERVAL_VALUE="${SCUTTLEBOT_POLL_INTERVAL:-2s}"
76
SCUTTLEBOT_PRESENCE_HEARTBEAT_VALUE="${SCUTTLEBOT_PRESENCE_HEARTBEAT:-60s}"
77
78
CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
79
HOOKS_DIR="${CLAUDE_HOOKS_DIR:-$HOME/.claude/hooks}"
80
SETTINGS_JSON="${CLAUDE_SETTINGS_JSON:-$HOME/.claude/settings.json}"
81
BIN_DIR="${CLAUDE_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
--settings-json)
136
SETTINGS_JSON="${2:?missing value for --settings-json}"
137
shift 2
138
;;
139
--bin-dir)
140
BIN_DIR="${2:?missing value for --bin-dir}"
141
shift 2
142
;;
143
--help|-h)
144
usage
145
exit 0
146
;;
147
*)
148
printf 'install-claude-relay: unknown argument %s\n' "$1" >&2
149
usage >&2
150
exit 2
151
;;
152
esac
153
done
154
155
need_cmd() {
156
if ! command -v "$1" >/dev/null 2>&1; then
157
printf 'install-claude-relay: required command not found: %s\n' "$1" >&2
158
exit 1
159
fi
160
}
161
162
backup_file() {
163
local path="$1"
164
if [ -f "$path" ] && [ ! -f "${path}.bak" ]; then
165
cp "$path" "${path}.bak"
166
fi
167
}
168
169
ensure_parent_dir() {
170
mkdir -p "$(dirname "$1")"
171
}
172
173
normalize_channels() {
174
local primary="$1"
175
local raw="$2"
176
local IFS=','
177
local items=()
178
local extra_items=()
179
local item channel seen=""
180
181
if [ -n "$primary" ]; then
182
items+=("$primary")
183
fi
184
if [ -n "$raw" ]; then
185
read -r -a extra_items <<< "$raw"
186
items+=("${extra_items[@]}")
187
fi
188
189
for item in "${items[@]}"; do
190
channel="${item//[$' \t\r\n']/}"
191
channel="${channel#\#}"
192
[ -n "$channel" ] || continue
193
case ",$seen," in
194
*,"$channel",*) ;;
195
*) seen="${seen:+$seen,}$channel" ;;
196
esac
197
done
198
199
printf '%s' "$seen"
200
}
201
202
first_channel() {
203
local channels
204
channels=$(normalize_channels "" "$1")
205
printf '%s' "${channels%%,*}"
206
}
207
208
if [ -z "$SCUTTLEBOT_CHANNEL_VALUE" ] && [ -n "$SCUTTLEBOT_CHANNELS_VALUE" ]; then
209
SCUTTLEBOT_CHANNEL_VALUE="$(first_channel "$SCUTTLEBOT_CHANNELS_VALUE")"
210
fi
211
if [ -n "$SCUTTLEBOT_CHANNEL_VALUE" ]; then
212
SCUTTLEBOT_CHANNELS_VALUE="$(normalize_channels "$SCUTTLEBOT_CHANNEL_VALUE" "$SCUTTLEBOT_CHANNELS_VALUE")"
213
fi
214
215
upsert_env_var() {
216
local file="$1"
217
local key="$2"
218
local value="$3"
219
local escaped
220
escaped=$(printf '%q' "$value")
221
awk -v key="$key" -v value="$escaped" '
222
BEGIN { done = 0 }
223
$0 ~ "^(export[[:space:]]+)?" key "=" {
224
if (!done) {
225
print key "=" value
226
done = 1
227
}
228
next
229
}
230
{ print }
231
END {
232
if (!done) {
233
print key "=" value
234
}
235
}
236
' "$file" > "${file}.tmp"
237
mv "${file}.tmp" "$file"
238
}
239
240
remove_env_var() {
241
local file="$1"
242
local key="$2"
243
awk -v key="$key" '
244
$0 ~ "^(export[[:space:]]+)?" key "=" { next }
245
{ print }
246
' "$file" > "${file}.tmp"
247
mv "${file}.tmp" "$file"
248
}
249
250
need_cmd jq
251
need_cmd go
252
253
POST_CMD="$HOOKS_DIR/scuttlebot-post.sh"
254
CHECK_CMD="$HOOKS_DIR/scuttlebot-check.sh"
255
LAUNCHER_DST="$BIN_DIR/claude-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 "$LAUNCHER_DST"
264
install -m 0755 "$REPO_ROOT/skills/scuttlebot-relay/hooks/scuttlebot-post.sh" "$POST_CMD"
265
install -m 0755 "$REPO_ROOT/skills/scuttlebot-relay/hooks/scuttlebot-check.sh" "$CHECK_CMD"
266
267
printf 'Building claude-relay binary...\n'
268
(cd "$REPO_ROOT" && go build -o "$LAUNCHER_DST" ./cmd/claude-relay)
269
270
backup_file "$SETTINGS_JSON"
271
if [ -f "$SETTINGS_JSON" ]; then
272
jq --arg pre_matcher "Bash|Edit|Write" \
273
--arg pre_cmd "$CHECK_CMD" \
274
--arg post_matcher "Bash|Read|Edit|Write|Glob|Grep|Agent" \
275
--arg post_cmd "$POST_CMD" '
276
def ensure_matcher_entry(section; matcher; cmd):
277
.hooks = (.hooks // {})
278
| .hooks[section] = (.hooks[section] // [])
279
| if any(.hooks[section][]?; .matcher == matcher) then
280
.hooks[section] |= map(
281
if .matcher == matcher then
282
(.hooks = (.hooks // []))
283
| if any(.hooks[]?; .type == "command" and .command == cmd) then . else .hooks += [{"type":"command","command":cmd}] end
284
else
285
.
286
end
287
)
288
else
289
.hooks[section] += [{"matcher":matcher,"hooks":[{"type":"command","command":cmd}]}]
290
end;
291
ensure_matcher_entry("PreToolUse"; $pre_matcher; $pre_cmd)
292
| ensure_matcher_entry("PostToolUse"; $post_matcher; $post_cmd)
293
' "$SETTINGS_JSON" > "${SETTINGS_JSON}.tmp"
294
else
295
jq -n \
296
--arg pre_cmd "$CHECK_CMD" \
297
--arg post_cmd "$POST_CMD" '
298
{
299
hooks: {
300
"PreToolUse": [
301
{
302
matcher: "Bash|Edit|Write",
303
hooks: [{type: "command", command: $pre_cmd}]
304
}
305
],
306
"PostToolUse": [
307
{
308
matcher: "Bash|Read|Edit|Write|Glob|Grep|Agent",
309
hooks: [{type: "command", command: $post_cmd}]
310
}
311
]
312
}
313
}
314
' > "${SETTINGS_JSON}.tmp"
315
fi
316
mv "${SETTINGS_JSON}.tmp" "$SETTINGS_JSON"
317
318
backup_file "$CONFIG_FILE"
319
if [ ! -f "$CONFIG_FILE" ]; then
320
: > "$CONFIG_FILE"
321
fi
322
if [ -n "$SCUTTLEBOT_URL_VALUE" ]; then
323
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_URL "$SCUTTLEBOT_URL_VALUE"
324
fi
325
if [ -n "$SCUTTLEBOT_TOKEN_VALUE" ]; then
326
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_TOKEN "$SCUTTLEBOT_TOKEN_VALUE"
327
fi
328
if [ -n "$SCUTTLEBOT_CHANNEL_VALUE" ]; then
329
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_CHANNEL "${SCUTTLEBOT_CHANNEL_VALUE#\#}"
330
fi
331
if [ -n "$SCUTTLEBOT_CHANNELS_VALUE" ]; then
332
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_CHANNELS "$SCUTTLEBOT_CHANNELS_VALUE"
333
fi
334
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_TRANSPORT "$SCUTTLEBOT_TRANSPORT_VALUE"
335
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_ADDR "$SCUTTLEBOT_IRC_ADDR_VALUE"
336
if [ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ]; then
337
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_PASS "$SCUTTLEBOT_IRC_PASS_VALUE"
338
else
339
remove_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_PASS
340
fi
341
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_IRC_DELETE_ON_CLOSE "$SCUTTLEBOT_IRC_DELETE_ON_CLOSE_VALUE"
342
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_HOOKS_ENABLED "$SCUTTLEBOT_HOOKS_ENABLED_VALUE"
343
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_INTERRUPT_ON_MESSAGE "$SCUTTLEBOT_INTERRUPT_ON_MESSAGE_VALUE"
344
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_POLL_INTERVAL "$SCUTTLEBOT_POLL_INTERVAL_VALUE"
345
upsert_env_var "$CONFIG_FILE" SCUTTLEBOT_PRESENCE_HEARTBEAT "$SCUTTLEBOT_PRESENCE_HEARTBEAT_VALUE"
346
347
printf 'Installed Claude relay files:\n'
348
printf ' hooks: %s\n' "$HOOKS_DIR"
349
printf ' settings: %s\n' "$SETTINGS_JSON"
350
printf ' launcher: %s\n' "$LAUNCHER_DST"
351
printf ' env: %s\n' "$CONFIG_FILE"
352
printf ' irc auth: %s\n' "$([ "$SCUTTLEBOT_IRC_PASS_MODE" = "fixed" ] && printf 'fixed-pass override' || printf 'auto-register')"
353
printf '\n'
354
printf 'Next steps:\n'
355
printf ' 1. Launch with: %s\n' "$LAUNCHER_DST"
356
printf ' 2. Watch IRC for: claude-{repo}-{session}\n'
357
printf ' 3. Mention that nick to interrupt before the next action\n'
358
printf '\n'
359
printf 'Disable without uninstalling:\n'
360
printf ' SCUTTLEBOT_HOOKS_ENABLED=0 %s\n' "$LAUNCHER_DST"
361

Keyboard Shortcuts

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