# shellcheck shell=bash # linux/lib/core.sh - state, presets, the provider table, and which preset a mode resolves to. # # 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. # --------------------------------------------------------------------------- # State / presets # --------------------------------------------------------------------------- init_root() { mkdir -p "$CM_ROOT" "$CM_BIN" "$CM_PRESETS" "$CM_BACKUPS" "$CM_ROOT/vault"; } jget() { "$PY" "$JSON" get "$1" "$2" 2>/dev/null; } state_mode() { local m; m="$(jget "$CM_STATE" mode)"; [ -n "$m" ] && printf '%s' "$m" || printf 'anthropic'; } state_preset() { jget "$CM_STATE" preset; } preset_path() { printf '%s/%s.json' "$CM_PRESETS" "$1"; } # Preset names become file names, so nothing but a plain word gets through - no # slash, and no leading dot. Without this `preset show ../../etc/passwd` # printed whatever it pointed at. valid_preset_name() { case "$1" in ''|.*|*[!A-Za-z0-9._-]*) return 1 ;; esac return 0 } preset_names() { "$PY" "$JSON" presets "$CM_PRESETS" | cut -f1; } # Deliberately no awk anywhere in this script: it is absent from minimal images # (this was found the hard way on a stock Fedora WSL rootfs). Bash can split TSV # on its own, and cut/sed/grep are far more reliably present. presets_for_provider() { local name provider desc while IFS=$'\t' read -r name provider desc; do [ "$provider" = "$1" ] && printf '%s\n' "$name" done < <("$PY" "$JSON" presets "$CM_PRESETS") } # Read TSV on stdin, print the whole line whose first field equals $1. tsv_find() { local want="$1" line f1 while IFS= read -r line; do f1="${line%%$'\t'*}" if [ "$f1" = "$want" ]; then printf '%s' "$line"; return 0; fi done return 1 } tsv_field() { printf '%s' "$1" | cut -f"$2"; } # --------------------------------------------------------------------------- # Providers # # Every gateway provider is an entry in providers.json; this script only knows # the *kinds* of behaviour (how a catalogue is fetched, how a server is probed) # and picks one by name from the entry. The table is read once, in the main # shell before dispatch - read inside a $( ) it would be re-read on every # lookup. Columns are cm-json.py's PROVIDER_TSV: # # 1 id 2 aliases 3 title 4 label 5 color 6 defaultPreset # 7 serverEditable 8 probe 9 probePaths 10 catalogueKind 11 perServer # 12 setupKey 13 setupModels 14 keyUrl 15 guardrail 16 doctor # 17 defaultKeyRef 18 literalToken 19 defaultBaseUrl 20 serverHint # 21 serverStart # # Always read with cut, never `IFS=$'\t' read`: tab counts as whitespace to # read, so an empty column (most providers have no aliases) would vanish and # shift every column after it. # --------------------------------------------------------------------------- CM_PROVIDERS_TSV='' cm_providers_load() { CM_PROVIDERS_TSV="$("$PY" "$JSON" provider-tsv)" || { echo "claude-mode: could not read providers.json (next to $CM_BIN)" >&2; exit 1; } local id for id in $(provider_ids); do MODES+=("$id"); done } provider_ids() { printf '%s\n' "$CM_PROVIDERS_TSV" | cut -f1; } prov_field() { local row row="$(printf '%s\n' "$CM_PROVIDERS_TSV" | tsv_find "$1")" || return 1 tsv_field "$row" "$2" } # An id or an alias (z.ai, z-ai) to the provider id; non-zero if neither. provider_resolve() { local w line w="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" [ -n "$w" ] || return 1 while IFS= read -r line; do [ -n "$line" ] || continue if [ "${line%%$'\t'*}" = "$w" ]; then printf '%s\n' "$w"; return 0; fi case ",$(tsv_field "$line" 2)," in *",$w,"*) printf '%s\n' "${line%%$'\t'*}"; return 0 ;; esac done </dev/null 2>&1; then c="$(tput cols 2>/dev/null)"; fi if [ -z "$c" ] && command -v stty >/dev/null 2>&1; then c="$(stty size 2>/dev/null | cut -d' ' -f2)" fi [ -z "$c" ] && c="${COLUMNS:-80}" case "$c" in ''|*[!0-9]*) c=80 ;; esac [ "$c" -gt 20 ] || c=80 printf '%s' "$c" } # The preset `claude-mode ` uses when none is named: the one chosen # with `preset default`, while its file still exists, else the built-in name. # The existence test is what lets a stale choice degrade instead of break. # cm-json.py's BUILTIN_DEFAULT_PRESET and the widget's Modes.DEFAULT_PRESET # mirror the built-in names. builtin_default_preset() { prov_field "$1" 6; } default_preset_for() { local chosen chosen="$(jget "$CM_DEFAULTS" "$1")" if [ -n "$chosen" ] && valid_preset_name "$chosen" && [ -f "$(preset_path "$chosen")" ]; then printf '%s' "$chosen"; return 0 fi builtin_default_preset "$1" } resolve_preset() { local provider="$1" requested="${2:-}" fallback first if [ -n "$requested" ]; then [ -f "$(preset_path "$requested")" ] || { err "preset '$requested' not found"; return 1; } local got; got="$(jget "$(preset_path "$requested")" provider)" [ -z "$got" ] && got=openrouter if [ "$got" != "$provider" ]; then err "preset '$requested' is a '$got' preset, not '$provider'"; return 1 fi printf '%s' "$requested"; return 0 fi fallback="$(default_preset_for "$provider")" if [ -n "$fallback" ] && [ -f "$(preset_path "$fallback")" ]; then printf '%s' "$fallback"; return 0 fi first="$(presets_for_provider "$provider" | head -n1)" [ -n "$first" ] || { err "no preset found for provider '$provider'"; return 1; } printf '%s' "$first" }