#!/bin/sh
# =============================================================================
#  Bonsai on your own machine — macOS and Linux
#
#  Asks you nothing. No account, no API key, no Hugging Face token, no sudo, no
#  compiler, no Python. Downloads a model and a server, starts them, makes them
#  come back after a reboot. Then aitherium.com finds it on its own.
#
#  OPTIONAL FULL STACK:  --with-adk  [--api-key <key>]
#      Also installs aither-adk (into its OWN venv — Debian 12+ is PEP 668
#      externally-managed and a plain pip install fails there, including on
#      Android's Linux terminal) and enrols this device with the portal, so it
#      shows up as yours. Enrolment is outbound HTTPS only: no WireGuard, no
#      root, no inbound port. `adk mesh onboard` is the other path and needs
#      wg-quick plus /etc/wireguard, which an Android VM does not reliably have.
#      Pass --api-key (or set AITHER_API_KEY) to attach this device to YOUR
#      workspace as an endpoint. Without it adk refuses to enrol rather than
#      registering the device under the fallback tenant "personal" — which used
#      to succeed and silently put the device in the wrong place.
#      Everything the venv touches lives under ~/.aitherium/bonsai, so
#      --uninstall still removes all of it.
#
#  Every question an installer asks is a place a person stops. Hardware decides
#  the model size and the backend, and hardware can be detected — so it is.
#
#  Installs to ~/.aitherium/bonsai (bin/, models/) plus one user-level autostart
#  entry. Reverse the whole thing with:  ./install-bonsai.sh --uninstall
#
#  BINDS 127.0.0.1 ON PURPOSE — this is your machine, and 0.0.0.0 would publish
#  an unauthenticated inference server to your whole network. The browser can
#  still reach loopback from an HTTPS page under the secure-context exception.
# =============================================================================
set -eu

MODEL="auto"; PORT="8080"; AUTOSTART=1; UNINSTALL=0; WITH_ADK=0; DEPS_ONLY=0; VERIFY_ONLY=0
API_KEY="${AITHER_API_KEY:-}"
PORTAL="${AITHER_PORTAL_URL:-https://portal.aitherium.com}"
while [ $# -gt 0 ]; do
    case "$1" in
        --model) MODEL="$2"; shift 2 ;;
        --port) PORT="$2"; shift 2 ;;
        --no-autostart) AUTOSTART=0; shift ;;
        --uninstall) UNINSTALL=1; shift ;;
        --with-adk) WITH_ADK=1; shift ;;
        --deps-only) DEPS_ONLY=1; shift ;;
        --verify-only) VERIFY_ONLY=1; shift ;;
        --api-key) API_KEY="$2"; WITH_ADK=1; shift 2 ;;
        --portal) PORTAL="$2"; shift 2 ;;
        -h|--help) sed -n '2,20p' "$0"; exit 0 ;;
        *) echo "unknown option: $1"; exit 1 ;;
    esac
done

ROOT="$HOME/.aitherium/bonsai"
BIN="$ROOT/bin"; MODELS="$ROOT/models"
RELEASE="prism-b9596-9fcaed7"
RELBASE="https://github.com/PrismML-Eng/llama.cpp/releases/download/$RELEASE"

say()  { printf '  %s\n' "$*"; }
step() { printf '\n\033[36m→ %s\033[0m\n' "$*"; }
warn() { printf '  \033[33m! %s\033[0m\n' "$*"; }
die()  { printf '\n\033[31m✗ %s\033[0m\n' "$*" >&2; exit 1; }

OS="$(uname -s)"

stop_server() {
    pkill -f "$BIN/llama-server" 2>/dev/null || true
}

if [ "$UNINSTALL" = "1" ]; then
    step "Removing Bonsai"
    stop_server
    if [ "$OS" = "Darwin" ]; then
        launchctl unload "$HOME/Library/LaunchAgents/com.aitherium.bonsai.plist" 2>/dev/null || true
        rm -f "$HOME/Library/LaunchAgents/com.aitherium.bonsai.plist"
    else
        systemctl --user disable --now aitherium-bonsai.service 2>/dev/null || true
        rm -f "$HOME/.config/systemd/user/aitherium-bonsai.service"
    fi
    rm -rf "$ROOT"
    say "Gone. Nothing of it is left on this machine."
    exit 0
fi

# --verify-only asks an ALREADY-RUNNING server a question and exits on the answer. It exists so
# an orchestrator can assert "this endpoint works" as its own stage without re-implementing the
# request. Inlining curl and JSON into a playbook is how quoting bugs ship — the first draft of
# bootstrap-bonsai-device.psd1 did exactly that and would not even parse.
if [ "$VERIFY_ONLY" = "1" ]; then
    _r="$(curl -fsS --max-time 180 "http://127.0.0.1:$PORT/v1/chat/completions" \
        -H 'Content-Type: application/json' \
        -d '{"model":"bonsai-selfhost","messages":[{"role":"user","content":"Reply with the single word: ready"}],"max_tokens":256}' \
        2>/dev/null || true)"
    _a="$(printf '%s' "$_r" | tr ',' '\n' | grep '"content"' | tail -1 | cut -d'"' -f4)"
    if [ -n "$_a" ]; then
        printf '  endpoint answers: %s\n' "$_a"
        exit 0
    fi
    printf '  endpoint on port %s did not answer\n' "$PORT" >&2
    exit 1
fi

printf '\n\033[32m  Bonsai — your own model, on your own machine\033[0m\n'
printf '  ============================================\n'

# ── 1. look at the machine ───────────────────────────────────────────────────
step "Looking at this machine"

ARCH="$(uname -m)"
case "$ARCH" in
    x86_64|amd64) ARCH=x64 ;;
    aarch64|arm64) ARCH=arm64 ;;
esac

# RAM DETECTION MUST NOT BE ABLE TO KILL THE INSTALL. Inside a fresh chroot /proc may not
# be mounted, and the old form expanded to `$(( / 1024 / 1024 ))` — a shell arithmetic syntax
# error that took the whole run down with "expecting primary". Observed for real inside the
# Debian 13 userspace this script bootstraps for itself.
# A missing memory reading is not fatal information: fall back to a conservative 8 GB, which
# picks the 4B model — small enough to work anywhere, and the size can always be overridden
# with --model.
RAM_GB=""
if [ "$OS" = "Darwin" ]; then
    _kb="$(sysctl -n hw.memsize 2>/dev/null || true)"
    [ -n "$_kb" ] && RAM_GB=$(( _kb / 1024 / 1024 / 1024 ))
