#!/bin/sh # Aktapus CLI installer. # Usage: curl -fsSL https://aktapus.ai/install.sh | bash # # Frictionless install path: # 1. Download the right aktapus binary for OS+arch. # 2. Install local-LLM prereqs (llama.cpp + llamafit) so `aktapus llm # auto` works out-of-the-box. Each prereq is best-effort — failure # surfaces a clear "install llama.cpp manually" message rather # than breaking install. Aktapus is Llamafit-only post the # 2026-05-17 pivot, so without llama.cpp there's no LLM at all. # 3. Print one next-step line — `aktapus setup` — that does # everything else (start gateway + register admin + auto-pick a # model + route cheap-tier agents to local). # # Net frictionless flow from "I heard about this" to "first cited # answer": # # curl -fsSL https://aktapus.ai/install.sh | bash # installs everything # aktapus setup # one prompt-driven onboarding # aktapus wiki append --dir ~/notes # add content # aktapus ask "..." # cited answer # # Three commands. No API keys. No second package manager invocation. set -eu OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) [ "$ARCH" = "aarch64" ] && ARCH=arm64 [ "$ARCH" = "x86_64" ] && ARCH=amd64 # Supported combos: darwin/{arm64,amd64} + linux/{amd64,arm64}. The # Mac binaries are cross-built from Greg's M-series via deploy.sh # (native arm64 + Xcode-arch'd amd64); the Linux amd64 binary is # built natively on the prod VPS; the Linux arm64 binary is cross- # built via zig on Greg's Mac. All four are published under /dl/. case "${OS}-${ARCH}" in darwin-arm64|darwin-amd64|linux-amd64|linux-arm64) ;; *) echo "Unsupported platform: ${OS}-${ARCH}." >&2 echo "Currently supported: darwin/arm64, darwin/amd64, linux/amd64, linux/arm64." >&2 echo "Build from source for other platforms: git clone the repo and run 'make build'." >&2 exit 1 ;; esac URL="https://aktapus.ai/dl/aktapus-${OS}-${ARCH}" # Two install modes: # 1. system-wide (default): /usr/local/bin/aktapus, requires sudo. # Opt-in only via --system. Best for shared machines where every # user should run the same binary; requires interactive sudo or # pre-authenticated sudo (NOPASSWD) — piped curl|bash with # --system will fail with "sudo: a terminal is required to read # the password." # 2. user-local: ~/.local/bin/aktapus, NO sudo. **DEFAULT** as of # 2026-05-29. Works on every host without privilege escalation; # survives MDM/EDR alerts on corporate Macs; works in piped # curl|bash with no TTY. The 2026-05-29 dogfood test surfaced # that the previous "default to system, sudo prompt" path broke # the canonical one-liner pitch on fresh machines. # # Order of precedence (first match wins): # AKTAPUS_PREFIX env — explicit override (e.g. /opt/aktapus/bin) # --system flag — force /usr/local/bin (requires sudo) # --user / -u flag — explicit ~/.local/bin (redundant with default; # kept for backward compat and as a self- # documenting form in scripts) # existing aktapus on PATH — upgrade-in-place at the SAME location # (so a previous system install doesn't get # silently re-homed to ~/.local/bin on # upgrade). Only honored when that dir is # user-writable; root-owned paths fall # through to the default. # default — ~/.local/bin (no sudo, always succeeds) USER_BIN="$HOME/.local/bin" EXISTING=$(command -v aktapus 2>/dev/null || true) if [ -n "${AKTAPUS_PREFIX:-}" ]; then INSTALL_MODE="prefix" elif [ "${1:-}" = "--system" ]; then INSTALL_MODE="system" elif [ "${1:-}" = "--user" ] || [ "${1:-}" = "-u" ]; then INSTALL_MODE="user" elif [ -n "$EXISTING" ] && [ -w "$(dirname "$EXISTING")" ]; then INSTALL_MODE="existing" EXISTING_DIR=$(dirname "$EXISTING") echo "==> Existing aktapus found at $EXISTING — upgrading in place" else INSTALL_MODE="user" fi case "$INSTALL_MODE" in user) DEST_DIR="$USER_BIN" ;; prefix) DEST_DIR="$AKTAPUS_PREFIX" ;; existing) DEST_DIR="$EXISTING_DIR" ;; system|*) DEST_DIR="/usr/local/bin" ;; esac DEST="$DEST_DIR/aktapus" # Upgrade-in-place: if a gateway is running on :18789, stop it before # we swap any binary so the embedded Llamafit child gets cleaned up # via the gateway's shutdown handler. Independent of whether $DEST # already has a binary — the user may have deleted the on-disk binary # while the old process kept running, or be reinstalling from a # different mode (--user vs sudo) so the old binary lives at a # different path. Gating this on `[ -x "$DEST" ]` (the pre-fix shape) # missed both cases. [ -x "$DEST" ] && echo "==> Existing install detected at $DEST" WAS_RUNNING=0 if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then echo "==> Gateway running on :18789 — sending SIGTERM for graceful shutdown..." # SIGTERM the gateway so its shutdown handler runs (kills the # embedded llama-server too). 5s window before SIGKILL fallback. lsof -ti:18789 -sTCP:LISTEN | xargs kill -TERM 2>/dev/null || true sleep 3 lsof -ti:18789 -sTCP:LISTEN | xargs kill -9 2>/dev/null || true # Belt-and-suspenders: orphan llama-server cleanup. lsof -ti:8081 -sTCP:LISTEN | xargs kill -TERM 2>/dev/null || true sleep 1 lsof -ti:8081 -sTCP:LISTEN | xargs kill -9 2>/dev/null || true echo " ✓ stopped gateway" WAS_RUNNING=1 fi echo "==> Downloading $URL" # macOS binary distribution has two failure modes we have to defeat # at install time. Both bit Greg's Datadog M4 64GB Mac 2026-05-28: # post-upgrade aktapus was SIGKILLed before it could print --version, # and recovery required a manual `codesign --force --sign -` of the # new binary. # # 1. Per-inode signature cache. `curl -o $DEST` (when $DEST already # exists) truncates in place and preserves the inode. macOS caches # the code-signing identity per-inode; when the bytes change but # the inode doesn't, the kernel keeps the OLD signature and the # new bytes get rejected. Fix: download to a temp file then mv-f # over $DEST so the destination path points at a NEW inode (mv on # same-volume APFS is atomic + creates a fresh inode). # # 2. Unsigned-binary refusal on hardened macOS. On recent macOS # versions (Sequoia and forward), Gatekeeper / hardened runtime # can refuse to load completely unsigned binaries that link # certain frameworks, regardless of inode freshness. The fix is # ad-hoc signing: `codesign --force --sign -` assigns an empty # signing identity that the kernel accepts as "no Developer ID, # but at least there's a sealed signature." No paid Apple Dev # account required. # # Both happen in the temp-file stage so $DEST is never the broken # binary even momentarily. xattr -c clears any auto-added quarantine # bit curl may stamp on. Stderr-suppress + `|| true` on the macOS-only # commands so Linux installs aren't affected. TMP_DEST=$(mktemp /tmp/aktapus.XXXXXX) case "$INSTALL_MODE" in user|prefix|existing) # No sudo — write to a user-writable directory. Create the # parent dir with normal perms; download + chmod with the # current user's identity. Adds $DEST_DIR to PATH advice at # the end if it isn't already on PATH. mkdir -p "$DEST_DIR" curl -fL --progress-bar "$URL" -o "$TMP_DEST" chmod +x "$TMP_DEST" if [ "$OS" = "darwin" ]; then xattr -c "$TMP_DEST" 2>/dev/null || true codesign --force --sign - "$TMP_DEST" 2>/dev/null || true fi mv -f "$TMP_DEST" "$DEST" ;; system|*) # Download as the regular user (curl doesn't need root), sudo # only the mv into the root-owned dir. Avoids leaving a temp # file owned by root in /tmp if the install fails mid-way. curl -fL --progress-bar "$URL" -o "$TMP_DEST" chmod +x "$TMP_DEST" if [ "$OS" = "darwin" ]; then xattr -c "$TMP_DEST" 2>/dev/null || true codesign --force --sign - "$TMP_DEST" 2>/dev/null || true fi sudo mv -f "$TMP_DEST" "$DEST" ;; esac if [ "${WAS_RUNNING:-0}" = "1" ]; then echo "✓ Upgraded aktapus → $DEST" else echo "✓ Installed aktapus → $DEST" fi # PATH hint for user-mode installs. Skip if $DEST_DIR is already on # PATH (most users with ~/.local/bin in PATH already, e.g. anyone # using pipx, pyenv, asdf, or modern Linux distros that ship with # ~/.local/bin pre-included). case "$INSTALL_MODE" in user|prefix|existing) case ":$PATH:" in *":$DEST_DIR:"*) ;; *) echo echo "⚠ $DEST_DIR is not on your PATH. Add this to your shell rc:" echo " export PATH=\"$DEST_DIR:\$PATH\"" ;; esac ;; esac # ──────────────────────────────────────────────────────────────────── # Local-LLM prereqs. Best-effort: failure surfaces a clear "install # llama.cpp manually" message and the script still exits 0 so the # user can decide. Aktapus is Llamafit-only — local, no cloud, no # API key — so without llama.cpp there's no LLM at all. echo echo "==> Installing local-LLM prereqs (best-effort)..." # llama.cpp ships the actual `llama-server` binary that Llamafit # auto-tunes and supervises. Required for any LLM operation. if ! command -v llama-server >/dev/null 2>&1; then case "$OS" in darwin) if command -v brew >/dev/null 2>&1; then echo " → brew install llama.cpp" brew install llama.cpp >/dev/null 2>&1 \ && echo " ✓ llama.cpp installed" \ || echo " ✗ brew install failed — install llama.cpp manually before running 'aktapus setup'" else echo " ✗ Homebrew not found — skipping llama.cpp install." echo " Install brew (https://brew.sh) and re-run 'brew install llama.cpp' before 'aktapus setup'." fi ;; linux) # Most distros don't have llama.cpp packaged yet. Point # the user at the upstream repo rather than guessing # apt/dnf/pacman incantations that may not work. echo " (llama.cpp not in most Linux package repos — see" echo " https://github.com/ggml-org/llama.cpp for build instructions." echo " Aktapus needs llama-server on PATH before 'aktapus setup'.)" ;; esac else echo " ✓ llama-server already on PATH" fi # Llamafit is imported as a Go library inside aktapus and started # in-process on demand (see pkg/llm/local). The install path no longer # `go install`s a standalone binary — `aktapus llm auto` triggers the # gateway's embedded runtime via /api/llm/warmup, so the standard # install/auto flow has zero external Llamafit dependency. # # Pre-fix, this section ran `go install … >/dev/null 2>&1`, swallowed # every error, and the silent failure surfaced minutes later when # `aktapus llm auto` aborted after a 20GB model pull. If you still # want the standalone binary for `aktapus llm serve` debugging or for # operating a remote Llamafit on a GPU box, install it explicitly: # # go install github.com/guregodevo/llamafit/cmd/llamafit@latest # # stderr stays visible; you'll see exactly why it failed if it does. echo echo "✓ Aktapus is installed." echo # Post-install: pull any companion models (speculative-decoding draft, # embedding) the new binary expects against the existing main model. # Runs only when a prior local-LLM setup is detected (at least one GGUF # under ~/.aktapus/llm/models/) — fresh installs get nothing pulled # here; the user runs `aktapus llm auto` themselves after setup. # # Why this lives in install.sh rather than only in `aktapus upgrade`: # the canonical install path is `curl -fsSL aktapus.ai/install.sh | bash` # (or its equivalent piped from `aktapus upgrade`). Putting the post- # swap pull hook only in `aktapus upgrade` left the `curl|bash` path # silently stale — the new binary contained new auto-pull rules, but # the install path never re-triggered them. That gap left a Datadog # M4 64GB Mac running qwen2.5-32b for a day without its qwen2.5-7b # speculative-decoding draft (~1.7x decode penalty no one opted into). # Owning the hook here closes the gap for every install path at once. # # `llm topup` is scoped narrower than `llm auto`: # - respects the existing main model (won't try to pull a different # one if `pickModelForHardware` would have picked differently) # - never calls /api/llm/warmup or /api/workspace/settings, so it # works fine in the gateway-stopped state install.sh leaves behind # - exits 0 even when individual pulls fail (best-effort) — never # blocks the install if [ -d "$HOME/.aktapus/llm/models" ] && ls "$HOME/.aktapus/llm/models"/*.gguf >/dev/null 2>&1; then echo "==> Refreshing local-LLM companions for the existing main model..." "$DEST" llm topup || true echo fi # Two next-step messages depending on whether this was a fresh # install or an upgrade-in-place. Always print the localhost URL — # most terminals auto-link it, so the user can click straight into # the web UI after the gateway starts. if [ -d "$HOME/.aktapus" ]; then echo "Existing data dir detected at ~/.aktapus." echo echo "Next:" if [ "${WAS_RUNNING:-0}" = "1" ]; then echo " aktapus gateway & # restart the gateway (it was stopped above)" else echo " aktapus gateway & # start the gateway" fi echo " → then open http://localhost:18789" else echo "Next:" echo " aktapus setup # one-step onboarding (workspace + admin + first wiki + LLM)" echo " → then open http://localhost:18789" fi