#!/bin/sh # Memdoor CLI installer. # Usage: curl -fsSL https://memdoor.ai/install.sh | bash # # Frictionless install path: # 1. Download the right memdoor binary for OS+arch. # 2. Install local-LLM prereqs (llama.cpp + llamafit) so `memdoor 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. Memdoor 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 — `memdoor 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://memdoor.ai/install.sh | bash # installs everything # memdoor setup # one prompt-driven onboarding # memdoor wiki append --dir ~/notes # add content # memdoor 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 an M-series Mac 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 the maintainer'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://memdoor.ai/dl/memdoor-${OS}-${ARCH}" # Two install modes: # 1. system-wide (default): /usr/local/bin/memdoor, 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/memdoor, 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): # MEMDOOR_PREFIX env — explicit override (e.g. /opt/memdoor/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 memdoor 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 memdoor 2>/dev/null || true) # Record whether a real data dir existed BEFORE this run. The llama.cpp # install below creates ~/.memdoor/llm/llamacpp, which would otherwise # fool the fresh-vs-upgrade next-step message into "existing install" # and hide the `memdoor setup` onboarding line from first-time users. HAD_DATA_DIR=0 [ -d "$HOME/.memdoor" ] && HAD_DATA_DIR=1 if [ -n "${MEMDOOR_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 memdoor found at $EXISTING — upgrading in place" else INSTALL_MODE="user" fi case "$INSTALL_MODE" in user) DEST_DIR="$USER_BIN" ;; prefix) DEST_DIR="$MEMDOOR_PREFIX" ;; existing) DEST_DIR="$EXISTING_DIR" ;; system|*) DEST_DIR="/usr/local/bin" ;; esac DEST="$DEST_DIR/memdoor" # 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 a 64 GB M-series Mac 2026-05-28: # post-upgrade memdoor 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/memdoor.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 memdoor → $DEST" else echo "✓ Installed memdoor → $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. Memdoor 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)..." # install_prebuilt_llamacpp — fetch the prebuilt llama-server for a # llama.cpp release asset (macos-arm64, ubuntu-x64, …), keep its libs beside # it (RUNPATH=$ORIGIN / @loader_path), symlink the binary onto PATH, and # self-check. On macOS it clears quarantine + ad-hoc codesigns. Sets # LCPP_OK=1 on success; removes a binary that can't print --version (never # leave a broken llama-server on PATH). Best-effort. install_prebuilt_llamacpp() { asset="$1"; LCPP_OK=0 [ -z "$asset" ] && return 1 echo " → fetching prebuilt llama.cpp (${asset})..." url=$(curl -fsSL "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest" 2>/dev/null \ | grep -oE "https://[^\"]*llama-[^\"]*-bin-${asset}\.tar\.gz" | head -1) [ -z "$url" ] && return 1 dir="$HOME/.memdoor/llm/llamacpp" tmp=$(mktemp -d /tmp/llamacpp.XXXXXX) if curl -fL --progress-bar "$url" -o "$tmp/llama.tar.gz" 2>/dev/null \ && tar -xzf "$tmp/llama.tar.gz" -C "$tmp" 2>/dev/null; then lsbin=$(find "$tmp" -name llama-server -type f 2>/dev/null | head -1) if [ -n "$lsbin" ]; then mkdir -p "$dir" cp "$(dirname "$lsbin")"/* "$dir"/ 2>/dev/null || true chmod +x "$dir/llama-server" 2>/dev/null || true if [ "$OS" = "darwin" ]; then xattr -cr "$dir" 2>/dev/null || true codesign --force --sign - "$dir/llama-server" 2>/dev/null || true fi mkdir -p "$DEST_DIR" ln -sf "$dir/llama-server" "$DEST_DIR/llama-server" if "$DEST_DIR/llama-server" --version >/dev/null 2>&1; then echo " ✓ llama-server installed → $dir (linked into $DEST_DIR)" LCPP_OK=1 else rm -f "$DEST_DIR/llama-server" echo " ✗ prebuilt llama-server didn't run here — removed it." fi fi fi rm -rf "$tmp" [ "$LCPP_OK" = "1" ] } # 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) # No package manager required (no Homebrew dependency). MAC_ASSET="" [ "$ARCH" = "arm64" ] && MAC_ASSET="macos-arm64" [ "$ARCH" = "amd64" ] && MAC_ASSET="macos-x64" install_prebuilt_llamacpp "$MAC_ASSET" || true if [ "$LCPP_OK" != "1" ]; then echo " ✗ couldn't auto-install llama.cpp. Install one manually" echo " (brew install llama.cpp, or build from" echo " https://github.com/ggml-org/llama.cpp) before 'memdoor setup'." fi ;; linux) # No llama.cpp in most distro repos and a from-source build is # a multi-minute cmake compile — too much friction for the # one-liner. Fetch the prebuilt CPU llama-server from # llama.cpp's GitHub releases instead. Two things a bare box # needs (both verified in a clean ubuntu:24.04 container): # 1. libgomp1 (GNU OpenMP) — a SYSTEM lib the binary links # but the archive does NOT ship; absent on minimal images. # Install it via whatever package manager exists. # 2. the llama.cpp .so files — shipped IN the archive next to # llama-server, found via its RUNPATH=$ORIGIN. So keep the # whole extracted dir together and symlink only the binary # onto PATH ($ORIGIN resolves through the symlink to the # real dir, so the libs are found). # Best-effort throughout; a self-check REMOVES a binary that # can't print --version so we never leave a broken one on PATH. # (1) libgomp1 — only if not already present. if ! ldconfig -p 2>/dev/null | grep -q libgomp; then SUDO="" [ "$(id -u)" != "0" ] && command -v sudo >/dev/null 2>&1 && SUDO="sudo" if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update -qq >/dev/null 2>&1 || true $SUDO apt-get install -y -qq libgomp1 >/dev/null 2>&1 || true elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y -q libgomp >/dev/null 2>&1 || true elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y -q libgomp >/dev/null 2>&1 || true elif command -v pacman >/dev/null 2>&1; then $SUDO pacman -Sy --noconfirm --needed gcc-libs >/dev/null 2>&1 || true elif command -v zypper >/dev/null 2>&1; then $SUDO zypper -q install -y libgomp1 >/dev/null 2>&1 || true elif command -v apk >/dev/null 2>&1; then $SUDO apk add --quiet libgomp >/dev/null 2>&1 || true fi fi # (2) prebuilt llama.cpp. LCPP_ASSET="" [ "$ARCH" = "amd64" ] && LCPP_ASSET="ubuntu-x64" [ "$ARCH" = "arm64" ] && LCPP_ASSET="ubuntu-arm64" install_prebuilt_llamacpp "$LCPP_ASSET" || true if [ "$LCPP_OK" != "1" ]; then echo " ↳ couldn't auto-install llama.cpp. Install libgomp1 + put a" echo " llama-server on PATH (https://github.com/ggml-org/llama.cpp)" echo " before 'memdoor setup'." fi ;; esac else echo " ✓ llama-server already on PATH" fi # Llamafit is imported as a Go library inside memdoor and started # in-process on demand (see pkg/llm/local). The install path no longer # `go install`s a standalone binary — `memdoor 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 # `memdoor llm auto` aborted after a 20GB model pull. If you still # want the standalone binary for `memdoor 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 "✓ Memdoor 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 ~/.memdoor/llm/models/) — fresh installs get nothing pulled # here; the user runs `memdoor llm auto` themselves after setup. # # Why this lives in install.sh rather than only in `memdoor upgrade`: # the canonical install path is `curl -fsSL memdoor.ai/install.sh | bash` # (or its equivalent piped from `memdoor upgrade`). Putting the post- # swap pull hook only in `memdoor 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 an # a 64 GB M-series 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/.memdoor/llm/models" ] && ls "$HOME/.memdoor/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 [ "$HAD_DATA_DIR" = "1" ]; then echo "Existing data dir detected at ~/.memdoor." echo echo "Next:" if [ "${WAS_RUNNING:-0}" = "1" ]; then echo " memdoor gateway & # restart the gateway (it was stopped above)" else echo " memdoor gateway & # start the gateway" fi echo " → then open http://localhost:18789" else echo "Next:" echo " memdoor setup # one-step onboarding (workspace + admin + first wiki + LLM)" echo " → then open http://localhost:18789" echo echo "Heads-up: first setup downloads a local model (~5 GB, one time) before" echo "your first answer. After that, everything runs offline on your machine." fi