#!/usr/bin/env sh # Okibi installer for macOS and Linux. # # curl -fsSL https://okibi.ai/install | sh # # Installs the PUBLIC okibi thin client (an API client for the Okibi # platform — no engine, no keys; see docs/architecture.md). The script # itself and everything it fetches are public: there is no password gate. # # It detects OS/arch, fetches latest.json from the release host, downloads # the matching tarball, SHA-256 verifies it, extracts to # ~/.local/share/okibi/versions//, symlinks okibi (+ okibi-tui) # into ~/.local/bin, installs the matching Agent Skill in ~/.agents/skills, # and links it into detected agents' native skill directories. Idempotent — # re-running upgrades the client and its managed skill to whatever latest.json # now points at. # # Env overrides: # OKIBI_RELEASE_BASE Release-host base URL (default baked in at serve time). # OKIBI_INSTALL_ROOT Install dir (default ~/.local/share/okibi). # OKIBI_BIN_DIR Symlink dir (default ~/.local/bin). # OKIBI_SKILLS_DIR Agent Skills dir (default ~/.agents/skills). set -eu # https://releases.okibi.ai is substituted by the /install route with the # deployment's configured release base; the fallback keeps a hand-piped # copy of this script working. DEFAULT_RELEASE_BASE='https://releases.okibi.ai' RELEASE_BASE="${OKIBI_RELEASE_BASE:-$DEFAULT_RELEASE_BASE}" INSTALL_ROOT="${OKIBI_INSTALL_ROOT:-$HOME/.local/share/okibi}" BIN_DIR="${OKIBI_BIN_DIR:-$HOME/.local/bin}" SKILLS_DIR="${OKIBI_SKILLS_DIR:-$HOME/.agents/skills}" err() { printf 'install: %s\n' "$*" >&2; exit 1; } info() { printf 'install: %s\n' "$*"; } link_detected_agent_skill() { agent_name="$1" agent_home="$2" [ -d "$agent_home" ] || return 0 agent_skills="$agent_home/skills" agent_dest="$agent_skills/$SKILL_NAME" [ "$agent_dest" = "$SKILL_DEST" ] && return 0 if [ -L "$agent_dest" ]; then agent_target="$(readlink "$agent_dest" 2>/dev/null || true)" [ "$agent_target" != "$SKILL_DEST" ] || return 0 fi if [ -f "$agent_dest/.okibi-managed" ] \ && [ "$(cat "$agent_dest/.okibi-managed")" = 'okibi' ]; then rm -rf "$agent_dest" elif [ -e "$agent_dest" ] || [ -L "$agent_dest" ]; then info "NOTE: $agent_name skill not linked because $agent_dest already exists and is not managed by okibi" return 0 fi mkdir -p "$agent_skills" ln -s "$SKILL_DEST" "$agent_dest" info "linked Agent Skill for $agent_name at $agent_dest" } link_detected_agent_skills() { claude_home="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" codex_home="${CODEX_HOME:-$HOME/.codex}" config_home="${XDG_CONFIG_HOME:-$HOME/.config}" link_detected_agent_skill 'Claude Code' "$claude_home" link_detected_agent_skill 'Codex' "$codex_home" link_detected_agent_skill 'Cursor' "$HOME/.cursor" link_detected_agent_skill 'Gemini CLI' "$HOME/.gemini" link_detected_agent_skill 'GitHub Copilot' "$HOME/.copilot" link_detected_agent_skill 'Windsurf' "$HOME/.codeium/windsurf" link_detected_agent_skill 'OpenCode' "$config_home/opencode" } command -v curl >/dev/null 2>&1 || err 'curl is required' command -v tar >/dev/null 2>&1 || err 'tar is required' detect_platform() { os='' arch='' case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; *) err "unsupported OS: $(uname -s) (okibi ships macOS and Linux builds)" ;; esac case "$(uname -m)" in arm64 | aarch64) arch=arm64 ;; x86_64 | amd64) arch=x64 ;; *) err "unsupported architecture: $(uname -m)" ;; esac printf '%s-%s' "$os" "$arch" } # Bare-bones JSON extraction — avoids requiring jq. Pulls `field` from the # artifact object whose "platform" matches. Fragile by design: latest.json # is a release-CI artifact with a fixed shape. extract_json_field() { platform="$1" field="$2" file="$3" tr -d '\n' <"$file" \ | grep -o "{[^{}]*\"platform\"[[:space:]]*:[[:space:]]*\"$platform\"[^{}]*}" \ | grep -o "\"$field\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \ | head -1 \ | sed -E "s/.*\"$field\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\\1/" } verify_sha256() { file="$1" expected="$2" if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$file" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "$file" | awk '{print $1}')" else err 'no sha256sum or shasum available to verify the download' fi [ "$actual" = "$expected" ] \ || err "checksum mismatch: expected $expected, got $actual" } PLATFORM="$(detect_platform)" info "platform: $PLATFORM" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT MANIFEST="$TMP/latest.json" info "fetching $RELEASE_BASE/latest.json" curl -fsSL "$RELEASE_BASE/latest.json" -o "$MANIFEST" \ || err "could not reach the release host at $RELEASE_BASE" VERSION="$(tr -d '\n' <"$MANIFEST" \ | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' \ | head -1 | sed -E 's/.*"([^"]+)"[[:space:]]*$/\1/')" [ -n "$VERSION" ] || err 'latest.json is missing a version field' # The version becomes a path segment that is rm -rf'd below, so a hostile or # corrupt manifest must not be able to smuggle in path separators or "..". case "$VERSION" in *[!A-Za-z0-9._+-]* | *..*) err "latest.json has an unsafe version string: $VERSION" ;; esac URL="$(extract_json_field "$PLATFORM" url "$MANIFEST")" SHA="$(extract_json_field "$PLATFORM" sha256 "$MANIFEST")" [ -n "$URL" ] || err "no okibi build published for $PLATFORM yet" [ -n "$SHA" ] || err "latest.json has no checksum for $PLATFORM" SKILL_NAME='okibi' SKILL_DEST="$SKILLS_DIR/$SKILL_NAME" SKILL_DEST_MANAGED=0 if [ -f "$SKILL_DEST/.okibi-managed" ] \ && [ "$(cat "$SKILL_DEST/.okibi-managed")" = 'okibi' ]; then SKILL_DEST_MANAGED=1 elif [ -L "$SKILL_DEST" ]; then existing_target="$(readlink "$SKILL_DEST" 2>/dev/null || true)" case "$existing_target" in "$INSTALL_ROOT"/versions/*/skills/okibi) SKILL_DEST_MANAGED=1 ;; esac fi # Relative URLs resolve against the release base. case "$URL" in http://* | https://*) ;; *) URL="$RELEASE_BASE/$URL" ;; esac TARBALL="$TMP/okibi.tar.gz" info "downloading okibi $VERSION" curl -fsSL "$URL" -o "$TARBALL" || err 'download failed' verify_sha256 "$TARBALL" "$SHA" info 'checksum verified' VERSION_DIR="$INSTALL_ROOT/versions/$VERSION" rm -rf "$VERSION_DIR" mkdir -p "$VERSION_DIR" # The archive wraps everything in one okibi-client--/ dir; # strip it so okibi, okibi-tui, release.json land directly in VERSION_DIR. tar -xzf "$TARBALL" -C "$VERSION_DIR" --strip-components=1 [ -f "$VERSION_DIR/okibi" ] || err 'archive did not contain an okibi binary' chmod +x "$VERSION_DIR/okibi" 2>/dev/null || true [ -f "$VERSION_DIR/okibi-tui" ] && chmod +x "$VERSION_DIR/okibi-tui" 2>/dev/null || true info "installed to $VERSION_DIR" SKILL_SOURCE="$VERSION_DIR/skills/okibi" if [ -f "$SKILL_SOURCE/SKILL.md" ]; then grep -Eq '^name:[[:space:]]*okibi[[:space:]]*$' "$SKILL_SOURCE/SKILL.md" \ || err 'bundled SKILL.md name does not match okibi' [ -f "$SKILL_SOURCE/.okibi-managed" ] \ && [ "$(cat "$SKILL_SOURCE/.okibi-managed")" = 'okibi' ] \ || err 'bundled skill is missing its Okibi ownership marker' mkdir -p "$SKILLS_DIR" if [ "$SKILL_DEST_MANAGED" -eq 1 ]; then rm -rf "$SKILL_DEST" fi if [ -e "$SKILL_DEST" ] || [ -L "$SKILL_DEST" ]; then info "NOTE: skill not installed because $SKILL_DEST already exists and is not Okibi-managed" else SKILL_SOURCE="$(cd "$SKILL_SOURCE" && pwd)" ln -s "$SKILL_SOURCE" "$SKILL_DEST" info "installed Agent Skill to $SKILL_DEST" link_detected_agent_skills fi else info 'NOTE: this older release has no bundled Agent Skill' fi mkdir -p "$BIN_DIR" ln -sfn "$VERSION_DIR/okibi" "$BIN_DIR/okibi" info "symlinked $BIN_DIR/okibi" # okibi-tui is the interactive frontend; okibi finds it via OKIBI_TUI_BIN or # a sibling on PATH, so keep it alongside. if [ -f "$VERSION_DIR/okibi-tui" ]; then ln -sfn "$VERSION_DIR/okibi-tui" "$BIN_DIR/okibi-tui" fi # Record the host so re-running the installer (today's upgrade path) and a # future `okibi upgrade` both know where to re-fetch. Unlike an auth-gated # host, nothing secret is stored — the release host is public. CONFIG_DIR="$HOME/.okibi" mkdir -p "$CONFIG_DIR" printf '%s\n' "$RELEASE_BASE" >"$CONFIG_DIR/installed-from" case ":$PATH:" in *":$BIN_DIR:"*) ;; *) info '' info "NOTE: $BIN_DIR is not on your PATH. Add this to your shell profile:" info '' info " export PATH=\"$BIN_DIR:\$PATH\"" info '' ;; esac # Post-install tail. Both a human in a terminal and an agent running this # read these lines, so the second path is discrete commands stated # neutrally — no audience labels, and no assumption of an existing API # key (`okibi auth login` works for an agent too: it prints the browser # URL and a human can finish the login). Keep every command real # (verify against `okibi --help`). info "done: okibi $VERSION is installed" info '' info 'run `okibi` to get started — it logs you in, gives you a quick tour,' info 'and walks your first CLI from repo to install link.' info '' info 'or step by step: `okibi auth login` to log in, then `okibi generate --help`.'