elif [ -r /proc/meminfo ]; then
    _kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || true)"
    [ -n "$_kb" ] && RAM_GB=$(( _kb / 1024 / 1024 ))
fi
if [ -z "$RAM_GB" ] || [ "$RAM_GB" -lt 1 ] 2>/dev/null; then
    RAM_GB=8
    warn "could not read this system's memory - assuming ${RAM_GB} GB (override with --model)"
fi
say "RAM: ${RAM_GB} GB, os: $OS, architecture: $ARCH"

# Backend. On macOS, Metal is built into the standard build — there is no choice to
# make. On Linux, Vulkan covers NVIDIA, AMD and Intel with one 34 MB download and no
# CUDA runtime; a vendor SDK would mean a bigger download that can fail, which is worse
# than a slightly slower one that cannot.
if [ "$OS" = "Darwin" ]; then
    ASSET="llama-$RELEASE-bin-macos-$ARCH.tar.gz"
    say "Backend: Metal (built in)"
elif [ -e /dev/dri ] || command -v vulkaninfo >/dev/null 2>&1 || command -v nvidia-smi >/dev/null 2>&1; then
    ASSET="llama-$RELEASE-bin-ubuntu-vulkan-$ARCH.tar.gz"
    say "Backend: Vulkan (works with NVIDIA, AMD and Intel alike)"
else
    ASSET="llama-$RELEASE-bin-ubuntu-$ARCH.tar.gz"
    say "Backend: CPU (this works, it is just slower)"
fi

# Peak memory is roughly the file plus KV cache at 4K, and the machine still has to be
# usable while it runs — so these leave real headroom rather than fitting on paper.
# ANDROID'S LINUX TERMINAL is a real Debian VM (Android 16 / AVF, Pixel and friends) and
# this installer genuinely works there on arm64 — but it is NOT a small laptop, and treating
# it like one gets the phone OOM-killed. Two differences that matter:
#   - the RAM the VM reports is carved out of the phone, and Android will kill the VM if it
#     leans on it, so the honest ceiling is well below what /proc/meminfo suggests;
#   - storage is a shared partition a user cannot easily grow.
# Detected by the AVF-specific virtiofs mount and vsock device rather than by a kernel
# string, which changes between releases.
IS_ANDROID_VM=0
IS_TERMUX=0
if [ -n "${TERMUX_VERSION:-}" ] || [ -d /data/data/com.termux ]; then
    # TERMUX, not a VM. This is the path for every Android phone that does NOT have the
    # Pixel-only Linux terminal — Samsung Galaxy included, since One UI does not ship the
    # AVF terminal. Termux runs native arm64 binaries directly (no VM, no root), which is
    # why the same llama.cpp build works. Two real differences from Debian:
    #   - packages come from `pkg`, not apt, and libgomp ships inside the compiler package
    #   - there is no systemd and no /etc, so autostart has to be Termux:Boot, not a unit
    IS_TERMUX=1
    say "Running under Termux — native $ARCH, no VM. Sizing for a phone."
elif [ -e /dev/vsock ] && { grep -qi 'virtiofs\|android' /proc/mounts 2>/dev/null || [ -d /mnt/shared ]; }; then
    IS_ANDROID_VM=1
    say "Running inside Android's Linux terminal — sizing for a phone, not a workstation."
fi

if [ "$MODEL" = "auto" ]; then
    if [ "$IS_ANDROID_VM" = "1" ] || [ "$IS_TERMUX" = "1" ]; then
        # Cap at 4B even on a 16 GB Pixel. 1.07 GB of weights plus KV is comfortable; 8B
        # would fit on paper and then get reaped the moment you switch apps.
        if [ "$RAM_GB" -ge 8 ]; then MODEL=4B; else MODEL=1.7B; fi
    elif [ "$RAM_GB" -ge 24 ]; then MODEL=27B
    elif [ "$RAM_GB" -ge 12 ]; then MODEL=8B
    elif [ "$RAM_GB" -ge 8  ]; then MODEL=4B
    else MODEL=1.7B
    fi
fi
say "Model: Ternary-Bonsai-$MODEL"

mkdir -p "$BIN" "$MODELS"

fetch() { # url dest label
    if [ -f "$2" ]; then say "$3 already here — skipping"; return 0; fi
    say "$3 ..."
    if command -v curl >/dev/null 2>&1; then
        curl -fL --progress-bar -C - -o "$2.part" "$1" || die "$3 download failed."
    elif command -v wget >/dev/null 2>&1; then
        wget -q --show-progress -c -O "$2.part" "$1" || die "$3 download failed."
    else
        # Minimal Debian images — including Android's Linux terminal — often ship neither.
        # Say exactly what to run rather than leaving someone to guess the package name.
        die "Neither curl nor wget is installed. Run:  sudo apt update && sudo apt install -y curl   then start this again."
    fi
    mv "$2.part" "$2"
}

