It was created 100644 for the curl | sh path, where the mode doesn't matter. Since bootstrap.sh started calling it via run_module, which execs the script directly, the module failed with "Permission denied" (126) and the sync hook / LITELLM_KEY never got deployed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
182 lines
7.7 KiB
Bash
Executable File
182 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env sh
|
|
# One-time bootstrap for the covert-gpt MCP gateway on a user's laptop.
|
|
#
|
|
# curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/mcp-config/install.sh | sh
|
|
#
|
|
# It is interactive (prompts for the user's LiteLLM key) UNLESS LITELLM_KEY is
|
|
# already set in the environment, e.g.:
|
|
# curl -fsSL .../install.sh | LITELLM_KEY=sk-... sh
|
|
#
|
|
# What it installs (all under the user's home, no root):
|
|
# ~/.config/mcp-sync/sync.sh the per-shell refresh hook (from Gitea)
|
|
# ~/.config/mcp-sync/key the user's LiteLLM virtual key (chmod 600)
|
|
# a line in the user's shell rc that sources sync.sh on each shell start
|
|
# After this, every new shell refreshes the gateway config automatically.
|
|
set -eu
|
|
|
|
# Fallback source, used ONLY when this script is not run from a checkout.
|
|
# A Gitea *API* endpoint, not the web /raw/ route: /raw/ answers an
|
|
# unauthorized request with a 200 and an HTML login page, which `curl -f`
|
|
# reports as success. The API returns an honest 401/403. Set GITEA_KEY to read
|
|
# a private repo.
|
|
API_BASE="${MCP_SYNC_API_BASE:-https://git.nic-oconnor.com/api/v1/repos/public/dotfiles}"
|
|
REF="${MCP_SYNC_REF:-main}"
|
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null)" || SCRIPT_DIR=""
|
|
SYNC_HOME="$HOME/.config/mcp-sync"
|
|
SYNC_SH="$SYNC_HOME/sync.sh"
|
|
KEYFILE="$SYNC_HOME/key"
|
|
HOOK_MARKER="# >>> covert-gpt mcp-sync >>>"
|
|
|
|
say() { printf '%s\n' "$*" >&2; }
|
|
|
|
command -v curl >/dev/null 2>&1 || { say "error: curl is required"; exit 1; }
|
|
|
|
mkdir -p "$SYNC_HOME"
|
|
|
|
# --- 1. install the sync hook ----------------------------------------------
|
|
# Prefer the copy sitting next to this script: when bootstrap.sh runs the
|
|
# module from a checkout there is no reason to go to the network, and it makes
|
|
# the module work on a box that cannot reach Gitea at all.
|
|
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/sync.sh" ]; then
|
|
say "Installing mcp-sync hook from $SCRIPT_DIR/sync.sh"
|
|
cp "$SCRIPT_DIR/sync.sh" "$SYNC_SH"
|
|
else
|
|
SYNC_URL="$API_BASE/raw/mcp-config/sync.sh?ref=$REF"
|
|
say "Fetching mcp-sync hook..."
|
|
# Fetch to a temp file and CHECK IT before overwriting the installed hook.
|
|
# The API endpoint gives an honest 4xx, but keep the shebang check as a
|
|
# backstop: if anything ever writes an HTML login page over sync.sh, the shell
|
|
# fails to parse it, the hook silently does nothing, OPENCODE_CONFIG is never
|
|
# exported, and opencode comes up with no covert-gpt provider at all.
|
|
_tmp="$(mktemp)" || { say "error: mktemp failed"; exit 1; }
|
|
if [ -n "${GITEA_KEY:-}" ]; then
|
|
set -- -H "Authorization: token $GITEA_KEY"
|
|
else
|
|
set --
|
|
fi
|
|
if ! curl -fsSL -m 15 "$@" "$SYNC_URL" -o "$_tmp" || [ ! -s "$_tmp" ]; then
|
|
rm -f "$_tmp"
|
|
say "error: could not fetch $SYNC_URL"
|
|
[ -n "${GITEA_KEY:-}" ] ||
|
|
say " The repo may be private — export GITEA_KEY and re-run."
|
|
exit 1
|
|
fi
|
|
case "$(head -c 2 "$_tmp")" in
|
|
'#!') mv "$_tmp" "$SYNC_SH" ;;
|
|
*)
|
|
rm -f "$_tmp"
|
|
say "error: $SYNC_URL did not return a script."
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
chmod 0644 "$SYNC_SH"
|
|
|
|
# --- 1b. record the checkout as the config source --------------------------
|
|
# THE POINT OF THIS FILE: bootstrap.sh already cloned this repo over
|
|
# authenticated git, so the per-shell hook must never have to re-fetch the
|
|
# gateway config anonymously over HTTP. Needing that was the only reason the
|
|
# repo ever had to be world-readable. Record where we ran from and sync.sh
|
|
# reads both the config and the skills straight off disk.
|
|
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/opencode.gateway.json" ]; then
|
|
if REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd 2>/dev/null)"; then
|
|
printf '%s\n' "$REPO_DIR" > "$SYNC_HOME/source"
|
|
say "Config source: $REPO_DIR (local checkout, no network needed)."
|
|
fi
|
|
fi
|
|
|
|
# --- 2. capture the LiteLLM key -------------------------------------------
|
|
# An already-stored key is as good as one in the env: bootstrap.sh re-runs this
|
|
# module on every pass, and prompting a box that is already configured would
|
|
# make an otherwise unattended bootstrap hang on a TTY read.
|
|
if [ "${LITELLM_KEY:-}" = "" ] && [ -f "$KEYFILE" ]; then
|
|
LITELLM_KEY="$(cat "$KEYFILE" 2>/dev/null || true)"
|
|
[ -n "$LITELLM_KEY" ] && say "Reusing the key already stored at $KEYFILE."
|
|
fi
|
|
if [ "${LITELLM_KEY:-}" = "" ]; then
|
|
# Read from the controlling TTY since stdin is the piped script.
|
|
if [ -r /dev/tty ]; then
|
|
printf 'Paste your LiteLLM virtual key (sk-...): ' >&2
|
|
IFS= read -r LITELLM_KEY < /dev/tty
|
|
else
|
|
say "error: no LITELLM_KEY in env and no TTY to prompt."
|
|
say "re-run as: curl ... | LITELLM_KEY=sk-... sh"
|
|
exit 1
|
|
fi
|
|
fi
|
|
LITELLM_KEY="$(printf '%s' "$LITELLM_KEY" | tr -d ' \t\r\n')"
|
|
case "$LITELLM_KEY" in
|
|
sk-*) : ;;
|
|
*) say "warning: key does not start with 'sk-' — continuing anyway." ;;
|
|
esac
|
|
printf '%s' "$LITELLM_KEY" > "$KEYFILE"
|
|
chmod 0600 "$KEYFILE"
|
|
say "Key stored at $KEYFILE (0600)."
|
|
|
|
# --- 3. wire the shell rc --------------------------------------------------
|
|
# For zsh the hook goes in ~/.zshrc.local, NOT ~/.zshrc. The dotfiles repo
|
|
# MANAGES ~/.zshrc and overwrites it wholesale on every bootstrap
|
|
# (zsh/install.sh), so a hook written there survives only until the next
|
|
# bootstrap run — and then models and skills silently vanish, because sync.sh
|
|
# never runs and OPENCODE_CONFIG is never exported. ~/.zshrc.local is the
|
|
# unmanaged file the managed zshrc sources last, so the hook persists.
|
|
case "${SHELL:-}" in
|
|
*zsh) RC="$HOME/.zshrc.local" ;;
|
|
*bash) RC="$HOME/.bashrc" ;;
|
|
*) [ -f "$HOME/.zshrc" ] && RC="$HOME/.zshrc.local" || RC="$HOME/.bashrc" ;;
|
|
esac
|
|
touch "$RC"
|
|
# It sits alongside the user's secrets; keep it non-readable by others.
|
|
[ "$RC" = "$HOME/.zshrc.local" ] && chmod 0600 "$RC" 2>/dev/null || true
|
|
|
|
if grep -qF "$HOOK_MARKER" "$RC" 2>/dev/null; then
|
|
say "Shell hook already present in $RC."
|
|
else
|
|
{
|
|
printf '\n%s\n' "$HOOK_MARKER"
|
|
printf '%s\n' '[ -f "$HOME/.config/mcp-sync/sync.sh" ] && . "$HOME/.config/mcp-sync/sync.sh"'
|
|
printf '%s\n' "# <<< covert-gpt mcp-sync <<<"
|
|
} >> "$RC"
|
|
say "Added mcp-sync hook to $RC."
|
|
fi
|
|
|
|
# --- 4. run it once now so the config exists immediately -------------------
|
|
# shellcheck disable=SC1090
|
|
. "$SYNC_SH" || true
|
|
|
|
# If this is the initial install (key just written now), restart Hermes.
|
|
# The key file mtime changed, but we need to compare hashes to know if it's
|
|
# actually new (not just a touch). Only restart if we're not in a re-run.
|
|
if [ -f "$KEYFILE" ]; then
|
|
KEY_HASH="$(sha256sum "$KEYFILE" 2>/dev/null | cut -d' ' -f1)"
|
|
PREV_KEY_HASH=""
|
|
if [ -f "$HOME/.config/mcp-sync/.key-hash" ]; then
|
|
PREV_KEY_HASH="$(cat "$HOME/.config/mcp-sync/.key-hash" 2>/dev/null || true)"
|
|
fi
|
|
if [ "$KEY_HASH" != "$PREV_KEY_HASH" ]; then
|
|
# Key changed - update .env and restart Hermes if running
|
|
if [ -f "$HOME/.hermes/.env" ]; then
|
|
say "hermes: Key changed during install — updating ~/.hermes/.env"
|
|
if command -v sed >/dev/null 2>&1; then
|
|
sed -i "s|^LITELLM_KEY=.*$|LITELLM_KEY=$LITELLM_KEY|" "$HOME/.hermes/.env"
|
|
else
|
|
awk -v key="$LITELLM_KEY" '/^LITELLM_KEY=/ {print "LITELLM_KEY=" key; next} {print}' "$HOME/.hermes/.env" >"$HOME/.hermes/.env.tmp" && mv "$HOME/.hermes/.env.tmp" "$HOME/.hermes/.env"
|
|
fi
|
|
chmod 0600 "$HOME/.hermes/.env"
|
|
fi
|
|
if pgrep -f 'hermes-agent/hermes' >/dev/null 2>&1; then
|
|
HERMES_PID="$(pgrep -f 'hermes-agent/hermes')"
|
|
say "hermes: Restarting to pick up new key (PID: $HERMES_PID)..."
|
|
kill "$HERMES_PID" || true
|
|
sleep 1
|
|
(cd "$HOME" && nohup hermes >/dev/null 2>&1 &)
|
|
say "hermes: Hermes restarted."
|
|
fi
|
|
fi
|
|
printf '%s\n' "$KEY_HASH" >"$HOME/.config/mcp-sync/.key-hash"
|
|
fi
|
|
|
|
say ""
|
|
say "Done. Open a new terminal (or 'source $RC') and run opencode."
|
|
say "The gateway config refreshes automatically on each new shell."
|