#!/usr/bin/env bash # C9RE installer: deterministic on-device memory for AI coding agents. # macOS / Linux: curl -fsSL https://c9re.dev/install.sh | bash # Windows (in PowerShell): irm https://c9re.dev/install.ps1 | iex # Everything runs inside main(), so a download cut short runs nothing at all. # # Options: --force (reinstall), --allow-downgrade, --yes (no questions), --no-setup, # --no-modify-path. Run with curl -fsSL https://c9re.dev/install.sh | bash -s -- --yes set -euo pipefail # Public half of the C9RE release key. A release installs only if release.json is signed by it. C9RE_PUBLIC_KEY="EjKK+xsTzlw3InloLYCgomKAwcRopm2N5DIN6/8z45w=" main() { umask 077 local FORCE=0 ALLOW_DOWNGRADE=0 YES=0 SETUP=1 MODIFY_PATH=1 for arg in "$@"; do case "$arg" in --force|-f) FORCE=1 ;; --allow-downgrade) ALLOW_DOWNGRADE=1 ;; --yes|-y) YES=1 ;; --no-setup) SETUP=0 ;; --no-modify-path) MODIFY_PATH=0 ;; esac done # A terminal to ask questions on (curl | bash still has one at /dev/tty) local TTY=0 if [ "${YES}" -eq 0 ] && { : /dev/null; then TTY=1; fi if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then BOLD="\033[1m"; DIM="\033[2m"; GREEN="\033[1;32m"; CYAN="\033[1;36m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; RESET="\033[0m" FANCY=1 else BOLD=""; DIM=""; GREEN=""; CYAN=""; YELLOW=""; RED=""; RESET="" FANCY=0 fi ok() { echo -e " ${GREEN}✓${RESET} $*"; } info() { echo -e " ${CYAN}›${RESET} $*"; } warn() { echo -e " ${YELLOW}!${RESET} $*"; } fail() { echo -e "\n ${RED}✗ $*${RESET}" >&2; exit 1; } # ask "question": yes unless they say no; with no terminal (or --yes) the answer is yes ask() { [ "${TTY}" -eq 1 ] || return 0 local a="" printf " %b [Y/n] " "$1" >/dev/tty read -r a /dev/null 2>&1; then "${ps}" -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "${script}")" && { rm -rf "${dir}"; exit 0; } break fi done fi rm -rf "${dir}" echo "Couldn't run the Windows installer from here. Open PowerShell and run:" echo " irm ${BASE}/install.ps1 | iex" exit 1 ;; esac if [ "${FANCY}" -eq 1 ]; then local c local -a ring=(99 99 135 171 207 205 205 42 42 36 36) local -a eye=( " ▄▄▄▄▄▄▄▄▄▄▄▄" " ▄▄▄███▀▀▀██████▀▀▀███▄▄▄" " ▄▄██▀▀ ▄██▀▀▀▀▀▀██▄ ▀▀██▄▄" " ▄▄█▀▀ ██▀ ▄▄█▀ ▀██ ▀▀█▄▄" " ▄█▀▀ ██ ████▄▄▄▄ ██ ▀▀█▄" " ██ ██ ██████████ ██ ██" " ▀█▄▄ ██ ████████ ██ ▄▄█▀" " ▀▀█▄▄ ██▄ ▀▀██▀▀ ▄██ ▄▄█▀▀" " ▀▀██▄▄ ▀██▄▄▄▄▄▄██▀ ▄▄██▀▀" " ▀▀▀███▄▄▄██████▄▄▄███▀▀▀" " ▀▀▀▀▀▀▀▀▀▀▀▀" ) echo for c in "${!eye[@]}"; do echo -e "\033[38;5;${ring[$c]}m${eye[$c]}${RESET}"; done echo -e "\n ${BOLD}C 9 R E${RESET} ${DIM}memory for coding agents${RESET}\n" else echo "C9RE installer" fi # 1. Tools. Anything missing is installed with the system package manager (asking first). local PM="" SUDO="" if command -v brew >/dev/null 2>&1 && [ "$(uname -s)" = "Darwin" ]; then PM="brew" elif command -v apt-get >/dev/null 2>&1; then PM="apt-get" elif command -v dnf >/dev/null 2>&1; then PM="dnf" elif command -v yum >/dev/null 2>&1; then PM="yum" elif command -v pacman >/dev/null 2>&1; then PM="pacman" elif command -v apk >/dev/null 2>&1; then PM="apk" elif command -v zypper >/dev/null 2>&1; then PM="zypper" fi if [ "${PM}" != "brew" ] && [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi need() { # need command -v "$1" >/dev/null 2>&1 && return 0 local cmd="" case "${PM}" in brew) cmd="brew install $2" ;; apt-get) cmd="${SUDO} apt-get install -y $2" ;; dnf|yum) cmd="${SUDO} ${PM} install -y $2" ;; pacman) cmd="${SUDO} pacman -S --noconfirm $2" ;; apk) cmd="${SUDO} apk add $2" ;; zypper) cmd="${SUDO} zypper -n install $2" ;; *) fail "$1 is missing and no package manager was found. Install $2, then run this again." ;; esac cmd="${cmd# }" if [ -n "${SUDO}" ] && ! command -v sudo >/dev/null 2>&1; then fail "$1 is missing. As root, run: ${cmd#sudo } then run this again." fi # With no one to ask, system packages are only installed when --yes said so if [ "${TTY}" -eq 0 ] && [ "${YES}" -eq 0 ]; then fail "$1 is missing. Install it with: ${cmd} (or re-run with --yes to let the installer do it)" fi if [ "${TTY}" -eq 0 ] && [ -n "${SUDO}" ] && ! sudo -n true 2>/dev/null; then fail "$1 is missing. Install it with: ${cmd} then run this again." fi ask "C9RE needs ${BOLD}$1${RESET}. Install it now with ${CYAN}${cmd}${RESET}?" \ || fail "$1 is needed. Install it with: ${cmd} then run this again." # Quiet unless it fails; then the package manager's own words are shown local log log="$(mktemp)" if [ "${PM}" = "apt-get" ]; then { ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" >"${log}" 2>&1 \ || { ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get update -qq >>"${log}" 2>&1 \ && ${SUDO} env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" >>"${log}" 2>&1; }; } \ || { tail -5 "${log}" >&2; rm -f "${log}"; fail "couldn't install $2. Install it yourself, then run this again."; } else eval "${cmd}" >"${log}" 2>&1 || { tail -5 "${log}" >&2; rm -f "${log}"; fail "couldn't install $2. Install it yourself, then run this again."; } fi rm -f "${log}" command -v "$1" >/dev/null 2>&1 || fail "$2 installed, but $1 still isn't on your PATH." ok "Installed $2" } need curl curl need tar tar # 2. Bun, the runtime C9RE runs on. Found on PATH or where its installer puts it, else installed. local BUN="" find_bun() { BUN="" if command -v bun >/dev/null 2>&1; then BUN="$(command -v bun)" elif [ -x "${BUN_INSTALL:-${HOME}/.bun}/bin/bun" ]; then BUN="${BUN_INSTALL:-${HOME}/.bun}/bin/bun" elif [ -x /opt/homebrew/bin/bun ]; then BUN=/opt/homebrew/bin/bun elif [ -x /usr/local/bin/bun ]; then BUN=/usr/local/bin/bun fi } find_bun if [ -z "${BUN}" ]; then ask "C9RE runs on ${BOLD}Bun${RESET}, which isn't installed. Install it now (from bun.sh, into ~/.bun, no sudo)?" \ || fail "Bun is needed. Install it with: curl -fsSL https://bun.sh/install | bash then run this again." [ "$(uname -s)" = "Darwin" ] || need unzip unzip info "Installing Bun…" curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1 || fail "the Bun installer failed. See https://bun.sh/docs/installation" find_bun [ -n "${BUN}" ] || fail "Bun installed, but it can't be found. Open a new terminal and run this again." fi local BUN_VER BUN_VER=$("${BUN}" --version 2>/dev/null) || fail "${BUN} doesn't run. Reinstall Bun: curl -fsSL https://bun.sh/install | bash" if ! "${BUN}" -e 'const [a,b]=process.argv[1].split(".").map(Number); process.exit(a>1||(a===1&&b>=1)?0:1)' "${BUN_VER}"; then ask "C9RE needs Bun 1.1 or newer (you have ${BUN_VER}). Upgrade it now?" \ || fail "C9RE needs Bun 1.1 or newer. Run: bun upgrade" "${BUN}" upgrade >/dev/null 2>&1 || fail "bun upgrade failed. Run it yourself, then run this again." BUN_VER=$("${BUN}" --version) fi ok "Bun ${BUN_VER} ${DIM}${BUN}${RESET}" local C9RE_HOME="${C9RE_HOME:-${HOME}/.c9re}" local LIB="${C9RE_HOME}/lib" local BIN="${HOME}/.local/bin" mkdir -p "${C9RE_HOME}" "${BIN}" chmod 700 "${C9RE_HOME}" # 3. Fetch and verify the release manifest (signature, version, download location) local MANIFEST CHECKED MANIFEST=$(curl -fsSL --max-time 20 "${BASE}/release.json") \ || fail "couldn't fetch ${BASE}/release.json. Check your connection and try again." CHECKED=$(printf '%s' "${MANIFEST}" | "${BUN}" -e ' const { createPublicKey, verify } = require("node:crypto"); const [base, pub] = process.argv.slice(1); let m; try { m = JSON.parse(await Bun.stdin.text()); } catch { console.error("release.json is not valid JSON"); process.exit(1); } if (!/^\d+\.\d+\.\d+$/.test(m?.version || "") || !/^[0-9a-f]{64}$/.test(m?.sha256 || "") || typeof m?.sig !== "string") { console.error("release.json is malformed"); process.exit(1); } const prefix = `${base}/releases/${m.version}/`; if (typeof m.tarball_url !== "string" || !m.tarball_url.startsWith(prefix)) { console.error(`release.json points outside ${prefix}`); process.exit(1); } const key = createPublicKey({ key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(pub, "base64")]), format: "der", type: "spki" }); let ok = false; try { ok = verify(null, Buffer.from(`c9re-release\n${m.version}\n${m.sha256}\n`), key, Buffer.from(m.sig, "base64")); } catch {} if (!ok) { console.error("release.json is not signed by the C9RE release key"); process.exit(1); } console.log([m.version, m.tarball_url, m.sha256].join("\t")); ' "${BASE}" "${C9RE_PUBLIC_KEY}") || fail "refusing to install: the release did not verify. Nothing was changed." local VERSION TARBALL_URL SHA IFS=$'\t' read -r VERSION TARBALL_URL SHA <<< "${CHECKED}" ok "Release v${VERSION} ${DIM}signature verified${RESET}" local INSTALLED="" if [ -f "${LIB}/package.json" ]; then INSTALLED=$("${BUN}" -e 'try { console.log(require(process.argv[1]).version) } catch { console.log("") }' "${LIB}/package.json" 2>/dev/null || true) fi local FRESH=1 [ -z "${INSTALLED}" ] || FRESH=0 # An old (even if signed) release must never replace a newer one behind your back if [ -n "${INSTALLED}" ] && [ "${ALLOW_DOWNGRADE}" -eq 0 ] \ && ! "${BUN}" -e 'const [a,b]=process.argv.slice(1).map(v=>v.split(".").map(Number)); for (let i=0;i<3;i++) { if (a[i]!==b[i]) process.exit(a[i]>b[i]?0:1) } process.exit(0)' "${VERSION}" "${INSTALLED}"; then fail "the published release (v${VERSION}) is older than the installed v${INSTALLED}. Refusing to downgrade; pass --allow-downgrade if you really mean it." fi if [ -n "${INSTALLED}" ] && [ "${INSTALLED}" = "${VERSION}" ] && [ "${FORCE}" -eq 0 ]; then ok "C9RE v${INSTALLED} is already installed ${DIM}(--force reinstalls)${RESET}" else # 4. Download, check the sha256 the signature covers, unpack, swap in local STAGE="${C9RE_HOME}/lib.new" rm -rf "${STAGE}" mkdir -p "${STAGE}" trap 'rm -rf "${STAGE}"' EXIT curl -fsSL --max-time 120 "${TARBALL_URL}" -o "${STAGE}/.release.tar.gz" || fail "download failed. Nothing was changed." local ACTUAL ACTUAL=$("${BUN}" -e 'console.log(new Bun.CryptoHasher("sha256").update(await Bun.file(process.argv[1]).arrayBuffer()).digest("hex"))' "${STAGE}/.release.tar.gz") [ "${ACTUAL}" = "${SHA}" ] || fail "checksum mismatch (expected ${SHA:0:12}, got ${ACTUAL:0:12}). Nothing was changed." tar -xzf "${STAGE}/.release.tar.gz" -C "${STAGE}" || fail "couldn't unpack the release. Nothing was changed." rm -f "${STAGE}/.release.tar.gz" chmod 755 "${STAGE}/bin/c9re" if [ -d "${LIB}" ]; then rm -rf "${C9RE_HOME}/lib.prev" mv "${LIB}" "${C9RE_HOME}/lib.prev" fi mv "${STAGE}" "${LIB}" trap - EXIT if [ -n "${INSTALLED}" ]; then ok "Updated v${INSTALLED} → v${VERSION} ${DIM}your brain was not touched${RESET}" else ok "Installed v${VERSION} ${DIM}${LIB}${RESET}"; fi fi # 5. Commands. A launcher (not a symlink) names Bun itself, so c9 works even where bun isn't # on PATH (agents, cron, GUI apps). Someone else's c9 or c9re is never overwritten. local name target linked="" for name in c9re c9; do target="${BIN}/${name}" if [ -e "${target}" ] || [ -L "${target}" ]; then if [ -L "${target}" ] && [[ "$(readlink "${target}")" == "${C9RE_HOME}/"* ]]; then rm -f "${target}" elif [ -f "${target}" ] && head -c 400 "${target}" 2>/dev/null | grep -q "C9RE launcher"; then : else warn "${target} already exists and isn't C9RE's; left it alone." continue fi fi cat > "${target}.c9re-tmp" <&1) || fail "the c9 command was installed but doesn't run:\n ${SELFTEST}\n Please report this with the output of: ${BUN} --version" ok "Commands${linked} ${DIM}${BIN} (${SELFTEST%% (*})${RESET}" # 6. PATH: add ~/.local/bin for new terminals, in the shell's own startup file if [[ ":${PATH}:" != *":${BIN}:"* ]]; then local shell_name rc line shell_name="$(basename "${SHELL:-sh}")" case "${shell_name}" in zsh) rc="${ZDOTDIR:-${HOME}}/.zshrc"; line="export PATH=\"${BIN}:\$PATH\"" ;; bash) if [ "$(uname -s)" = "Darwin" ]; then rc="${HOME}/.bash_profile"; else rc="${HOME}/.bashrc"; fi; line="export PATH=\"${BIN}:\$PATH\"" ;; fish) rc="${HOME}/.config/fish/conf.d/c9re.fish"; line="fish_add_path \"${BIN}\"" ;; *) rc="${HOME}/.profile"; line="export PATH=\"${BIN}:\$PATH\"" ;; esac if [ "${MODIFY_PATH}" -eq 1 ]; then local PATH_NOTE="Open a new terminal (or run: ${CYAN}${line}${RESET}) so ${CYAN}c9${RESET} is found." if grep -q "c9re:path-start" "${rc}" 2>/dev/null \ || { mkdir -p "$(dirname "${rc}")" && printf '\n# c9re:path-start (added by the C9RE installer; c9 uninstall removes it)\n%s\n# c9re:path-end\n' "${line}" >> "${rc}"; } 2>/dev/null; then ok "Added ${BIN} to PATH ${DIM}in ${rc/#${HOME}/~}${RESET}" else warn "Couldn't write ${rc/#${HOME}/~}. Add this line to your shell's startup file yourself: ${CYAN}${line}${RESET}" fi else local PATH_NOTE="${BIN} is not on your PATH. Add: ${CYAN}${line}${RESET}" fi fi # 7. The brain: created on first run, never replaced (it lives outside lib/) local BRAIN STATS STATS=$("${C9}" stats 2>&1) || fail "the brain couldn't be opened:\n ${STATS}" BRAIN=$(printf '%s\n' "${STATS}" | sed -n 's/.*Database file: *//p') [ -n "${BRAIN}" ] || fail "the brain couldn't be opened:\n ${STATS}" ok "Brain ${DIM}${BRAIN}${RESET}" # 8. Connect agents: the interactive wizard when there's a terminal, otherwise say how echo if [ "${SETUP}" -eq 1 ] && [ "${TTY}" -eq 1 ]; then "${C9}" setup --from-installer