# shellcheck shell=bash # linux/lib/setup.sh - first-run setup: key, server, models. # # Part of the claude-mode CLI: sourced by linux/claude-mode, which sets the # CM_* paths, PY and JSON used here. Not meant to run on its own. # --------------------------------------------------------------------------- # First-run setup # # A shipped preset is a starting point, not a working configuration. OpenRouter # needs a key before it can serve anything; LM Studio needs to be told where the # server is and which of the models it actually has installed to use - and the # ids it ships with are whatever happened to be on the machine this was written # on, which is almost certainly not yours. # # So a preset says whether it has been through setup. `configured: false` is # written into the shipped presets and cleared once setup has run, and preflight # treats it as a blocker: better to be walked through it once than to switch # into something that half-works and produces a confusing failure later. # # Absent means configured. That is deliberate - presets that predate this, and # ones the user built by hand with `preset new`, are their own business and must # not suddenly start demanding a wizard. # --------------------------------------------------------------------------- preset_configured() { local v; v="$(jget "$1" configured)" [ "$v" = "false" ] && return 1 return 0 } mark_configured() { "$PY" "$JSON" set-flag "$1" configured true >/dev/null 2>&1 } # A y/N prompt that defaults to no on anything that is not a clear yes. ask_yes() { local prompt="$1" reply printf ' %s [y/N] ' "$prompt" IFS= read -r reply || return 1 case "$reply" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac } # Prompt with a default shown in brackets; empty input keeps the default. # # The prompt goes to stderr for the same reason warn/err do: this is called # inside $( ), where anything on stdout is captured as the return value. Printed # to stdout it came back as part of the answer - " server base URL [...]: " with # the typed URL glued on the end, which set-url then rejected. ask_value() { local prompt="$1" default="$2" reply if [ -n "$default" ]; then printf ' %s [%s]: ' "$prompt" "$default" >&2 else printf ' %s: ' "$prompt" >&2; fi IFS= read -r reply || return 1 [ -n "$reply" ] && printf '%s' "$reply" || printf '%s' "$default" } setup_key() { local ref="$1" label="$2" if cm_vault_has "$ref"; then ok "a key is already stored for '$ref' ($(cm_vault_backend_label))" ask_yes "replace it?" || return 0 else say "$label needs an API key. It goes into $(cm_vault_backend_label)," say 'not into settings.json.' fi cmd_set_key "$ref" } # Offer the provider's own catalogue rather than asking someone to type a model # id from memory. Falls back to typing when the catalogue cannot be reached, # because being offline should not block finishing setup. setup_models() { local pf="$1" provider="$2" name="$3" printf '\n' say 'current model map:' local t v for t in "${TIERS[@]}"; do v="$(jget "$pf" "models.$t")" [ -n "$v" ] && printf ' %-8s %s\n' "$t" "$v" done printf '\n' ask_yes 'change which models back these tiers?' || return 0 if ! ui_interactive; then warn 'model picking needs an interactive terminal' return 0 fi if [ "$(prov_field "$provider" 13)" = one-for-all ]; then # One model for every tier is the normal shape for a local server: it # has one loaded at a time, and mapping tiers to different models just # means paying the load cost on every tier change. local base ids=() id st ctx base="$(jget "$pf" baseUrl)" while IFS=$'\t' read -r id st ctx; do [ -n "$id" ] || continue ids+=("$id") done < <(provider_catalogue "$provider" "$base" "$(cm_preset_token "$pf")") if [ "${#ids[@]}" -eq 0 ]; then warn 'the server returned no models; type an id by hand instead' local manual; manual="$(ask_value 'model id for every tier' "$(jget "$pf" models.opus)")" [ -n "$manual" ] && "$PY" "$JSON" set-all "$pf" "$manual" >/dev/null && ok "all tiers -> $manual" return 0 fi ui_reset_items for id in "${ids[@]}"; do ui_add_item "$id" 'use this for every tier'; done if ui_filter_select "model for all tiers of '$name'" 'esc = keep current'; then "$PY" "$JSON" set-all "$pf" "${ids[$UI_SEL]}" >/dev/null ok "all tiers -> ${ids[$UI_SEL]}" fi return 0 fi # Remote gateways map a different model per tier, which is the whole point # of them, so each tier is asked for separately. local cur for t in "${TIERS[@]}"; do cur="$(jget "$pf" "models.$t")" ui_pick_model "$pf" "$t" "$cur" || continue [ -n "$UI_PICKED" ] || continue "$PY" "$JSON" set-tier "$pf" "$t" "$UI_PICKED" >/dev/null && ok "$t -> $UI_PICKED" done } # Where a server provider (LM Studio, Ollama, a custom endpoint) lives, and # whether it wants a key. None of them has to be on this machine. setup_server() { local pf="$1" name="$2" mode="$3" url probe token title hint start title="$(prov_field "$mode" 3)"; hint="$(prov_field "$mode" 20)"; start="$(prov_field "$mode" 21)" url="$(jget "$pf" baseUrl)"; [ -n "$url" ] || url="$(prov_field "$mode" 19)" printf '\n' [ -n "$hint" ] && say "$hint" url="$(ask_value 'server base URL' "$url")" [ -n "$url" ] || { err 'a server address is needed'; return 1; } "$PY" "$JSON" set-url "$pf" "$url" >/dev/null || return 1 ok "baseUrl -> $url" printf '\n' if ask_yes 'does that server require an API key?'; then local ref; ref="$(ask_value 'key name to store it under' "$(prov_field "$mode" 17)")" "$PY" "$JSON" set-auth "$pf" key "$ref" >/dev/null ok "auth -> vault key '$ref'" cm_vault_has "$ref" || cmd_set_key "$ref" token="$(cm_vault_get "$ref" 2>/dev/null || true)" else "$PY" "$JSON" set-auth "$pf" none >/dev/null token="$(prov_field "$mode" 18)" ok "auth -> none (inline placeholder token '$token')" fi printf '\n' say "checking $url ..." probe="$(cm_probe_server "$url" "$token" "$(prov_field "$mode" 9)")" case "$probe" in ok) ok 'server answered' ;; auth) err 'the server refused that credential'; return 1 ;; notfound) if [ "$(prov_field "$mode" 8)" = lenient ]; then warn "something answered, but it lists no models - fine for a proxy; model ids are typed by hand" else err "something answered there, but not a $title API"; return 1 fi ;; skip) warn 'curl is missing, so the server was not checked' ;; *) err 'nothing answered at that address' say "${start:+$start, then }run: claude-mode setup $mode" return 1 ;; esac return 0 } cmd_setup() { local mode="${1:-}" name pf case "$mode" in anthropic) head_ 'setup: anthropic' ok 'nothing to configure - it uses your existing Claude login' return 0 ;; '') err 'usage: claude-mode setup '; return 1 ;; *) mode="$(provider_resolve "$mode")" || { err "unknown mode '${1:-}'"; return 1; } ;; esac name="$(resolve_preset "$mode" "${2:-}")" || return 1 pf="$(preset_path "$name")" if ! ui_interactive; then err 'setup needs an interactive terminal' say "run: claude-mode setup $mode" return 1 fi head_ "setup: $mode / preset '$name'" say "$(mode_label "$mode")" # A server provider is asked where it is and whether it wants a key; a # hosted one always wants a key, and the entry says where to get one. if [ "$(prov_field "$mode" 7)" = 1 ]; then setup_server "$pf" "$name" "$mode" || return 1 else local ref url ref="$(jget "$pf" auth.keyRef)"; [ -n "$ref" ] || ref="$(prov_field "$mode" 17)" url="$(prov_field "$mode" 14)" printf '\n' [ -n "$url" ] && say "get a key from $url" setup_key "$ref" "$(prov_field "$mode" 3)" fi setup_models "$pf" "$mode" "$name" mark_configured "$pf" printf '\n' ok "$mode is set up" say "switch to it with: claude-mode $mode" return 0 }