Files
claude-mode/linux/install.sh
T
smoidoandClaude Opus 5 b5824c6611 Providers as data; add Ollama and Custom endpoints
Every gateway provider is now an entry in providers.json - endpoint and auth
template, how its model list is read, how it is probed before a switch, what
setup asks, which doctor checks apply, and its title, colour and logo -
installed next to the presets and read by all three consumers: the bash CLI
(through cm-json.py), the Windows script, and the bar widget (through
health.json). anthropic stays built in; it is the native login, not a gateway.

Behaviour that differs in kind stays in code, chosen by name from the entry:
catalogue parsers (openrouter, lmstudio, ollama, openai, static), probe rules
(always, lenient, local), and named doctor checks. A provider that reuses them
is an entry and a default preset, with no code. The widget draws providers
from health.json, so a new one needs no QML change and no shell restart.

The existing three are unchanged in behaviour: their blank presets come out
byte-identical from the file, and setup, doctor, models and the picker run the
same checks through the generic paths.

Ollama: local server on :11434, placeholder token, one model for every tier,
models from /api/tags. doctor reads the context each loaded model actually runs
with (/api/ps) and its maximum (/api/show), because Ollama defaults to 4096
tokens unless OLLAMA_CONTEXT_LENGTH is set and silently truncates past it. A
bare model name matches its :latest tag.

Custom: any Anthropic-compatible endpoint. Ships with no address and is refused
until it has one; key optional; models from /v1/models when the endpoint has a
list, and a lenient probe so a proxy without one is not blocked.

Also: preflight (and Set-ClaudeMode on Windows) refuses a preset with no
server address; the server form, setup and set-auth use each provider's own
default URL, key name and placeholder token instead of LM Studio's; the
Windows build gains the no-models and no-address guards it never had.
Tested on Linux against fake Ollama/Custom servers, and on Windows 5.1 in a
USERPROFILE sandbox on winbox.

1.12.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 01:20:33 +03:00