# ── 1b. glibc floor, checked BEFORE anything is downloaded ───────────────────
# THE FLOOR IS PER-ARCHITECTURE. Measured with readelf -V on both builds, 2026-07-27:
#
#            llama-server   libllama-server-impl.so   floor
#   x64          2.34              2.34               2.34
#   arm64        2.34              2.38               2.38   <- binding constraint
#
# Two traps here, both hit while writing this:
#   1. Checking only the MAIN binary gives 2.34 on arm64 and says a 2.36 system is fine.
#      It is not — the bundled impl library needs 2.38. That is exactly how a Pixel on
#      Debian 12 was told it would work and then failed.
#   2. Applying arm64's 2.38 everywhere BLOCKS x64 machines that demonstrably work —
#      debian:12-slim (glibc 2.36) runs the x64 build fine; it was verified end to end
#      before this check existed. A floor that rejects a proven-working configuration is
#      a worse bug than the one it fixes.
#
#   Debian 12 bookworm = 2.36  -> fine on x64, TOO OLD on arm64
#   Debian 13 trixie   = 2.41  -> fine everywhere
#   Ubuntu 24.04       = 2.39  -> fine everywhere
#
# glibc cannot be upgraded in place (every binary links it, including apt and your shell), so
# there is no automated fix — only a newer base. Saying so in two seconds beats saying it
# after a multi-GB download.
if [ "$OS" != "Darwin" ] && command -v ldd >/dev/null 2>&1; then
    _glibc="$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' | head -1)"
    if [ -n "$_glibc" ]; then
        case "$ARCH" in
            arm64) _need=38 ;;
            *)     _need=34 ;;
        esac
        _maj="${_glibc%%.*}"; _min="${_glibc##*.}"
        if [ "$_maj" -lt 2 ] || { [ "$_maj" -eq 2 ] && [ "$_min" -lt "$_need" ]; }; then
            # ── TOO OLD? FIX IT. Do not hand the user a paste. ──────────────────
            # glibc genuinely cannot be upgraded in place — every binary on the system
            # links it, including apt and the shell you are typing in — so "just upgrade
            # glibc" is not an operation that exists. What IS possible, and fully
            # automatic, is giving the model a NEWER USERSPACE and re-entering this
            # installer inside it.
            #
            # Tried in order, cheapest first; each is a real fix rather than advice:
            #   1. Termux         -> proot-distro, its native mechanism
            #   2. docker/podman  -> a debian:13 container
            #   3. Debian/Ubuntu  -> debootstrap a trixie rootfs and chroot into it
            #
            # BONSAI_INSIDE stops a base image that is somehow still too old from
            # re-escaping forever.
            step "glibc $_glibc is below 2.$_need - building a newer userspace automatically"

            if [ "${BONSAI_INSIDE:-0}" = "1" ]; then
                die "Already inside a bootstrapped userspace and glibc is STILL $_glibc.
  The base image is wrong; stopping rather than looping."
            fi

            _SELF="https://aitherium.com/install-bonsai.sh"
            _ARGS="--model $MODEL --port $PORT"
            [ "$AUTOSTART" = "0" ] && _ARGS="$_ARGS --no-autostart"
            [ "$DEPS_ONLY" = "1" ] && _ARGS="$_ARGS --deps-only"
            # NEVER `curl ... | sh` HERE. A pipeline reports the LAST command's status, so if
            # curl fails, `sh` reads empty input and exits 0 — the inner install collapses and
            # the outer run declares success. That is not hypothetical: this exact chain
            # printed "curl: (6) Could not resolve host: aitherium.com" immediately followed by
            # "installed inside .../trixie - it still serves on 127.0.0.1:18080".
            # Same trap as D-1382, one layer down.
            # Download, prove the file is real, THEN run it, with every step chained on &&.
            _INNER="set -e
export BONSAI_INSIDE=1
apt-get update -qq
apt-get install -y -qq curl ca-certificates
curl -fsSL '$_SELF' -o /tmp/bonsai-inner.sh
test -s /tmp/bonsai-inner.sh
head -1 /tmp/bonsai-inner.sh | grep -q '^#!' || { echo 'downloaded installer is not a script' >&2; exit 1; }
sh /tmp/bonsai-inner.sh $_ARGS"

            # 1. Termux — proot-distro is the supported way to get a real distro there.
            if [ "$IS_TERMUX" = "1" ]; then
                say "using proot-distro"
                command -v proot-distro >/dev/null 2>&1 || pkg install -y proot-distro >/dev/null 2>&1
                if command -v proot-distro >/dev/null 2>&1; then
                    proot-distro install debian-trixie >/dev/null 2>&1 || proot-distro install debian >/dev/null 2>&1 || true
                    for _d in debian-trixie debian; do
                        if proot-distro login "$_d" -- sh -c "$_INNER"; then exit 0; fi
                    done
                fi
                die "proot-distro could not provide a newer Debian here."
            fi

            # 2. docker / podman — --network host so 127.0.0.1:$PORT still reaches it.
            for _rt in docker podman; do
                if command -v "$_rt" >/dev/null 2>&1 && "$_rt" info >/dev/null 2>&1; then
                    say "using $_rt with debian:13"
                    if "$_rt" run --rm --network host -v "$ROOT:$ROOT" -e HOME="$HOME" \
                         debian:13 sh -c "$_INNER"; then
                        exit 0
                    fi
                    warn "$_rt attempt did not complete; trying the next option"
                fi
            done

            # 3. debootstrap a trixie rootfs and chroot in. Heaviest, but it works on a
            #    plain Debian/Ubuntu with sudo — which is what Android's Linux terminal is.
            #    Nothing on the host is modified: the new userspace lives under $ROOT.
            if command -v apt-get >/dev/null 2>&1; then
                _RF="$ROOT/trixie"
                say "bootstrapping a Debian 13 userspace at $_RF (a few hundred MB, once)"
                _SUDO=""
                if [ "$(id -u)" != "0" ]; then
                    # Distinguish "no sudo" from "sudo works" HERE, so a later apt failure is
                    # never blamed on privileges. The first version of this printed
                    # "needs sudo without a password prompt" when the real cause was a
                    # transient apt-get update failure — a message that sends someone to fix
                    # the wrong thing.
                    if sudo -n true >/dev/null 2>&1; then
                        _SUDO="sudo -n"
                    else
                        die "This needs to install debootstrap, which requires root, and sudo
  here wants a password. Run:  sudo apt install -y debootstrap
  then start this again — everything after that is automatic."
                    fi
                fi
                if ! command -v debootstrap >/dev/null 2>&1; then
                    # Retry: apt-get update fails transiently on flaky networks, and one
                    # blip should not end the install.
                    _aptlog="$ROOT/apt.log"
                    _ok=0
                    for _try in 1 2 3; do
                        $_SUDO apt-get update -qq >"$_aptlog" 2>&1 || true
                        if $_SUDO apt-get install -y -qq debootstrap >>"$_aptlog" 2>&1; then _ok=1; break; fi
                        say "apt attempt $_try did not succeed; retrying"
                        sleep 3
                    done
                    [ "$_ok" = "1" ] || die "Could not install debootstrap after 3 attempts. apt said:

