Repository navigation
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·231 lines (201 loc) · 8.53 KB
/
Copy pathsetup.sh
File metadata and controls
executable file
·231 lines (201 loc) · 8.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#!/usr/bin/env bash
set -Eeuo pipefail
APP_NAME="commandcode-hermes-setup"
UPSTREAM_REPO="https://github.com/yelixir-dev/commandcode-bridge.git"
UPSTREAM_REF="main"
HOST="127.0.0.1"
PORT="9992"
MODEL="gpt-5.6-luna"
MODELS="deepseek/deepseek-v4-pro,deepseek/deepseek-v4-flash,gpt-5.6-luna"
USE=0
ASSUME_YES=0
NO_START=0
SKIP_SMOKE=0
COMMANDCODE_API_KEY=""
usage() {
cat <<'USAGE'
Connect CommandCode Bridge to Hermes Agent.
Usage: ./setup.sh [options]
Options:
--yes Accept safe defaults; do not answer optional prompts
--host HOST Bridge bind host (default: 127.0.0.1)
--port PORT Bridge port (default: 9992)
--model MODEL Hermes provider model (default: gpt-5.6-luna)
--models CSV Allowed bridge models
--use Make custom:commandcode the Hermes default after setup
--no-start Install files but leave bridge stopped
--bridge-ref REF Upstream git ref (default: main)
--commandcode-api-key KEY Upstream key; avoid this because shell history/processes may expose it
--skip-smoke Skip live Hermes completion verification
-h, --help Show this help
The normal safe path is localhost-only and uses an existing ~/.commandcode/auth.json.
USAGE
}
log() { printf '[%s] %s\n' "$APP_NAME" "$*"; }
fatal() { printf '[%s] ERROR: %s\n' "$APP_NAME" "$*" >&2; exit 1; }
while (($#)); do
case "$1" in
--yes|-y) ASSUME_YES=1; shift ;;
--host) (($# >= 2)) || fatal "--host requires a value"; HOST="$2"; shift 2 ;;
--port) (($# >= 2)) || fatal "--port requires a value"; PORT="$2"; shift 2 ;;
--model) (($# >= 2)) || fatal "--model requires a value"; MODEL="$2"; shift 2 ;;
--models) (($# >= 2)) || fatal "--models requires a value"; MODELS="$2"; shift 2 ;;
--use) USE=1; shift ;;
--no-start) NO_START=1; shift ;;
--bridge-ref) (($# >= 2)) || fatal "--bridge-ref requires a value"; UPSTREAM_REF="$2"; shift 2 ;;
--commandcode-api-key) (($# >= 2)) || fatal "--commandcode-api-key requires a value"; COMMANDCODE_API_KEY="$2"; shift 2 ;;
--skip-smoke) SKIP_SMOKE=1; shift ;;
--help|-h) usage; exit 0 ;;
*) fatal "unknown option: $1" ;;
esac
done
case "$HOST" in 127.0.0.1|0.0.0.0) ;; *) fatal "--host must be 127.0.0.1 or 0.0.0.0" ;; esac
case "$PORT" in ''|*[!0-9]*) fatal "--port must be numeric" ;; esac
((PORT >= 1 && PORT <= 65535)) || fatal "--port must be between 1 and 65535"
[[ "$MODEL" != *$'\n'* && "$MODELS" != *$'\n'* ]] || fatal "model values cannot contain newlines"
require() { command -v "$1" >/dev/null 2>&1 || fatal "required command not found: $1"; }
require git
require node
require npm
require hermes
require systemctl
require curl
require python3
[[ "$(uname -s)" == Linux ]] || fatal "this helper currently supports Linux user systemd only"
(( EUID != 0 )) || fatal "do not run as root; this uses user systemd"
NODE_MAJOR="$(node -p 'Number(process.versions.node.split(".")[0])')"
(( NODE_MAJOR >= 20 )) || fatal "Node.js 20+ required; found $(node --version)"
systemctl --user list-units >/dev/null 2>&1 || fatal "user systemd is unavailable; use a normal login session or enable linger"
TMP_DIR="$(mktemp -d -t commandcode-hermes-setup.XXXXXX)"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT
log "Cloning upstream bridge ref '$UPSTREAM_REF' into a temporary directory"
git clone --depth 1 --branch "$UPSTREAM_REF" "$UPSTREAM_REPO" "$TMP_DIR/commandcode-bridge" >/dev/null
cd "$TMP_DIR/commandcode-bridge"
INSTALL_ARGS=(--yes --host "$HOST" --port "$PORT" --no-start)
if [[ -n "$COMMANDCODE_API_KEY" ]]; then
INSTALL_ARGS+=(--commandcode-api-key "$COMMANDCODE_API_KEY")
fi
log "Running the upstream rootless installer (service start is deferred)"
./install.sh "${INSTALL_ARGS[@]}" >/dev/null
BRIDGE_ENV="$HOME/.config/commandcode-bridge/env"
[[ -f "$BRIDGE_ENV" ]] || fatal "upstream installer did not create $BRIDGE_ENV"
chmod 600 "$BRIDGE_ENV"
# Update only non-secret model settings. The upstream installer owns the env format;
# JSON quoting keeps commas, spaces, and shell metacharacters safe.
python3 - "$BRIDGE_ENV" "$MODELS" "$MODEL" <<'PY'
from pathlib import Path
import json
import re
import sys
env_path = Path(sys.argv[1])
models = sys.argv[2]
default_model = sys.argv[3]
text = env_path.read_text()
updates = {
"COMMANDCODE_ALLOWED_MODELS": models,
"COMMANDCODE_DEFAULT_MODEL": default_model,
}
for key, value in updates.items():
line = f"{key}={json.dumps(value)}"
pattern = rf"(?m)^{re.escape(key)}=.*$"
text, count = re.subn(pattern, line, text)
if count == 0:
text += "\n" + line + "\n"
env_path.write_text(text)
env_path.chmod(0o600)
PY
# Read the client-facing bridge token without displaying it. The upstream installer
# writes a systemd-compatible quoted env file.
BRIDGE_API_KEY="$(python3 - "$BRIDGE_ENV" <<'PY'
import shlex
import sys
from pathlib import Path
for raw in Path(sys.argv[1]).read_text().splitlines():
if raw.startswith("BRIDGE_API_KEY="):
value = raw.split("=", 1)[1]
print(shlex.split(value, comments=False, posix=True)[0] if value else "")
break
PY
)"
[[ -n "$BRIDGE_API_KEY" ]] || fatal "upstream installer did not create a bridge client key"
# Hermes stores provider credentials in its private .env. Update just this key,
# preserving all unrelated user configuration and enforcing mode 600.
HERMES_ENV="$(hermes config env-path)"
mkdir -p "$(dirname "$HERMES_ENV")"
python3 - "$HERMES_ENV" "$BRIDGE_API_KEY" <<'PY'
from pathlib import Path
import os
import re
import sys
path = Path(sys.argv[1]).expanduser()
value = sys.argv[2]
path.parent.mkdir(parents=True, exist_ok=True)
text = path.read_text() if path.exists() else ""
line = "COMMANDCODE_BRIDGE_API_KEY=" + value
pattern = r"(?m)^COMMANDCODE_BRIDGE_API_KEY=.*$"
if re.search(pattern, text):
text = re.sub(pattern, line, text)
else:
text = text.rstrip("\n") + ("\n" if text else "") + line + "\n"
path.write_text(text)
os.chmod(path, 0o600)
PY
# Register the provider through Hermes' supported config interface. Do not replace
# the user's active model unless --use was explicitly supplied.
hermes config set providers.commandcode.name commandcode >/dev/null
hermes config set providers.commandcode.api "http://127.0.0.1:${PORT}/v1" >/dev/null
hermes config set providers.commandcode.key_env COMMANDCODE_BRIDGE_API_KEY >/dev/null
hermes config set providers.commandcode.transport openai_chat >/dev/null
hermes config set providers.commandcode.default_model "$MODEL" >/dev/null
hermes config set providers.commandcode.models "[$MODELS]" >/dev/null
if (( NO_START == 0 )); then
log "Starting commandcode-bridge user service"
systemctl --user daemon-reload
systemctl --user enable --now commandcode-bridge >/dev/null
else
log "Bridge service was installed but not started (--no-start)"
fi
BASE_URL="http://${HOST}:${PORT}"
if (( NO_START == 0 )); then
log "Checking bridge health"
curl -fsS "$BASE_URL/health" >/dev/null || fatal "bridge health check failed"
log "Checking authenticated model discovery"
curl -fsS "$BASE_URL/v1/models" -H "Authorization: Bearer ${BRIDGE_API_KEY}" >/dev/null \
|| fatal "authenticated model discovery failed"
fi
if (( USE == 1 )); then
hermes config set model.provider custom:commandcode >/dev/null
hermes config set model.base_url "http://127.0.0.1:${PORT}/v1" >/dev/null
hermes config set model.default "$MODEL" >/dev/null
log "Hermes default switched to custom:commandcode / $MODEL"
else
log "Provider registered as custom:commandcode; existing Hermes default unchanged"
fi
if (( SKIP_SMOKE == 0 && NO_START == 0 )); then
log "Running Hermes completion smoke test"
smoke_output="$(hermes chat -Q --provider custom:commandcode -m "$MODEL" \
-q 'Reply exactly COMMANDCODE_HERMES_OK' 2>&1)" || {
printf '%s\n' "$smoke_output" >&2
fatal "Hermes smoke test failed; inspect: journalctl --user -u commandcode-bridge -n 80 --no-pager"
}
grep -Fq 'COMMANDCODE_HERMES_OK' <<<"$smoke_output" || {
printf '%s\n' "$smoke_output" >&2
fatal "Hermes responded but smoke marker was missing"
}
log "Hermes smoke test passed"
fi
cat <<EOF
Setup complete.
Provider: custom:commandcode
Endpoint: ${BASE_URL}/v1
Model: ${MODEL}
Service: commandcode-bridge (user systemd)
Config: ${BRIDGE_ENV} and Hermes private env
Try:
hermes chat -Q --provider custom:commandcode -m '${MODEL}' \\
-q 'Reply exactly COMMANDCODE_HERMES_OK'
Status:
systemctl --user status commandcode-bridge --no-pager
journalctl --user -u commandcode-bridge -n 80 --no-pager
EOF