182 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# One-time installer for claude-mode (Linux / macOS).
#
# Installs to ~/.claude-mode, symlinks the entry point into ~/.local/bin, and
# adds a marked block to your shell rc for the `claude` wrapper.
#
# Idempotent: safe to re-run to upgrade. Existing presets are kept unless
# --force is passed. ~/.claude/settings.json is NOT touched here - only by an
# actual `claude-mode <mode>`.
set -euo pipefail
FORCE=0
SKIP_KEY=0
for arg in "$@"; do
case "$arg" in
--force) FORCE=1 ;;
--skip-key-prompt) SKIP_KEY=1 ;;
-h|--help) echo "usage: install.sh [--force] [--skip-key-prompt]"; exit 0 ;;
esac
done
ROOT="${CM_ROOT:-$HOME/.claude-mode}"
BINDIR="$HOME/.local/bin"
green() { printf ' \033[32mok \033[0m %s\n' "$*"; }
warn() { printf ' \033[33mwarn\033[0m %s\n' "$*"; }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; }
# A checkout next to this script is the normal source. Piped in
# (curl ... | bash) there is no script file and no checkout, so fetch the
# repository archive and install from that instead.
SRC="$(cd "$(dirname "${BASH_SOURCE[0]:-''}")" 2>/dev/null && pwd)" || SRC=""
if [ ! -f "$SRC/claude-mode" ] || [ ! -f "$SRC/cm-json.py" ]; then
REPO_URL="${CM_REPO_URL:-https://git.nebulm.com/smoido/claude-mode}"
command -v curl >/dev/null 2>&1 || { fail 'curl required to fetch the source'; exit 1; }
TMP="$(mktemp -d "${TMPDIR:-/tmp}/claude-mode-src.XXXXXX")"
printf 'no local checkout found - fetching source from %s\n' "$REPO_URL"
curl -fsSL "$REPO_URL/archive/master.tar.gz" | tar -xz -C "$TMP"
# a plain glob rather than find -print -quit: BSD find (macOS) has no -quit
SRC=''
for _c in "$TMP"/*/linux/claude-mode "$TMP"/linux/claude-mode; do
[ -f "$_c" ] && { SRC="$(dirname "$_c")"; break; }
done
[ -n "$SRC" ] || { fail 'archive did not contain linux/claude-mode - repo layout changed?'; exit 1; }
fi
printf '\ninstalling claude-mode -> %s\n' "$ROOT"
# --- preflight -------------------------------------------------------------
PY="${CLAUDE_MODE_PYTHON:-python3}"
if ! command -v "$PY" >/dev/null 2>&1; then
fail "python3 not found. claude-mode uses it for JSON handling."
fail "install it (apt install python3 / dnf install python3 / brew install python) and re-run."
exit 1
fi
green "python3: $(command -v "$PY")"
if ! command -v curl >/dev/null 2>&1; then
warn 'curl not found - `models` and `doctor` network checks will not work'
fi
if ! command -v claude >/dev/null 2>&1; then
warn 'claude is not on PATH - claude-mode installs anyway, but nothing uses its config yet'
fi
# --- directories -----------------------------------------------------------
mkdir -p "$ROOT/bin" "$ROOT/presets" "$ROOT/vault" "$ROOT/backups" "$BINDIR"
chmod 700 "$ROOT" "$ROOT/vault" 2>/dev/null || true
# --- payload ---------------------------------------------------------------
install -m 0755 "$SRC/claude-mode" "$ROOT/bin/claude-mode"
install -m 0755 "$SRC/claude-key-helper.sh" "$ROOT/bin/claude-key-helper.sh"
install -m 0644 "$SRC/cm-json.py" "$ROOT/bin/cm-json.py"
install -m 0644 "$SRC/cm-vault.sh" "$ROOT/bin/cm-vault.sh"
green 'copied claude-mode + helpers'
# cm_version() reads this; without it every health.json reported 0.0.0.
VERSION_SRC="$SRC/../VERSION"
[ -f "$VERSION_SRC" ] || VERSION_SRC="$SRC/VERSION"
if [ -f "$VERSION_SRC" ]; then
install -m 0644 "$VERSION_SRC" "$ROOT/VERSION"
green "version $(tr -d '[:space:]' < "$ROOT/VERSION")"
fi
# --- providers (shared with the Windows build) -----------------------------
# Always replaced, unlike presets: it is the tool's own table of what each
# provider is, not something a user edits. cm-json.py looks for it one level
# above bin/.
PROVIDERS_SRC="$SRC/../providers.json"
[ -f "$PROVIDERS_SRC" ] || PROVIDERS_SRC="$SRC/providers.json"
if [ -f "$PROVIDERS_SRC" ]; then
install -m 0644 "$PROVIDERS_SRC" "$ROOT/providers.json"
green "providers: $("$PY" -c 'import json,sys; print(", ".join(p["id"] for p in json.load(open(sys.argv[1]))["providers"]))' "$ROOT/providers.json")"
else
fail 'providers.json missing from the payload - claude-mode cannot run without it'
exit 1
fi
# --- presets (shared with the Windows build) -------------------------------
PRESET_SRC="$SRC/../presets"
[ -d "$PRESET_SRC" ] || PRESET_SRC="$SRC/presets"
if [ -d "$PRESET_SRC" ]; then
for f in "$PRESET_SRC"/*.json; do
[ -e "$f" ] || continue
base="$(basename "$f")"
if [ -e "$ROOT/presets/$base" ] && [ "$FORCE" -eq 0 ]; then
printf ' skip preset %s (exists; --force to overwrite)\n' "${base%.json}"
else
install -m 0644 "$f" "$ROOT/presets/$base"
green "preset ${base%.json}"
fi
done
else
warn 'no presets directory found in the payload'
fi
# --- initial state ---------------------------------------------------------
if [ ! -f "$ROOT/state.json" ]; then
printf '{\n "mode": "anthropic",\n "preset": "",\n "writtenEnvKeys": []\n}\n' > "$ROOT/state.json"
green 'state.json initialised (mode=anthropic)'
fi
# --- PATH entry ------------------------------------------------------------
ln -sf "$ROOT/bin/claude-mode" "$BINDIR/claude-mode"
green "linked $BINDIR/claude-mode"
case ":$PATH:" in
*":$BINDIR:"*) ;;
*) warn "$BINDIR is not on PATH - add it in your shell rc: export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
esac
# --- shell rc block --------------------------------------------------------
SNIPPET="$SRC/shell-snippet.sh"
START='# >>> claude-mode >>>'
END='# <<< claude-mode <<<'
add_block() {
local rc="$1"
[ -f "$rc" ] || return 0
if grep -qF "$START" "$rc" 2>/dev/null; then
# replace the existing block in place
"$PY" - "$rc" "$SNIPPET" "$START" "$END" <<'PYEOF'
import sys, re
rc, snip, start, end = sys.argv[1:5]
body = open(rc, encoding='utf-8').read()
new = open(snip, encoding='utf-8').read().rstrip('\n')
pattern = re.compile(re.escape(start) + r'.*?' + re.escape(end), re.S)
open(rc, 'w', encoding='utf-8').write(pattern.sub(lambda _: new, body))
PYEOF
green "updated claude-mode block in $rc"
else
printf '\n%s\n' "$(cat "$SNIPPET")" >> "$rc"
green "appended claude-mode block to $rc"
fi
}
RC_TOUCHED=0
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
if [ -f "$rc" ]; then add_block "$rc"; RC_TOUCHED=1; fi
done
[ "$RC_TOUCHED" -eq 0 ] && warn 'no ~/.bashrc or ~/.zshrc found - the `claude` wrapper was not installed'
# --- key -------------------------------------------------------------------
if [ "$SKIP_KEY" -eq 0 ]; then
# shellcheck source=/dev/null
. "$ROOT/bin/cm-vault.sh"
if cm_vault_has openrouter && [ "$FORCE" -eq 0 ]; then
green 'OpenRouter key already stored'
else
printf '\n'
"$ROOT/bin/claude-mode" set-key openrouter || warn 'key not stored - run `claude-mode set-key openrouter` later'
fi
fi
# --- machine-readable state ------------------------------------------------
"$ROOT/bin/claude-mode" health >/dev/null 2>&1 && green 'health.json seeded'
printf '\ndone. Open a new shell, then:\n'
printf ' claude-mode\n'
printf ' claude-mode status\n'
printf ' claude-mode doctor\n'