#!/bin/sh # s9n CLI installer. # # curl -fsSL https://install.sekondbrain.ai | sh # # Downloads the latest prebuilt s9n binary for your OS/arch from the public # release repo, verifies its SHA-256, installs it under ~/.s9n/lib, and links # the launcher into a bin dir on your PATH. No Python required. # # Env overrides: # S9N_VERSION pin a version (e.g. v0.2.0); default: latest release # S9N_BIN_DIR where to symlink the launcher (default: ~/.local/bin) # GITHUB_TOKEN optional; only used to raise the GitHub API rate limit when # resolving the latest version. Never sent to the download. set -eu REPO="SeKondBrainAILabs/homebrew-s9n" BIN_DIR="${S9N_BIN_DIR:-$HOME/.local/bin}" LIB_ROOT="$HOME/.s9n/lib" say() { printf '%s\n' "$*"; } err() { printf 'error: %s\n' "$*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } main() { # --- detect platform ------------------------------------------------------- os=$(uname -s) arch=$(uname -m) case "$os" in Darwin) os_tag="macos" ;; Linux) os_tag="linux" ;; *) err "unsupported OS '$os'. On Windows, run this in PowerShell instead: irm https://raw.githubusercontent.com/$REPO/refs/heads/main/install.ps1 | iex" ;; esac case "$arch" in arm64|aarch64) arch_tag="arm64" ;; x86_64|amd64) arch_tag="x64" ;; *) err "unsupported architecture '$arch'." ;; esac # macOS ships a single universal2 binary (Apple Silicon + Intel). if [ "$os_tag" = "macos" ]; then target="macos-universal" else target="${os_tag}-${arch_tag}" fi # --- resolve version ------------------------------------------------------- # The tap holds two products in ONE release stream: s9n tags are `vX.Y.Z`, the # kemory CLI's are `cli-vX.Y.Z`. /releases/latest returns whichever shipped # last, which is usually a kemory tag carrying no s9n asset — asking it for the # version made every install 404 on the download. Take the newest tag that is # actually an s9n release instead. The exact-match pattern also skips # prereleases (`v0.2.0-rc1`), which /releases (unlike /releases/latest) lists. version="${S9N_VERSION:-}" if [ -z "$version" ]; then api="https://api.github.com/repos/$REPO/releases?per_page=100" have curl || have wget || err "need curl or wget." releases=$(api_get "$api") || err "could not reach the GitHub API. A 403 here is the unauthenticated rate limit — 60 requests/hour per IP, which CI runners and anyone behind a shared NAT exhaust routinely. Either set GITHUB_TOKEN, or skip the lookup entirely by pinning S9N_VERSION." version=$(printf '%s' "$releases" \ | sed -n 's/.*"tag_name" *: *"\([^"]*\)".*/\1/p' \ | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | head -1) [ -n "$version" ] || err "could not find an s9n release (its tags look like v0.1.3). Set S9N_VERSION to pin one." fi archive="s9n-${target}.tar.gz" base="https://github.com/$REPO/releases/download/$version" say "Installing s9n $version ($target)…" # --- download + verify ----------------------------------------------------- tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT fetch "$base/$archive" "$tmp/$archive" || err "download failed: $base/$archive" if fetch "$base/$archive.sha256" "$tmp/$archive.sha256" 2>/dev/null; then expected=$(tr -d '[:space:]' < "$tmp/$archive.sha256") if have shasum; then actual=$(shasum -a 256 "$tmp/$archive" | awk '{print $1}') elif have sha256sum; then actual=$(sha256sum "$tmp/$archive" | awk '{print $1}') else actual=""; fi if [ -n "$actual" ] && [ "$actual" != "$expected" ]; then err "checksum mismatch (expected $expected, got $actual)" fi [ -n "$actual" ] && say " checksum ok" fi # --- install --------------------------------------------------------------- dest="$LIB_ROOT/$version" rm -rf "$dest" mkdir -p "$dest" tar -xzf "$tmp/$archive" -C "$dest" [ -x "$dest/s9n" ] || chmod +x "$dest/s9n" 2>/dev/null || true mkdir -p "$BIN_DIR" ln -sf "$dest/s9n" "$BIN_DIR/s9n" # point a stable "current" symlink at this version too ln -sfn "$dest" "$LIB_ROOT/current" # macOS: strip the quarantine flag so Gatekeeper doesn't block the unsigned binary if [ "$os_tag" = "macos" ] && have xattr; then xattr -dr com.apple.quarantine "$dest" 2>/dev/null || true fi say "✓ Installed s9n → $BIN_DIR/s9n" # --- PATH hint ------------------------------------------------------------- case ":$PATH:" in *":$BIN_DIR:"*) ;; *) say "" say "⚠ $BIN_DIR is not on your PATH. Add it:" say " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.zshrc && exec \$SHELL" ;; esac say "" say "Next:" say " s9n login # sign in (browser)" say " s9n install # wire it into Claude Code, then /mcp" } fetch() { if have curl; then curl -fsSL "$1" -o "$2"; else wget -qO "$2" "$1"; fi } # Only ever used for api.github.com. The token deliberately does not reach # fetch(): release downloads redirect to a separate asset host, and an # Authorization header follows the redirect. api_get() { if have curl; then if [ -n "${GITHUB_TOKEN:-}" ] then curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" "$1" else curl -fsSL "$1" fi else if [ -n "${GITHUB_TOKEN:-}" ] then wget -qO- --header="Authorization: Bearer $GITHUB_TOKEN" "$1" else wget -qO- "$1" fi fi } # Everything above is a definition; this is the only line that acts. The script # is piped straight into `sh`, so a connection that drops mid-transfer would # otherwise run whatever prefix arrived — downloading without installing, or # unpacking into a half-made directory. sh cannot reach this call until it has # parsed the whole file, so a truncated body does nothing at all. main "$@"