$(tail -5 "$_aptlog" 2>/dev/null)

  This is usually network. Check that deb.debian.org is reachable and try again."
                fi
                if [ ! -x "$_RF/bin/sh" ]; then
                    $_SUDO mkdir -p "$_RF"
                    $_SUDO debootstrap --variant=minbase trixie "$_RF" http://deb.debian.org/debian \
                        || die "debootstrap failed - check network access to deb.debian.org."
                fi
                # MOUNT /proc PROPERLY AND SAY SO IF IT FAILS. `mount --bind /proc` is refused
                # in some sandboxes, and `mountpoint` is often absent from a minbase rootfs so
                # the guard silently evaluated false and the failure was swallowed by
                # `|| true`. The result was a chroot with no /proc, where reading memory blew
                # up the inner run. Try a real proc mount first, fall back to bind, and warn
                # rather than pretend — the installer tolerates a missing /proc now, but the
                # user should know the environment is degraded.
                $_SUDO mkdir -p "$_RF/proc" "$_RF/sys" "$_RF/dev" "$_RF/dev/pts"
                if ! $_SUDO mount -t proc proc "$_RF/proc" 2>/dev/null; then
                    $_SUDO mount --bind /proc "$_RF/proc" 2>/dev/null || \
                        warn "could not mount /proc into the new userspace (sandbox restriction)"
                fi
                $_SUDO mount --bind /sys "$_RF/sys" 2>/dev/null || true
                $_SUDO mount --bind /dev "$_RF/dev" 2>/dev/null || true
                $_SUDO mount --bind /dev/pts "$_RF/dev/pts" 2>/dev/null || true
                $_SUDO cp /etc/resolv.conf "$_RF/etc/resolv.conf" 2>/dev/null || true
                $_SUDO mkdir -p "$_RF$HOME"
                say "entering the new userspace"
                if $_SUDO chroot "$_RF" /bin/sh -c "export HOME=$HOME; $_INNER"; then
                    say "installed inside $_RF - it still serves on 127.0.0.1:$PORT"
                    exit 0
                fi
                die "The bootstrapped userspace did not finish the install. Its root is at
  $_RF and nothing on your main system was changed."
            fi

            die "glibc $_glibc is below 2.$_need and no way to build a newer userspace was
  found (no Termux proot-distro, no docker/podman, no apt). A Debian 13 or Ubuntu
  24.04 base is required."
        fi
        say "glibc $_glibc (need 2.$_need for $ARCH) - ok"
    fi
fi

# ── 2. the server ────────────────────────────────────────────────────────────
step "Getting the inference server (public build, ~11-35 MB)"
if [ ! -x "$BIN/llama-server" ]; then
    fetch "$RELBASE/$ASSET" "$ROOT/$ASSET" "$ASSET"
    tar -xzf "$ROOT/$ASSET" -C "$ROOT"
    rm -f "$ROOT/$ASSET"
    # Archives vary in whether they nest a build/bin directory; find the binary rather
    # than assuming a layout that a future release could change.
    found="$(find "$ROOT" -name llama-server -type f -perm -u+x 2>/dev/null | head -1)"
    [ -n "$found" ] || die "llama-server was not in $ASSET. Nothing was started."
    if [ "$found" != "$BIN/llama-server" ]; then
        cp -a "$(dirname "$found")/." "$BIN/"
    fi
fi
chmod +x "$BIN/llama-server" 2>/dev/null || true
[ -x "$BIN/llama-server" ] || die "Could not install the server."

# ── AUTONOMOUS DEPENDENCY RESOLUTION ─────────────────────────────────────────
# Smoke-test the binary BEFORE downloading gigabytes, and resolve whatever it is missing
# rather than failing with one library's name.
#
# The first version of this handled exactly libgomp, because that is what a debian:12
# container happened to lack. A real phone then hit a DIFFERENT set and got "the inference
# server will not run on this system, and did not say why" — the script had the error and
# threw it away. So this now enumerates every missing library via ldd, maps them all, and
# loops: fixing one library often reveals the next.
#
# Verified facts about these builds (read from the ELF, not assumed):
#   interpreter = /lib/ld-linux-aarch64.so.1  -> GLIBC, so Termux/bionic cannot exec them
#   RUNPATH     = $ORIGIN                     -> bundled .so files resolve without help
_pkg_for_lib() {
    case "$1" in
        libgomp*)    echo "apt:libgomp1 dnf:libgomp apk:libgomp pacman:gcc-libs zypper:libgomp1" ;;
        libstdc++*)  echo "apt:libstdc++6 dnf:libstdc++ apk:libstdc++ pacman:gcc-libs zypper:libstdc++6" ;;
        libgcc_s*)   echo "apt:libgcc-s1 dnf:libgcc apk:libgcc pacman:gcc-libs zypper:libgcc_s1" ;;
        libcurl*)    echo "apt:libcurl4 dnf:libcurl apk:libcurl pacman:curl zypper:libcurl4" ;;
        libgssapi*|libkrb5*) echo "apt:libgssapi-krb5-2 dnf:krb5-libs apk:krb5-libs pacman:krb5 zypper:krb5" ;;
        libvulkan*)  echo "apt:libvulkan1 dnf:vulkan-loader apk:vulkan-loader pacman:vulkan-icd-loader zypper:libvulkan1" ;;
        *) echo "" ;;
    esac
}

_pkg_mgr() {
    for m in apt-get dnf yum apk pacman zypper; do
        command -v "$m" >/dev/null 2>&1 && { echo "$m"; return; }
    done
    echo ""
}

_install_pkgs() {   # $* = package names
    [ -n "$*" ] || return 1
    _mgr="$(_pkg_mgr)"
    [ -n "$_mgr" ] || return 1
    # Try unprivileged first (root, or an Android VM where you already are), then sudo -n.
    # Never prompt: an installer that stops for a password is the wall this avoids.
    case "$_mgr" in
        apt-get) _cmd="apt-get install -y -qq $*" ;;
        dnf|yum) _cmd="$_mgr install -y $*" ;;
        apk)     _cmd="apk add --no-cache $*" ;;
        pacman)  _cmd="pacman -S --noconfirm $*" ;;
        zypper)  _cmd="zypper --non-interactive install $*" ;;
    esac
    sh -c "$_cmd" >/dev/null 2>&1 || sudo -n sh -c "$_cmd" >/dev/null 2>&1
}

_missing_libs() {
    if command -v ldd >/dev/null 2>&1; then
        # MATCH ONLY '=> not found'. A bare /not found/ also matches ldd's OTHER failure
        # shape — a library that IS present but too OLD:
        #   /path/llama-server: /lib/.../libstdc++.so.6: version `GLIBCXX_3.4.32' not found
        # There $1 is the BINARY PATH, so the resolver dutifully reported
        # "missing libraries with no known package mapping: /home/.../llama-server"
        # and told the user to install their own binary. Reported from a Pixel.
        ldd "$BIN/llama-server" 2>/dev/null | awk '/=> not found/ {print $1}' | sort -u
    else
        # No ldd (Alpine without glibc tooling, some minimal images): fall back to the
        # loader's own message, which names one library at a time.
        "$BIN/llama-server" --version 2>&1 \
            | grep -o 'shared libraries: [^:]*' | awk '{print $NF}' | sort -u
    fi
}

