Installation guide ↗Download Bodek ↗One-command installer for macOS & Linux — it also sets up the Odek engine (needs a provider API key). Windows binaries are on the releases page.
Start here. Build from here.
Starts Odek’s server and opens a fresh session in the current directory.
bodek
Pick up the thread.
Continue this directory’s last session instead of starting fresh.
bodek --resume
diff --git a/docs/install.sh b/docs/install.sh
new file mode 100644
index 0000000..0843e3a
--- /dev/null
+++ b/docs/install.sh
@@ -0,0 +1,205 @@
+#!/bin/sh
+# bodek installer — downloads prebuilt release binaries from GitHub.
+#
+# Usage:
+# curl -fsSL https://bodek.21no.de/install.sh | sh
+# curl -fsSL https://bodek.21no.de/install.sh | sh -s -- --with-odek
+# sh install.sh [--with-odek] # install odek without asking
+#
+# macOS and Linux. Windows users: grab a binary from the releases page.
+#
+# Installs bodek (and optionally odek) for the current platform into
+# ~/.local/bin (or /usr/local/bin when writable). Verifies every download
+# against the release checksums.txt when a sha256 tool is available.
+set -eu
+
+BODEK_REPO="BackendStack21/bodek"
+ODEK_REPO="BackendStack21/odek"
+GH_API="https://api.github.com/repos"
+GH_DL="https://github.com"
+
+say() { printf '==> %s\n' "$*"; }
+warn() { printf 'warning: %s\n' "$*" >&2; }
+die() { printf 'error: %s\n' "$*" >&2; exit 1; }
+
+# ---------------------------------------------------------------- platform
+OS=$(uname -s)
+ARCH=$(uname -m)
+case "$OS" in
+ Darwin) os=darwin ;;
+ Linux) os=linux ;;
+ *) die "unsupported OS '$OS' — this installer covers macOS and Linux." ;;
+esac
+case "$ARCH" in
+ x86_64|amd64) arch=amd64 ;;
+ aarch64|arm64) arch=arm64 ;;
+ *) die "unsupported architecture '$ARCH'." ;;
+esac
+
+command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 \
+ || die "need curl or wget to download."
+
+fetch() { # fetch -> stdout
+ if command -v curl >/dev/null 2>&1; then curl -fsSL "$1"
+ else wget -qO- "$1"; fi
+}
+download() { # download
+ if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$2" "$1"
+ else wget -qO "$2" "$1"; fi
+}
+
+checksum() { # checksum
+ if command -v sha256sum >/dev/null 2>&1; then
+ actual=$(sha256sum "$1" | awk '{print $1}')
+ elif command -v shasum >/dev/null 2>&1; then
+ actual=$(shasum -a 256 "$1" | awk '{print $1}')
+ else
+ warn "no sha256 tool found — skipping checksum verification."
+ return 0
+ fi
+ [ "$actual" = "$2" ] || die "checksum mismatch for $(basename "$1") — download corrupted or tampered. Aborting."
+}
+
+# ---------------------------------------------------------------- install dir
+install_dir=""
+pick_install_dir() {
+ for d in /usr/local/bin "$HOME/.local/bin"; do
+ if mkdir -p "$d" 2>/dev/null && [ -w "$d" ]; then install_dir=$d; return; fi
+ done
+ die "no writable install directory found (tried /usr/local/bin, ~/.local/bin)."
+}
+
+path_has_dir() { case ":$PATH:" in *":$1:"*) return 0 ;; *) return 1 ;; esac; }
+
+# ---------------------------------------------------------------- releases
+latest_tag() { # latest_tag -> "vX.Y.Z" (empty on failure)
+ # Resolve via the redirect of /releases/latest — no API rate limits.
+ url="$GH_DL/$1/releases/latest"
+ if command -v curl >/dev/null 2>&1; then
+ loc=$(curl -fsSI -o /dev/null -w '%{redirect_url}' "$url" 2>/dev/null || true)
+ else
+ loc=$(wget -qS --spider "$url" 2>&1 | sed -n 's/^ *Location: *//p' | tail -1)
+ fi
+ case "$loc" in
+ */tag/*) printf '%s\n' "${loc##*/}" ;;
+ *) printf '' ;;
+ esac
+}
+
+checksum_line() { # checksum_line -> hex digest or ""
+ fetch "$GH_DL/$1/releases/download/$2/checksums.txt" 2>/dev/null \
+ | awk -v a="$3" '$2 == a {print $1; exit}'
+}
+
+TMP=$(mktemp -d) || die "cannot create temp dir."
+trap 'rm -rf "$TMP"' EXIT INT TERM
+
+install_asset() { # install_asset
+ repo=$1 tag=$2 asset=$3 bin=$4
+ say "Downloading $asset ($tag)"
+ download "$GH_DL/$repo/releases/download/$tag/$asset" "$TMP/$asset"
+ digest=$(checksum_line "$repo" "$tag" "$asset")
+ if [ -n "$digest" ]; then
+ checksum "$TMP/$asset" "$digest"
+ say "Checksum verified."
+ else
+ # Fail closed: a missing/unfetchable checksum line is suspicious, not a
+ # reason to install unverified code.
+ die "no checksum entry for $asset — refusing to install unverified."
+ fi
+ case "$asset" in
+ *.tar.gz) tar -xzf "$TMP/$asset" -C "$TMP" "$bin" 2>/dev/null \
+ || tar -xzf "$TMP/$asset" -C "$TMP" ;;
+ *) cp "$TMP/$asset" "$TMP/$bin" && chmod +x "$TMP/$bin" ;;
+ esac
+ [ -f "$TMP/$bin" ] || die "binary '$bin' not found in archive."
+ mv -f "$TMP/$bin" "$install_dir/$bin"
+ chmod +x "$install_dir/$bin"
+ say "Installed $install_dir/$bin"
+}
+
+# ---------------------------------------------------------------- odek
+install_odek() {
+ tag=$(latest_tag "$ODEK_REPO")
+ [ -n "$tag" ] || die "cannot resolve the latest odek release (network or GitHub problem — install curl if it is missing)."
+ install_asset "$ODEK_REPO" "$tag" "odek-$os-$arch" odek
+ cat </dev/null 2>&1; then
+ say "Odek found: $(command -v odek) — skipping."
+ return
+ fi
+ printf 'Odek (the engine bodek drives) is not installed.\n'
+ if [ "${1:-}" = "--with-odek" ]; then
+ install_odek
+ return
+ fi
+ printf 'Install it now from prebuilt binaries? [y/N] '
+ # Never read the script's own stdin: under `curl | sh` that would swallow
+ # the rest of the script. Use the terminal, else default to No.
+ answer=""
+ if [ -t 0 ]; then
+ read -r answer
+ elif [ -t 1 ] && [ -r /dev/tty ]; then
+ read -r answer > ~/.profile && source ~/.profile
+EOF
+fi
+
+cat <p{color:var(--muted);font-size:16px;margin:0}.tabs{display:flex;gap:4px;flex-wrap:wrap;border-bottom:1px solid var(--line);padding:0 16px}.tabs button{color:var(--muted);background:none;border:0;border-bottom:2px solid transparent;padding:18px 16px;cursor:pointer;font-size:14px}.tabs button[aria-selected=true]{color:var(--amber);border-color:var(--amber)}.tabs button:hover{color:var(--text)}.theme-gallery{border:1px solid #ffbb5230;border-radius:12px;overflow:hidden;background:#111216;box-shadow:0 30px 100px -60px #ffbb5250}.theme-swatch{display:inline-block;width:9px;height:9px;margin-right:8px;border-radius:50%;background:var(--amber)}.theme-swatch.ember-light{background:#f4ead5}.theme-swatch.high-contrast{background:#fff}.theme-swatch.classic{background:#bd8dfa}.terminal-bar{display:flex;justify-content:space-between;gap:16px;padding:14px 20px;font:12px/1.5 var(--mono);color:var(--muted);border-bottom:1px solid var(--line)}.theme-panel img{width:100%;height:auto;display:block}.theme-panel>a{display:block}.gallery-caption{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:16px 0 0;color:var(--muted);font-size:12px}.gallery-caption p{margin:0 auto 0 0}.gallery-caption code{color:var(--amber)}.workflow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:40px;border-bottom:1px solid var(--line);padding-bottom:64px;margin-top:60px}.workflow p{color:var(--muted);font-size:16px}.feature-key{display:block;min-height:32px;font-size:13px;color:var(--muted)}kbd{display:inline-block;min-width:26px;text-align:center;border:1px solid #ffffff28;border-bottom-width:3px;border-radius:5px;padding:3px 7px;font-size:12px;color:var(--text);background:#17191d}.why-section{display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.2fr);gap:88px}.why-intro p{color:var(--muted);max-width:350px}.why-intro .eyebrow{color:var(--amber)}.why-intro h2{font-size:clamp(32px,4vw,46px)}.why-intro h2 span{color:var(--amber)}.why-list{list-style:none;padding:0;margin:0}.why-list li{display:grid;grid-template-columns:32px minmax(0,1fr);gap:20px;padding:34px 0;border-top:1px solid var(--line)}.why-list li:first-child{padding-top:0;border-top:0}.why-list h3{font-size:32px;line-height:1.12;margin:0 0 20px}.why-list .eyebrow{font-size:11px;color:var(--muted);margin-bottom:16px}.why-list p:not(.eyebrow){color:var(--muted)}.why-list a{font-size:14px;margin-right:15px}.number{font:13px var(--mono);color:var(--amber);padding-top:2px}.engine-section{display:grid;grid-template-columns:1.2fr 1fr;gap:80px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:56px 0}.engine-section p{color:var(--muted)}.engine-section .eyebrow{color:var(--amber)}.connection{align-self:center}.connection>div{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:20px 24px;display:flex;justify-content:space-between;align-items:center;gap:16px}.connection strong{font:18px var(--mono);color:var(--amber)}.connection span{font-size:12px;color:var(--muted)}.connection p{text-align:center;margin:14px 0;font-size:24px}.connection p span{vertical-align:middle;margin-left:8px}.install-links{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:32px 0}.install-links>span{display:block;width:100%;font-size:13px;color:var(--muted);margin-top:4px}.launch-examples{border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}.launch-examples [data-panel]{padding:28px}.launch-examples h3{margin:0 0 10px}.launch-examples p{color:var(--muted)}.command{display:flex;align-items:center;gap:20px;border-top:1px solid var(--line);padding-top:18px}.command pre{margin:0;overflow:auto;flex:1;min-width:0;font-size:15px;color:var(--amber)}.command button{background:#ffffff08;color:var(--text);border:1px solid var(--line);border-radius:5px;padding:7px 14px;cursor:pointer;font-size:13px}.hint{margin-top:20px}.closing{margin-top:90px;padding-top:48px;border-top:1px solid var(--line);display:flex;gap:30px;align-items:center;justify-content:space-between}.closing p{font-size:30px;line-height:1.15;letter-spacing:-.035em;margin:0}.closing p span{color:var(--amber)}[role=tabpanel]:focus-visible{outline:2px solid var(--amber);outline-offset:-3px}@media(max-width:900px){.why-section{gap:40px}.engine-section{gap:32px}.connection>div{align-items:start;flex-direction:column;gap:6px}.workflow{gap:24px}.section-head{align-items:start;flex-direction:column;gap:20px}}@media(max-width:650px){nav{font-size:12px}header .wrap{padding-top:14px;padding-bottom:14px}.hero-meta span{padding:0 2px}.tabs{padding:0 8px;gap:0}.tabs button{padding:14px 10px;font-size:12px}.terminal-bar{font-size:10px;padding:12px;flex-wrap:wrap}.workflow,.why-section,.engine-section{grid-template-columns:1fr}.workflow{gap:28px}.workflow h3{margin-top:14px}.why-list h3{font-size:30px}.why-intro p{max-width:none}.why-list li{gap:12px;grid-template-columns:24px minmax(0,1fr)}.engine-section{gap:30px}.connection>div{flex-direction:row;align-items:center}.closing{align-items:start;flex-direction:column;gap:24px}.launch-examples [data-panel]{padding:20px}.button{gap:20px}.command{gap:10px}}@media(prefers-reduced-motion:no-preference){.tabs button,.button{transition:background-color .16s,border-color .16s,color .16s}}
+.skip{position:absolute;top:-100px;left:20px;padding:10px;background:var(--panel);z-index:5}.skip:focus{top:10px}.brand{color:var(--text)}.brand:hover{text-decoration:none}.nav-start{color:var(--amber)}.hero-meta span{color:#62636d;padding:0 10px}.hero{position:relative}.hero:after{content:'';position:absolute;pointer-events:none;inset:-80px -20px 0 25%;z-index:-1;background:radial-gradient(ellipse at top right,#ffbb5210,transparent 65%)}.showcase{margin-top:64px}.section-head{display:flex;justify-content:space-between;align-items:end;gap:30px;margin-bottom:30px}.section-head h2{margin-bottom:0}.section-head>p{color:var(--muted);font-size:16px;margin:0}.tabs{display:flex;gap:4px;flex-wrap:wrap;border-bottom:1px solid var(--line);padding:0 16px}.tabs button{color:var(--muted);background:none;border:0;border-bottom:2px solid transparent;padding:18px 16px;cursor:pointer;font-size:14px}.tabs button[aria-selected=true]{color:var(--amber);border-color:var(--amber)}.tabs button:hover{color:var(--text)}.theme-gallery{border:1px solid #ffbb5230;border-radius:12px;overflow:hidden;background:#111216;box-shadow:0 30px 100px -60px #ffbb5250}.theme-swatch{display:inline-block;width:9px;height:9px;margin-right:8px;border-radius:50%;background:var(--amber)}.theme-swatch.ember-light{background:#f4ead5}.theme-swatch.high-contrast{background:#fff}.theme-swatch.classic{background:#bd8dfa}.terminal-bar{display:flex;justify-content:space-between;gap:16px;padding:14px 20px;font:12px/1.5 var(--mono);color:var(--muted);border-bottom:1px solid var(--line)}.theme-panel img{width:100%;height:auto;display:block}.theme-panel>a{display:block}.gallery-caption{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:16px 0 0;color:var(--muted);font-size:12px}.gallery-caption p{margin:0 auto 0 0}.gallery-caption code{color:var(--amber)}.workflow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:40px;border-bottom:1px solid var(--line);padding-bottom:64px;margin-top:60px}.workflow p{color:var(--muted);font-size:16px}.feature-key{display:block;min-height:32px;font-size:13px;color:var(--muted)}kbd{display:inline-block;min-width:26px;text-align:center;border:1px solid #ffffff28;border-bottom-width:3px;border-radius:5px;padding:3px 7px;font-size:12px;color:var(--text);background:#17191d}.why-section{display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.2fr);gap:88px}.why-intro p{color:var(--muted);max-width:350px}.why-intro .eyebrow{color:var(--amber)}.why-intro h2{font-size:clamp(32px,4vw,46px)}.why-intro h2 span{color:var(--amber)}.why-list{list-style:none;padding:0;margin:0}.why-list li{display:grid;grid-template-columns:32px minmax(0,1fr);gap:20px;padding:34px 0;border-top:1px solid var(--line)}.why-list li:first-child{padding-top:0;border-top:0}.why-list h3{font-size:32px;line-height:1.12;margin:0 0 20px}.why-list .eyebrow{font-size:11px;color:var(--muted);margin-bottom:16px}.why-list p:not(.eyebrow){color:var(--muted)}.why-list a{font-size:14px;margin-right:15px}.number{font:13px var(--mono);color:var(--amber);padding-top:2px}.engine-section{display:grid;grid-template-columns:1.2fr 1fr;gap:80px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:56px 0}.engine-section p{color:var(--muted)}.engine-section .eyebrow{color:var(--amber)}.connection{align-self:center}.connection>div{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:20px 24px;display:flex;justify-content:space-between;align-items:center;gap:16px}.connection strong{font:18px var(--mono);color:var(--amber)}.connection span{font-size:12px;color:var(--muted)}.connection p{text-align:center;margin:14px 0;font-size:24px}.connection p span{vertical-align:middle;margin-left:8px}.install-links{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:32px 0}.install-links>.command{flex:1 1 100%;min-width:0}.install-links>span{display:block;width:100%;font-size:13px;color:var(--muted);margin-top:4px}.launch-examples{border:1px solid var(--line);border-radius:10px;background:var(--panel);overflow:hidden}.launch-examples [data-panel]{padding:28px}.launch-examples h3{margin:0 0 10px}.launch-examples p{color:var(--muted)}.command{display:flex;align-items:center;gap:20px;border-top:1px solid var(--line);padding-top:18px}.command pre{margin:0;overflow:auto;flex:1;min-width:0;font-size:15px;color:var(--amber)}.command button{background:#ffffff08;color:var(--text);border:1px solid var(--line);border-radius:5px;padding:7px 14px;cursor:pointer;font-size:13px}.hint{margin-top:20px}.closing{margin-top:90px;padding-top:48px;border-top:1px solid var(--line);display:flex;gap:30px;align-items:center;justify-content:space-between}.closing p{font-size:30px;line-height:1.15;letter-spacing:-.035em;margin:0}.closing p span{color:var(--amber)}[role=tabpanel]:focus-visible{outline:2px solid var(--amber);outline-offset:-3px}@media(max-width:900px){.why-section{gap:40px}.engine-section{gap:32px}.connection>div{align-items:start;flex-direction:column;gap:6px}.workflow{gap:24px}.section-head{align-items:start;flex-direction:column;gap:20px}}@media(max-width:650px){nav{font-size:12px}header .wrap{padding-top:14px;padding-bottom:14px}.hero-meta span{padding:0 2px}.tabs{padding:0 8px;gap:0}.tabs button{padding:14px 10px;font-size:12px}.terminal-bar{font-size:10px;padding:12px;flex-wrap:wrap}.workflow,.why-section,.engine-section{grid-template-columns:1fr}.workflow{gap:28px}.workflow h3{margin-top:14px}.why-list h3{font-size:30px}.why-intro p{max-width:none}.why-list li{gap:12px;grid-template-columns:24px minmax(0,1fr)}.engine-section{gap:30px}.connection>div{flex-direction:row;align-items:center}.closing{align-items:start;flex-direction:column;gap:24px}.launch-examples [data-panel]{padding:20px}.button{gap:20px}.command{gap:10px}}@media(prefers-reduced-motion:no-preference){.tabs button,.button{transition:background-color .16s,border-color .16s,color .16s}}