#!/bin/bash
# Kernora — One-click installer for Mac (and Linux)
# Usage: curl -fsSL https://kernora.ai/install | bash
#
# Elastic License 2.0 — commercial use requires agreement with kernora.ai
# https://github.com/kernora/kernora/blob/main/LICENSE
set -e
KERNORA_HOME="$HOME/.kernora"
KERNORA_APP="$KERNORA_HOME/app"
REPO="https://github.com/kernora/kernora.git"
BIN_DIR="/usr/local/bin"
# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'
BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $1"; }
info() { echo -e "${CYAN}→${NC} $1"; }
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
fail() { echo -e "${RED}✗${NC} $1"; exit 1; }
echo ""
echo -e "${CYAN}◎ Kernora${NC} — meet Nora, your AI work intelligence colleague"
echo " One-click setup: clone → configure → hook → dashboard → done."
echo ""
# ── 1. OS check ──────────────────────────────────────────────────────────────
OS="$(uname -s)"
if [ "$OS" != "Darwin" ] && [ "$OS" != "Linux" ]; then
fail "Unsupported OS: $OS. Kernora supports macOS and Linux."
fi
# ── 2. Python 3.9+ ──────────────────────────────────────────────────────────
PYTHON=""
for candidate in python3 /opt/homebrew/bin/python3 /usr/local/bin/python3 \
python3.14 python3.13 python3.12 python3.11 python3.10 python3.9; do
if command -v "$candidate" &>/dev/null; then
if "$candidate" -c "import sys; assert sys.version_info >= (3,9)" 2>/dev/null; then
PYTHON="$(command -v "$candidate")"
break
fi
fi
done
if [ -z "$PYTHON" ]; then
fail "Python 3.9+ required. Install from https://python.org or: brew install python3"
fi
ok "Python $($PYTHON --version | cut -d' ' -f2) → $PYTHON"
# ── 3. Git check ────────────────────────────────────────────────────────────
if ! command -v git &>/dev/null; then
fail "git is required. Install from https://git-scm.com or: xcode-select --install"
fi
# ── 4. Stop existing Kernora if running ──────────────────────────────────────
if pgrep -f "kernora.*daemon" &>/dev/null || pgrep -f "kernora.*dashboard" &>/dev/null; then
info "Stopping existing Kernora..."
if [ "$(uname)" = "Darwin" ]; then
launchctl unload "$HOME/Library/LaunchAgents/ai.kernora.daemon.plist" 2>/dev/null || true
launchctl unload "$HOME/Library/LaunchAgents/ai.kernora.dashboard.plist" 2>/dev/null || true
fi
pkill -f "kernora.*daemon" 2>/dev/null || true
pkill -f "kernora.*dashboard" 2>/dev/null || true
sleep 1
ok "Stopped existing processes"
fi
# ── 5. Clone or update ──────────────────────────────────────────────────────
if [ -d "$KERNORA_APP/.git" ]; then
info "Updating existing install..."
cd "$KERNORA_APP"
git pull --ff-only origin main 2>/dev/null || git pull origin main
ok "Updated to latest"
else
info "Downloading Kernora..."
rm -rf "$KERNORA_APP"
git clone --depth 1 "$REPO" "$KERNORA_APP" 2>&1 | tail -1
ok "Downloaded to ~/.kernora/app/"
fi
# ══════════════════════════════════════════════════════════════════════════════
# ── 6. Interactive Setup Wizard ──────────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════
#
# Only runs on FRESH install (no existing config.toml).
# Re-installs (updates) skip the wizard — user already configured.
# To re-run the wizard: rm ~/.kernora/config.toml && re-run installer.
mkdir -p "$KERNORA_HOME"
if [ ! -f "$KERNORA_HOME/config.toml" ]; then
echo ""
echo -e " ┌──────────────────────────────────────────────────────────────────┐"
echo -e " │ ${BOLD}Setup Wizard${NC} │"
echo -e " │ ${DIM}3 questions. Takes 30 seconds. Skip any with Enter.${NC} │"
echo -e " └──────────────────────────────────────────────────────────────────┘"
# ── Q1: LLM Provider ─────────────────────────────────────────────────────
echo ""
echo -e " ${BOLD}1. Which LLM should analyze your coding sessions?${NC}"
echo ""
echo -e " ${GREEN}1${NC}) Anthropic (Claude) ${DIM}— best quality, needs API key${NC}"
echo -e " ${GREEN}2${NC}) Google (Gemini) ${DIM}— great quality, needs API key${NC}"
echo -e " ${GREEN}3${NC}) OpenAI (GPT-4o) ${DIM}— good quality, needs API key${NC}"
echo -e " ${GREEN}4${NC}) xAI (Grok) ${DIM}— good quality, needs API key${NC}"
echo -e " ${GREEN}5${NC}) Ollama (local) ${DIM}— free, runs on your Mac, no key${NC}"
echo -e " ${GREEN}6${NC}) Auto-detect ${DIM}— uses best available from your keys (recommended)${NC}"
echo -e " ${GREEN}7${NC}) Skip for now ${DIM}— configure later in ~/.kernora/config.toml${NC}"
echo ""
echo -ne " Choice [6]: "
read -r LLM_CHOICE
LLM_CHOICE="${LLM_CHOICE:-6}"
# Map choice to provider
PROVIDER="auto"
API_KEY_VAR=""
API_KEY_VAL=""
case "$LLM_CHOICE" in
1) PROVIDER="anthropic"; API_KEY_VAR="anthropic" ;;
2) PROVIDER="google"; API_KEY_VAR="gemini" ;;
3) PROVIDER="openai"; API_KEY_VAR="openai" ;;
4) PROVIDER="grok"; API_KEY_VAR="xai" ;;
5) PROVIDER="ollama" ;;
6) PROVIDER="auto" ;;
7) PROVIDER="auto" ;;
*) PROVIDER="auto" ;;
esac
# ── Q1b: API Key (if provider needs one) ─────────────────────────────────
if [ -n "$API_KEY_VAR" ]; then
# Check if key already exists in environment
case "$API_KEY_VAR" in
anthropic) EXISTING_KEY="${ANTHROPIC_API_KEY:-}" ;;
gemini) EXISTING_KEY="${GEMINI_API_KEY:-}" ;;
openai) EXISTING_KEY="${OPENAI_API_KEY:-}" ;;
xai) EXISTING_KEY="${XAI_API_KEY:-}" ;;
esac
if [ -n "$EXISTING_KEY" ]; then
MASKED="${EXISTING_KEY:0:8}...${EXISTING_KEY: -4}"
echo -e " ${GREEN}✓${NC} Found ${API_KEY_VAR} key in environment: ${DIM}${MASKED}${NC}"
API_KEY_VAL="$EXISTING_KEY"
else
echo ""
echo -ne " Paste your API key ${DIM}(hidden, Enter to skip)${NC}: "
read -r -s API_KEY_VAL
echo ""
if [ -n "$API_KEY_VAL" ]; then
MASKED="${API_KEY_VAL:0:8}...${API_KEY_VAL: -4}"
ok "Key saved: ${DIM}${MASKED}${NC}"
else
warn "No key provided — you can add it later to ~/.kernora/config.toml"
fi
fi
elif [ "$PROVIDER" = "auto" ] && [ "$LLM_CHOICE" != "7" ]; then
# Auto-detect: check what keys are already in env
FOUND_KEYS=0
[ -n "${ANTHROPIC_API_KEY:-}" ] && FOUND_KEYS=$((FOUND_KEYS + 1)) && ok "Found Anthropic key in environment"
[ -n "${GEMINI_API_KEY:-}" ] && FOUND_KEYS=$((FOUND_KEYS + 1)) && ok "Found Gemini key in environment"
[ -n "${OPENAI_API_KEY:-}" ] && FOUND_KEYS=$((FOUND_KEYS + 1)) && ok "Found OpenAI key in environment"
[ -n "${XAI_API_KEY:-}" ] && FOUND_KEYS=$((FOUND_KEYS + 1)) && ok "Found xAI key in environment"
if [ "$FOUND_KEYS" -eq 0 ]; then
echo ""
echo -e " ${DIM}No API keys found in environment. Paste one now or add later.${NC}"
echo -e " ${DIM}Anthropic recommended for best results.${NC}"
echo ""
echo -ne " Paste any API key ${DIM}(hidden, Enter to skip)${NC}: "
read -r -s API_KEY_VAL
echo ""
if [ -n "$API_KEY_VAL" ]; then
# Auto-detect key type from prefix
case "$API_KEY_VAL" in
sk-ant-*) API_KEY_VAR="anthropic"; ok "Detected Anthropic key" ;;
sk-*) API_KEY_VAR="openai"; ok "Detected OpenAI key" ;;
AI*) API_KEY_VAR="gemini"; ok "Detected Gemini key" ;;
xai-*) API_KEY_VAR="xai"; ok "Detected xAI key" ;;
*) API_KEY_VAR="anthropic"
warn "Couldn't detect key type — saving as Anthropic" ;;
esac
fi
else
ok "Auto-detect will use best available from ${FOUND_KEYS} key(s)"
fi
elif [ "$PROVIDER" = "ollama" ]; then
# Check if Ollama is running
if curl -sf http://localhost:11434/api/version &>/dev/null; then
ok "Ollama is running"
else
warn "Ollama not detected at localhost:11434"
echo -e " ${DIM}Install: https://ollama.ai → then: ollama pull llama3.2:8b${NC}"
fi
fi
# ── Q2: Knowledge Backup ─────────────────────────────────────────────────
echo ""
echo -e " ${BOLD}2. Back up your knowledge base?${NC}"
echo -e " ${DIM}Your knowledge DB (~/.kernora/echo.db) lives locally by default.${NC}"
echo -e " ${DIM}Pick a sync folder and your existing cloud service handles the rest.${NC}"
echo ""
if [ "$OS" = "Darwin" ]; then
ICLOUD_DIR="$HOME/Library/Mobile Documents/com~apple~CloudDocs"
if [ -d "$ICLOUD_DIR" ]; then
echo -e " ${GREEN}1${NC}) iCloud Drive ${DIM}→ syncs across your Apple devices${NC}"
else
echo -e " ${DIM}1) iCloud Drive (not detected)${NC}"
fi
DROPBOX_DIR="$HOME/Dropbox"
if [ -d "$DROPBOX_DIR" ]; then
echo -e " ${GREEN}2${NC}) Dropbox ${DIM}→ syncs across all your devices${NC}"
else
echo -e " ${DIM}2) Dropbox (not detected)${NC}"
fi
else
echo -e " ${DIM}1) iCloud Drive (macOS only)${NC}"
echo -e " ${DIM}2) Dropbox (not detected)${NC}"
fi
echo -e " ${GREEN}3${NC}) Custom path ${DIM}→ you pick the folder${NC}"
echo -e " ${GREEN}4${NC}) No backup ${DIM}→ local only (default)${NC}"
echo ""
echo -ne " Choice [4]: "
read -r BACKUP_CHOICE
BACKUP_CHOICE="${BACKUP_CHOICE:-4}"
BACKUP_PATH=""
case "$BACKUP_CHOICE" in
1)
if [ "$OS" = "Darwin" ] && [ -d "$ICLOUD_DIR" ]; then
BACKUP_PATH="$ICLOUD_DIR/Kernora"
mkdir -p "$BACKUP_PATH"
ok "Backup: iCloud Drive → $BACKUP_PATH"
else
warn "iCloud Drive not available — skipping backup"
fi
;;
2)
if [ -d "$DROPBOX_DIR" ]; then
BACKUP_PATH="$DROPBOX_DIR/Kernora"
mkdir -p "$BACKUP_PATH"
ok "Backup: Dropbox → $BACKUP_PATH"
else
warn "Dropbox not found — skipping backup"
fi
;;
3)
echo -ne " Enter folder path: "
read -r CUSTOM_PATH
if [ -n "$CUSTOM_PATH" ]; then
# Expand ~ if present
CUSTOM_PATH="${CUSTOM_PATH/#\~/$HOME}"
mkdir -p "$CUSTOM_PATH" 2>/dev/null
if [ -d "$CUSTOM_PATH" ]; then
BACKUP_PATH="$CUSTOM_PATH"
ok "Backup: $BACKUP_PATH"
else
warn "Could not create $CUSTOM_PATH — skipping backup"
fi
fi
;;
4|"")
info "No backup — local only"
;;
esac
# ── Q3: Team Sync (S3) ───────────────────────────────────────────────────
echo ""
echo -e " ${BOLD}3. Team knowledge sync?${NC}"
echo -e " ${DIM}Share patterns and decisions across your team via S3.${NC}"
echo -e " ${DIM}Each team member runs Kernora locally; S3 is the shared brain.${NC}"
echo ""
echo -e " ${GREEN}1${NC}) Connect S3 bucket ${DIM}→ team sync (needs AWS credentials)${NC}"
echo -e " ${GREEN}2${NC}) Solo mode ${DIM}→ just me, no sync (default)${NC}"
echo ""
echo -ne " Choice [2]: "
read -r TEAM_CHOICE
TEAM_CHOICE="${TEAM_CHOICE:-2}"
S3_BUCKET=""
S3_REGION=""
AWS_ACCESS=""
AWS_SECRET=""
if [ "$TEAM_CHOICE" = "1" ]; then
echo ""
echo -ne " S3 bucket name: "
read -r S3_BUCKET
echo -ne " AWS region [us-east-1]: "
read -r S3_REGION
S3_REGION="${S3_REGION:-us-east-1}"
# Check for AWS credentials in env or ~/.aws
if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then
ok "Found AWS credentials in environment"
AWS_ACCESS="${AWS_ACCESS_KEY_ID}"
AWS_SECRET="${AWS_SECRET_ACCESS_KEY}"
elif [ -f "$HOME/.aws/credentials" ]; then
ok "Found AWS credentials at ~/.aws/credentials"
info "Kernora will use your default AWS profile"
else
echo ""
echo -ne " AWS Access Key ID: "
read -r AWS_ACCESS
echo -ne " AWS Secret Access Key ${DIM}(hidden)${NC}: "
read -r -s AWS_SECRET
echo ""
fi
if [ -n "$S3_BUCKET" ]; then
ok "Team sync: s3://$S3_BUCKET ($S3_REGION)"
else
warn "No bucket provided — skipping team sync"
TEAM_CHOICE="2"
fi
fi
# ── Write config.toml ────────────────────────────────────────────────────
echo ""
info "Writing configuration..."
cat > "$KERNORA_HOME/config.toml" << TOML
# Kernora — AI Work Intelligence
# Generated by get-kernora.sh on $(date '+%Y-%m-%d %H:%M')
# Edit anytime: ~/.kernora/config.toml
[mode]
type = "byok"
[keys]
# API keys for LLM analysis. More keys = better model selection.
TOML
# Write the key that was provided
if [ -n "$API_KEY_VAL" ] && [ -n "$API_KEY_VAR" ]; then
echo "$API_KEY_VAR = \"$API_KEY_VAL\"" >> "$KERNORA_HOME/config.toml"
fi
# Also capture any env keys the user already has
[ -n "${ANTHROPIC_API_KEY:-}" ] && [ "$API_KEY_VAR" != "anthropic" ] && \
echo "anthropic = \"${ANTHROPIC_API_KEY}\"" >> "$KERNORA_HOME/config.toml"
[ -n "${GEMINI_API_KEY:-}" ] && [ "$API_KEY_VAR" != "gemini" ] && \
echo "gemini = \"${GEMINI_API_KEY}\"" >> "$KERNORA_HOME/config.toml"
[ -n "${OPENAI_API_KEY:-}" ] && [ "$API_KEY_VAR" != "openai" ] && \
echo "openai = \"${OPENAI_API_KEY}\"" >> "$KERNORA_HOME/config.toml"
[ -n "${XAI_API_KEY:-}" ] && [ "$API_KEY_VAR" != "xai" ] && \
echo "xai = \"${XAI_API_KEY}\"" >> "$KERNORA_HOME/config.toml"
cat >> "$KERNORA_HOME/config.toml" << TOML
[model]
provider = "$PROVIDER"
[analysis]
run_every_minutes = 60
[dashboard]
port = 2742
auto_open = true
[privacy]
verified = true
TOML
# Backup config
if [ -n "$BACKUP_PATH" ]; then
cat >> "$KERNORA_HOME/config.toml" << TOML
[backup]
enabled = true
path = "$BACKUP_PATH"
# Kernora copies echo.db here after every analysis run.
# Your cloud service (iCloud/Dropbox/etc.) handles the sync.
TOML
fi
# Team/S3 config
if [ "$TEAM_CHOICE" = "1" ] && [ -n "$S3_BUCKET" ]; then
cat >> "$KERNORA_HOME/config.toml" << TOML
[swarm]
type = "byok_s3"
bucket = "$S3_BUCKET"
region = "$S3_REGION"
[aws]
access_key = "$AWS_ACCESS"
secret_key = "$AWS_SECRET"
TOML
fi
ok "Config written to ~/.kernora/config.toml"
echo ""
echo -e " ┌──────────────────────────────────────────────────────────────────┐"
echo -e " │ ${GREEN}Setup complete!${NC} Installing Kernora... │"
echo -e " └──────────────────────────────────────────────────────────────────┘"
echo ""
else
info "Config exists — keeping ~/.kernora/config.toml (delete it to re-run wizard)"
fi
# ── 7. Run the real installer ────────────────────────────────────────────────
info "Running setup..."
cd "$KERNORA_APP"
bash install.sh
# ── 8. Set up backup cron (if backup path configured) ────────────────────────
if [ -n "$BACKUP_PATH" ] && [ -d "$BACKUP_PATH" ]; then
# Create a backup script
cat > "$KERNORA_HOME/backup.sh" << BSCRIPT
#!/bin/bash
# Kernora knowledge backup — copies echo.db to sync folder
SRC="$KERNORA_HOME/echo.db"
DST="$BACKUP_PATH/echo.db"
if [ -f "\$SRC" ]; then
cp "\$SRC" "\$DST"
fi
BSCRIPT
chmod +x "$KERNORA_HOME/backup.sh"
# On macOS, add a LaunchAgent for hourly backup
if [ "$OS" = "Darwin" ]; then
cat > "$HOME/Library/LaunchAgents/ai.kernora.backup.plist" << PLIST
Labelai.kernora.backup
ProgramArguments
/bin/bash
$KERNORA_HOME/backup.sh
StartInterval3600
RunAtLoad
PLIST
launchctl load "$HOME/Library/LaunchAgents/ai.kernora.backup.plist" 2>/dev/null || true
ok "Hourly backup to $BACKUP_PATH"
fi
fi
# ── 9. Create `kernora` CLI command ──────────────────────────────────────────
KERNORA_BIN="$KERNORA_APP/cli.py"
WRAPPER="$KERNORA_HOME/kernora"
cat > "$WRAPPER" << SCRIPT
#!/bin/bash
# Kernora CLI wrapper — auto-generated by get-kernora.sh
exec "$PYTHON" "$KERNORA_APP/cli.py" "\$@"
SCRIPT
chmod +x "$WRAPPER"
# Also create `nora` alias — shorter, friendlier, same thing
NORA_WRAPPER="$KERNORA_HOME/nora"
cat > "$NORA_WRAPPER" << SCRIPT
#!/bin/bash
# Nora CLI — alias for kernora. Same thing, fewer keystrokes.
exec "$PYTHON" "$KERNORA_APP/cli.py" "\$@"
SCRIPT
chmod +x "$NORA_WRAPPER"
# Symlink both `kernora` and `nora` into PATH
if [ -w "$BIN_DIR" ]; then
ln -sf "$WRAPPER" "$BIN_DIR/kernora"
ln -sf "$NORA_WRAPPER" "$BIN_DIR/nora"
ok "CLI available: kernora (or just nora)"
elif sudo ln -sf "$WRAPPER" "$BIN_DIR/kernora" 2>/dev/null && \
sudo ln -sf "$NORA_WRAPPER" "$BIN_DIR/nora" 2>/dev/null; then
ok "CLI available: kernora / nora (used sudo for /usr/local/bin)"
else
warn "Could not symlink to $BIN_DIR. Add this to your PATH:"
echo " export PATH=\"\$HOME/.kernora:\$PATH\""
# Add to shell profile as fallback
SHELL_RC=""
[ -f "$HOME/.zshrc" ] && SHELL_RC="$HOME/.zshrc"
[ -f "$HOME/.bashrc" ] && SHELL_RC="$HOME/.bashrc"
if [ -n "$SHELL_RC" ] && ! grep -q 'kernora' "$SHELL_RC" 2>/dev/null; then
echo 'export PATH="$HOME/.kernora:$PATH"' >> "$SHELL_RC"
ok "Added to $SHELL_RC — restart your terminal or: source $SHELL_RC"
fi
fi
# ── 10. Open dashboard ──────────────────────────────────────────────────────
sleep 1
if curl -sf http://localhost:2742 > /dev/null 2>&1; then
if [ "$OS" = "Darwin" ]; then
open "http://localhost:2742" 2>/dev/null || true
fi
fi
# ── Done ─────────────────────────────────────────────────────────────────────
echo ""
echo -e " ┌─ ${GREEN}Nora is ready${NC} ───────────────────────────────────────────────┐"
echo " │ │"
echo " │ Dashboard → http://localhost:2742 │"
echo " │ CLI → nora --help (or kernora --help) │"
echo " │ Config → ~/.kernora/config.toml │"
echo " │ Update → curl -fsSL https://kernora.ai/install | bash │"
echo " │ Uninstall → bash ~/.kernora/app/uninstall.sh │"
echo " │ │"
echo " └───────────────────────────────────────────────────────────────────┘"
echo ""
echo -e " ${CYAN}Quick start — just use Claude normally. Nora watches silently:${NC}"
echo ""
echo " Claude Code: claude → code → /exit → Nora captures the session"
echo " Cowork: Automatic. Check localhost:2742 for Nora's learnings."
echo ""
echo " CLI commands (use nora or kernora — same thing):"
echo " nora status Daemon status, DB size, session counts"
echo " nora kiq Knowledge Intelligence Quotient score"
echo " nora search Search patterns, decisions, bugs"
echo " nora bugs Top bugs by recurrence"
echo " nora patterns Top patterns by effectiveness"
echo " nora decisions Recent architectural decisions"
echo " nora savings Token savings from memory injection"
echo ""