_attempt=0
while [ "$_attempt" -lt 4 ]; do
    "$BIN/llama-server" --version >/dev/null 2>&1 && break
    _attempt=$((_attempt + 1))

    # A PRESENT-BUT-TOO-OLD library is a different problem from a missing one, and it needs
    # an upgrade rather than an install. Detect it before falling through to "no idea".
    _verr="$(ldd "$BIN/llama-server" 2>&1 | grep -o "version \`[A-Z_0-9.]*' not found" | head -1)"
    if [ -n "$_verr" ]; then
        _sym="$(printf '%s' "$_verr" | tr -d "\`'" | awk '{print $2}')"
        _oldlib="$(ldd "$BIN/llama-server" 2>&1 | grep "not found" | grep -o '/[^ ]*lib[a-z+]*\.so\.[0-9]*' | head -1)"
        step "A system library is too old ($_sym)"
        say "wanted: $_sym   from: ${_oldlib:-libstdc++}"

        # GLIBC IS A SPECIAL CASE AND MUST NOT BE UPGRADED IN PLACE.
        # Reproduced on debian:11-slim 2026-07-27: the binary wants GLIBC_2.34 and bullseye
        # ships 2.31. Forcing a newer libc6 from another suite is how you brick a machine —
        # every binary on the system links it, including apt and the shell you are typing in.
        # There is no safe automated fix; the honest answer is a newer base.
        case "${_oldlib:-}" in
            */libc.so.6|libc.so.6)
                die "This system's glibc is older than these builds need ($_sym).

  glibc cannot be upgraded in place — every program on the machine links it, so
  forcing a newer one from another release is a good way to break the system. This
  installer will not do that to you.

  What does work:
    - Android Linux terminal: use a current image (Debian 13 / trixie ships glibc 2.41).
    - Debian: bookworm (12) or newer. Ubuntu: 22.04 or newer.
    - Or run it in a container, which carries its own glibc:
        docker run --rm -p $PORT:$PORT debian:13 sh -c           'apt update && apt install -y curl && curl -fsSL https://aitherium.com/install-bonsai.sh | sh'

  Nothing large has been downloaded yet — you have not wasted a download on this."
                ;;
        esac
        # Try the cheap fix first: upgrade the package in place.
        _upg=""
        case "${_oldlib:-libstdc++}" in
            *stdc++*) _upg="libstdc++6" ;;
            *gcc_s*)  _upg="libgcc-s1" ;;
        esac
        _fixed=0
        if [ -n "$_upg" ] && command -v apt-get >/dev/null 2>&1; then
            if apt-get install -y -qq --only-upgrade "$_upg" >/dev/null 2>&1                || sudo -n apt-get install -y -qq --only-upgrade "$_upg" >/dev/null 2>&1; then
                "$BIN/llama-server" --version >/dev/null 2>&1 && _fixed=1
            fi
            # Still short: the distro itself is older than the build. On Debian that is
            # bookworm (GCC 12, GLIBCXX_3.4.30) against a binary built on a newer toolchain.
            # trixie's libstdc++6 satisfies it and installs cleanly alongside.
            if [ "$_fixed" = "0" ]; then
                say "distro's $_upg is older than this build needs — pulling a newer one"
                _sl="/etc/apt/sources.list.d/aitherium-newer-libstdc.list"
                if printf 'deb http://deb.debian.org/debian trixie main
' > "$_sl" 2>/dev/null                    || printf 'deb http://deb.debian.org/debian trixie main
' | sudo -n tee "$_sl" >/dev/null 2>&1; then
                    { apt-get update -qq >/dev/null 2>&1 || sudo -n apt-get update -qq >/dev/null 2>&1; }
                    { apt-get install -y -qq -t trixie "$_upg" >/dev/null 2>&1                       || sudo -n apt-get install -y -qq -t trixie "$_upg" >/dev/null 2>&1; }
                    # Leave the machine as we found it — an extra apt source is not ours to keep.
                    rm -f "$_sl" 2>/dev/null || sudo -n rm -f "$_sl" 2>/dev/null || true
                    { apt-get update -qq >/dev/null 2>&1 || sudo -n apt-get update -qq >/dev/null 2>&1; }
                    "$BIN/llama-server" --version >/dev/null 2>&1 && _fixed=1
                fi
            fi
        fi
        if [ "$_fixed" = "1" ]; then
            say "resolved — $_upg now provides $_sym"
            continue
        fi
        die "This build needs $_sym, which ${_oldlib:-your libstdc++} does not provide, and
  upgrading it here did not work.

  Your OS is older than the toolchain these binaries were built with. Options:
    - Android Linux terminal: use a current image (Debian 13 / trixie).
    - Debian/Ubuntu:  sudo apt install -y -t trixie ${_upg:-libstdc++6}
    - Or run it in a container, which carries its own libraries.

  Nothing large has been downloaded yet."
    fi

    _missing="$(_missing_libs)"
    if [ -z "$_missing" ]; then
        _why="$("$BIN/llama-server" --version 2>&1 | head -4)"
        if [ "$IS_TERMUX" = "1" ]; then
            # NOT FIXABLE BY INSTALLING PACKAGES. Termux is Android/bionic; these are glibc
            # binaries (interpreter /lib/ld-linux-aarch64.so.1, confirmed by reading the ELF).
            # A glibc binary cannot exec under bionic, so the honest answer is a real distro.
            die "These builds cannot run under Termux directly.

  It said:
$_why

  Why: the binaries are glibc (Ubuntu) and Termux is bionic (Android). That is a
  fundamental mismatch, not a missing package.

  Two ways forward, both one paste:
    1. Pixel / Android 16 Linux terminal — Settings > System > Developer options >
       Linux development environment. Run the same command in THAT terminal.
    2. A real distro inside Termux:
         pkg install -y proot-distro && proot-distro install debian && \
         proot-distro login debian -- sh -c 'apt update && apt install -y curl && curl -fsSL https://aitherium.com/install-bonsai.sh | sh'"
        fi
        die "The inference server will not start. It said:

$_why

  Detected: os=$OS arch=$ARCH
  Reproduce with:  $BIN/llama-server --version"
    fi

    step "Resolving missing system libraries (attempt $_attempt)"
    _mgr="$(_pkg_mgr)"
    _want=""
    _unmapped=""
    for _lib in $_missing; do
        say "needs $_lib"
        _map="$(_pkg_for_lib "$_lib")"
        _p=""
        for _entry in $_map; do
            case "$_entry" in "${_mgr%%-*}:"*|"$_mgr:"*) _p="${_entry#*:}" ;; esac
        done
        if [ -n "$_p" ]; then _want="$_want $_p"; else _unmapped="$_unmapped $_lib"; fi
    done

    if [ -n "$_unmapped" ]; then
        die "Missing libraries with no known package mapping:$_unmapped
  Install them with your package manager and run this again.
  (Nothing large has been downloaded yet.)"
    fi

    if ! _install_pkgs $_want; then
        die "Could not install:$_want
  Run:  sudo <your package manager> install$_want
  then start this again. (Nothing large has been downloaded yet.)"
    fi
    say "installed:$_want"
done

if ! "$BIN/llama-server" --version >/dev/null 2>&1; then
    die "Still cannot start the server after resolving dependencies. It said:

$("$BIN/llama-server" --version 2>&1 | head -4)"
fi

# --deps-only exists so an orchestrator (AitherZero's bootstrap-bonsai-device playbook) can
# run dependency resolution as its OWN stage and fail there, rather than discovering a broken
# toolchain three stages later. Same code path as the one-paste install, so the two cannot
# drift — which is the whole reason it is a flag here and not a second script.
if [ "$DEPS_ONLY" = "1" ]; then
    say "Dependencies resolved; server binary runs. Stopping here (--deps-only)."
    exit 0
fi
say "Server ready."

# ── 3. the model ─────────────────────────────────────────────────────────────
step "Getting the model (this is the big one — leave it running)"
GGUF="$MODELS/Ternary-Bonsai-$MODEL-Q2_0.gguf"
fetch "https://huggingface.co/prism-ml/Ternary-Bonsai-$MODEL-gguf/resolve/main/Ternary-Bonsai-$MODEL-Q2_0.gguf" \
      "$GGUF" "Ternary-Bonsai-$MODEL (Q2_0)"
say "Model ready."

# ── 4. start it ──────────────────────────────────────────────────────────────
step "Starting on http://127.0.0.1:$PORT"

# --reasoning-budget BOUNDS THE THINKING, and it is not optional. Bonsai ships the Qwen3
# chat template, which force-OPENS a <think> block every turn; with llama-server's default
# of -1 the block never closes, message.content comes back EMPTY, and the quantised model
# drifts off-language mid-thought. Reproduced independently in our own browser runtime.
SERVE_ARGS="--model $GGUF --host 127.0.0.1 --port $PORT --ctx-size 16384 --reasoning-budget 2048 --alias bonsai-selfhost"

stop_server
# shellcheck disable=SC2086
nohup "$BIN/llama-server" $SERVE_ARGS >"$ROOT/server.log" 2>&1 &

say "Waiting for it to load (a big model takes a minute) ..."
ok=0
i=0
while [ $i -lt 120 ]; do
    sleep 1
    if curl -fsS --max-time 2 "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then ok=1; break; fi
    i=$((i+1))
done
[ "$ok" = "1" ] || die "It did not come up in two minutes. The log is at $ROOT/server.log"
say "Serving."

# Prove it ANSWERS, not merely that a port is open. A process that listens and returns
# nothing useful is the exact failure that makes a health check worthless.
# max_tokens 16 was too small: Bonsai opens a <think> block on every turn, so a 16-token
# budget is spent reasoning and `content` comes back EMPTY — which printed "Health is green
# but the test question returned nothing" on an install that was working perfectly. Measured
# in a clean debian:12 container 2026-07-27. 256 leaves room to think AND answer.
_resp="$(curl -fsS --max-time 180 "http://127.0.0.1:$PORT/v1/chat/completions"     -H 'Content-Type: application/json'     -d '{"model":"bonsai-selfhost","messages":[{"role":"user","content":"Reply with the single word: ready"}],"max_tokens":256}'     2>/dev/null || true)"
# Take the LAST content field: the response carries reasoning_content before it.
reply="$(printf '%s' "$_resp" | tr ',' '
' | grep '"content"' | tail -1 | cut -d'"' -f4)"
if [ -n "$reply" ]; then
    say "It answered: $reply"
elif printf '%s' "$_resp" | grep -q 'reasoning_content'; then
    # Thinking but not answering is a REAL condition worth naming, not a generic failure.
    warn "It thought about the question but ran out of tokens before answering."
    warn "That is a budget issue, not a broken install — it is serving."
elif [ -n "$_resp" ]; then
    warn "The server replied but not in the shape expected. It is running; check:"
    warn "  curl http://127.0.0.1:$PORT/v1/models"
else
    warn "Health is green but the test question got no response at all."
fi

# ── 5. survive a reboot ──────────────────────────────────────────────────────
if [ "$AUTOSTART" = "1" ]; then
    step "Making it start when you log in"
    if [ "$IS_TERMUX" = "1" ]; then
        # No systemd under Termux. Termux:Boot runs anything in ~/.termux/boot at device
        # start, IF that companion app is installed — so write the hook and say plainly that
        # it needs the app, rather than claiming autostart works when it may not.
        mkdir -p "$HOME/.termux/boot"
        cat > "$HOME/.termux/boot/aitherium-bonsai.sh" <<TERMUXEOF
#!/data/data/com.termux/files/usr/bin/sh
termux-wake-lock
$BIN/llama-server $SERVE_ARGS >$ROOT/server.log 2>&1 &
TERMUXEOF
        chmod +x "$HOME/.termux/boot/aitherium-bonsai.sh"
        say "Boot hook written to ~/.termux/boot/aitherium-bonsai.sh"
        warn "It only runs if the Termux:Boot app is installed (F-Droid). Without it,"
        warn "start Bonsai by re-running this script after a reboot."
    elif [ "$OS" = "Darwin" ]; then
        mkdir -p "$HOME/Library/LaunchAgents"
        PLIST="$HOME/Library/LaunchAgents/com.aitherium.bonsai.plist"
        {
            printf '<?xml version="1.0" encoding="UTF-8"?>\n'
            printf '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
            printf '<plist version="1.0"><dict>\n'
            printf '  <key>Label</key><string>com.aitherium.bonsai</string>\n'
            printf '  <key>RunAtLoad</key><true/>\n  <key>KeepAlive</key><true/>\n'
            printf '  <key>ProgramArguments</key><array>\n'
            printf '    <string>%s/llama-server</string>\n' "$BIN"
            for a in $SERVE_ARGS; do printf '    <string>%s</string>\n' "$a"; done
            printf '  </array>\n'
            printf '  <key>StandardOutPath</key><string>%s/server.log</string>\n' "$ROOT"
            printf '</dict></plist>\n'
        } > "$PLIST"
        launchctl unload "$PLIST" 2>/dev/null || true
        launchctl load "$PLIST" 2>/dev/null && say "Registered with launchd." \
            || warn "Could not register autostart; it runs now but will not survive a reboot."
    else
        mkdir -p "$HOME/.config/systemd/user"
        cat > "$HOME/.config/systemd/user/aitherium-bonsai.service" <<EOF
[Unit]
Description=Aitherium Bonsai (local inference)
After=network.target

[Service]
ExecStart=$BIN/llama-server $SERVE_ARGS
Restart=on-failure

[Install]
WantedBy=default.target
EOF
        if command -v systemctl >/dev/null 2>&1 && systemctl --user daemon-reload 2>/dev/null; then
            systemctl --user enable aitherium-bonsai.service >/dev/null 2>&1 && say "Registered with systemd."
            # Without lingering, a user service stops when the last session closes. Try to
            # enable it, but never require sudo — an installer that demands a password to
            # finish is exactly the wall this script exists to avoid.
            loginctl enable-linger "$USER" >/dev/null 2>&1 || true
        else
            warn "No user systemd here; it runs now but will not survive a reboot."
        fi
    fi
fi

# ── 6. the agent (optional: --with-adk) ──────────────────────────────────────
if [ "$WITH_ADK" = "1" ]; then
    step "Installing aither-adk and enrolling this device"

    # DEBIAN 12+ IS PEP 668 "EXTERNALLY MANAGED" and a plain `pip install` fails with
    # error: externally-managed-environment. That includes Android's Linux terminal image.
    # A venv is the correct answer — NOT --break-system-packages, which does exactly what
    # its name says on a machine the person also uses for other things.
    if ! command -v python3 >/dev/null 2>&1; then
        die "python3 is missing. Run:  sudo apt update && sudo apt install -y python3 python3-venv   then re-run with --with-adk."
    fi
    VENV="$ROOT/adk-venv"
    if [ ! -x "$VENV/bin/adk" ]; then
        python3 -m venv "$VENV" 2>/dev/null || die "Could not create a venv. Run:  sudo apt install -y python3-venv   then try again."
        "$VENV/bin/pip" install --quiet --upgrade pip
        "$VENV/bin/pip" install --quiet aither-adk || die "pip install aither-adk failed. Its 8 dependencies are pure-Python or ship arm64 wheels, so this is usually network — try again."
    fi
    ADK="$VENV/bin/adk"
    say "adk installed at $ADK"

    # PUT IT ON PATH. Installing into a venv and never linking it means the command exists
    # and cannot be typed — reported from a real phone: "if it installed aither-adk why can't
    # I run it". An installer that leaves its own tool uninvokable has not finished.
    # ~/.local/bin is on PATH by default on Debian/Ubuntu (and therefore in Android's Linux
    # terminal); if it is not, we say so rather than assuming.
    mkdir -p "$HOME/.local/bin" 2>/dev/null || true
    if ln -sf "$ADK" "$HOME/.local/bin/adk" 2>/dev/null; then
        case ":$PATH:" in
            *":$HOME/.local/bin:"*) say "linked: you can now just type  adk" ;;
            *) warn "linked to ~/.local/bin/adk, which is NOT on your PATH. Add it:"
               warn "  echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc && . ~/.bashrc" ;;
        esac
    fi

    # `aither` IS A DIFFERENT PACKAGE. Same project, two channels: pip gives the `adk`
    # command, npm gives `aither` (aither-adk/packaging/npm/package.json declares
    # bin.aither). Someone who installed via pip and then tried to run `aither` gets
    # "command not found" with no clue why — so install it when npm is present, and say
    # plainly what is missing when it is not.
    if command -v npm >/dev/null 2>&1; then
        step "Installing the aither CLI (AitherShell)"
        if npm install -g aither-adk >/dev/null 2>&1; then
            say "you can now run  aither"
        else
            warn "npm install -g aither-adk failed (often needs sudo, or a writable npm prefix)."
            warn "  adk still works; 'aither' will not until this succeeds."
        fi
    else
        say "note: the 'aither' command (AitherShell) ships via npm, not pip."
        say "      install node, then:  npm install -g aither-adk"
    fi

    # ENROL IS OUTBOUND HTTPS ONLY — no WireGuard, no root, no inbound port. That matters
    # here: `adk mesh onboard` needs wg-quick and /etc/wireguard, and an Android VM has
    # neither a wireguard kernel module nor reliably /dev/net/tun. Enrolment registers the
    # device with the portal and heartbeats outward, which is the part that actually makes
    # it show up as yours.
    # IDENTITY FIRST, AND IT IS NOT OPTIONAL. Enrolment reads ~/.aither/auth.json for the
    # tenant; with no identity it used to fall back to the tenant "personal" and register the
    # device there — succeeding loudly while putting your phone in someone else's workspace.
    # adk now refuses in that state (exit 1, verified), so supply the key here.
    # DEVICE FLOW BY DEFAULT — no key to copy onto a phone.
    # adk implements RFC 8628 (_device_flow_login, cli.py:1821, /auth/device/code +
    # /auth/device/token): it PRINTS a short code, you approve it in any browser, and it polls
    # until you do. It needs no stdin, which matters because `curl ... | sh` has already taken
    # stdin for the script itself — so this works even in the one-paste path.
    #
    # Requiring --api-key here was the wrong call: copying a key onto a phone is exactly the
    # kind of manual step this installer exists to remove, and adk already had the better
    # mechanism. --api-key stays for genuinely headless installs (CI, cloud images) where
    # nobody is present to approve anything.
    if [ ! -f "$HOME/.aither/auth.json" ]; then
        if [ -n "$API_KEY" ]; then
            step "Signing in with the supplied key"
            if "$ADK" login --api-key "$API_KEY" --portal-url "$PORTAL" >/dev/null 2>&1; then
                say "Signed in."
            else
                warn "Sign-in with that key failed — enrolment will be skipped rather than"
                warn "registering this device under the wrong tenant."
            fi
        else
            step "Signing in — approve this device in your browser"
            say "A code will appear below. Open the link, enter it, and this continues by itself."
            printf '
'
            # Output is NOT suppressed: the code is the entire point, and a device flow whose
            # code you cannot see is just a hang.
            if "$ADK" login --portal-url "$PORTAL"; then
                say "Signed in."
            else
                warn "Device sign-in did not complete (declined, or timed out)."
                warn "Bonsai is installed and running regardless; re-run this to try again,"
                warn "or use --api-key <key> on a machine where nobody is present to approve."
            fi
            printf '
'
        fi
    else
        say "Already signed in — using the existing identity."
    fi

    # DO NOT ENROL WITHOUT AN IDENTITY, and do not rely on adk to refuse.
    # The fail-closed guard added to cmd_enroll lives in our repo, NOT in the aither-adk
    # currently on PyPI (2.43.0) that this venv just installed — proven in a clean container:
    # that version tried to enrol with no auth and returned "Enrollment failed: unknown
    # error". An older or newer adk could equally well SUCCEED and bind the device to the
    # fallback tenant "personal" instead of your workspace. Deciding here makes the outcome
    # independent of which adk version got installed.
    if [ -z "$API_KEY" ] && [ ! -f "$HOME/.aither/auth.json" ]; then
        warn "Skipping enrolment: no identity on this machine, and enrolling without one"
        warn "can register the device under the fallback tenant 'personal' rather than your"
        warn "workspace — which looks like it worked. Bonsai is running and fully usable."
        warn ""
        warn "Sign in and enrol whenever you like — no key needed:"
        warn "  $ADK login && $ADK enroll --portal $PORTAL"
        warn "That prints a code you approve in any browser. For an unattended machine:"
        warn "  sh install-bonsai.sh --api-key <key>   ($PORTAL -> Settings -> API keys)"
        ENROL=0
    else
        ENROL=1
    fi

    # POINT adk AT THE MODEL THAT IS ALREADY RUNNING.
    # Without this, `adk up` calls quickstart-local, which installs ANOTHER llama.cpp and
    # downloads ANOTHER model onto port 8209 — gigabytes of duplicate download on a phone
    # that already has Bonsai serving on $PORT. Read from cli.py: cmd_up -> "[1/3] Setting up
    # local inference backend" -> QuickstartArgs(port=8209).
    #
    # llama-server is OpenAI-compatible, which is the shape the `vllm` provider already
    # speaks, so pointing that at the running server needs no new backend type.
    step "Wiring the agent to the Bonsai already running here"
    if "$ADK" backend set vllm --base-url "http://127.0.0.1:$PORT/v1" --model bonsai-selfhost >/dev/null 2>&1; then
        say "adk will use the local Bonsai on :$PORT — no second model download."
    else
        warn "Could not set the backend automatically. Run this to avoid a duplicate download:"
        warn "  $ADK backend set vllm --base-url http://127.0.0.1:$PORT/v1 --model bonsai-selfhost"
    fi

    if [ "$ENROL" = "1" ]; then
    step "Enrolling this device with $PORTAL"
    if "$ADK" enroll --portal "$PORTAL"; then
        say "Enrolled. This device is now an endpoint in your workspace."
        say "Start the agent so it joins your fleet:"
        say "  $ADK up --port $PORT --yes"
    else
        # The refusal above is a FEATURE, so explain it rather than treating it as an error.
        warn "Not enrolled — this device has no identity yet, so it was not registered"
        warn "anywhere. Bonsai is running locally and fully usable regardless."
        warn ""
        warn "To attach it to YOUR workspace, get an API key from"
        warn "  $PORTAL  (Settings -> API keys)"
        warn "then re-run:"
        warn "  sh install-bonsai.sh --api-key <key>"
        warn "or, if you have a browser on this machine:  $ADK login && $ADK enroll"
    fi
    fi
fi

# THE ONE REMAINING MANUAL STEP, and it goes AFTER the install where it will be read.
# CONFIRMED ON A REAL PIXEL 2026-07-27: the server runs inside the Linux VM, Chrome runs
# outside it in Android, and they do not share a loopback interface — so 127.0.0.1:$PORT in
# the browser hits Android's own loopback and finds nothing until the Terminal app's port
# toggle is switched on. The owner hit exactly this: everything installed and ran, and the
# page saw nothing until they allowed the llama-server port.
#
# It is printed LAST, loud, and after the success banner, because that is where someone
# actually looks when they are done — burying it in a pre-flight note is how it gets missed.
# Confirmed working once enabled, so this is a real instruction now, not a guess.
if [ "$IS_ANDROID_VM" = "1" ] || [ "$IS_TERMUX" = "1" ]; then
    printf '\n\033[33m  ┌────────────────────────────────────────────────────────────┐\033[0m\n'
    printf '\033[33m  │  ONE STEP LEFT — the browser cannot see this yet            │\033[0m\n'
    printf '\033[33m  └────────────────────────────────────────────────────────────┘\033[0m\n'
    say "The model is running, but it lives inside this Linux environment and Chrome"
    say "lives outside it. They do not share a loopback interface, so the page finds"
    say "nothing until you bridge them:"
    printf '\n'
    say "  Open the Terminal app -> settings -> port forwarding"
    say "  Allow port $PORT (it will be listed as llama-server)"
    printf '\n'
    say "Then load https://aitherium.com and the dock will show 'running on your node'."
    printf '\n'
fi

printf '\n\033[32m  Done.\033[0m\n\n'
say "Open https://aitherium.com — it looks for a model on 127.0.0.1:$PORT and will just find this one."
say "Local API (OpenAI-compatible): http://127.0.0.1:$PORT/v1"
say "Stop it:    pkill -f llama-server"
# $0 is "sh" when the script arrives through a pipe (curl ... | sh), so printing it gives
# the useless "Remove it: sh --uninstall". Detect that and print something runnable.
if [ -f "$0" ] && [ "$0" != "sh" ]; then
    say "Remove it:  sh $0 --uninstall"
else
    say "Remove it:  curl -fsSL https://aitherium.com/install-bonsai.sh | sh -s -- --uninstall"
fi

# THE NEXT STEP HAS TO BE VISIBLE OR IT DOES NOT EXIST.
# A plain install left someone with a running model and no idea how to make it an AGENT
# attached to their account — the flag was documented only in --help, which nobody reads
# after a successful install. Asked directly: "there isn't a clear next step to actually
# turn it into a full aither-adk agent using bonsai as the brain".
if [ "$WITH_ADK" != "1" ]; then
    printf '\n'
    say "NEXT: turn this into an agent that belongs to your account"
    say ""
    say "     curl -fsSL 'https://aitherium.com/install-bonsai.sh?v=12' | sh -s -- --with-adk"
    say ""
    say "  It shows a code, you approve it in any browser, and the device registers itself."
    say "  No API key to copy. Nothing is re-downloaded — the agent reuses this model."
fi

printf '\n'
say "Want to share this machine's model with the mesh instead of just yourself?"
say "  adk mesh onboard && adk mesh provide --inference-url http://127.0.0.1:$PORT/v1 --model bonsai-selfhost"
say "  Opt-in, and an operator still has to grant trust — nothing routes to you until then."
printf '\n'
