#!/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-ai/nora/blob/main/LICENSE set -e KERNORA_HOME="$HOME/.kernora" KERNORA_APP="$KERNORA_HOME/app" # B-2 fix (launch-loop L1, 2026-07-19): the git clone/update target MUST be # distinct from KERNORA_APP. The inner install.sh (run from this clone) does # `cp "$REPO_DIR/$f" "$KERNORA_DIR/app/$f"` for every APP_FILE — if REPO_DIR # (this clone) and KERNORA_DIR/app were the SAME path, that cp sees identical # source/dest, BSD cp exits 1 ("are identical (not copied)"), and `set -e` # aborts the whole install. REPO_STAGING is the clone; KERNORA_APP remains # the copy destination the inner install.sh writes into. REPO_STAGING="$KERNORA_HOME/.repo" REPO="https://github.com/kernora-ai/nora.git" # ── Pinned release (supply-chain: this served script is TLS-trusted; the cloned # repo is NOT trusted until its HEAD matches NORA_PIN_SHA — see the fail-CLOSED # verify block just before `bash install.sh`). A push to nora:main can no longer # reach users; only a cut release + a pin bump here (then redeploy) does. # Bump BOTH on every release (see nora-publish-website skill). SHA is 40-hex. NORA_PIN_TAG="v2.8.1" NORA_PIN_SHA="e59b043090c2bd959ef30bd6683f6114e14c35ad" 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. Clone or update ──────────────────────────────────────────────────────── # NOTE (H-4, launch-loop L1): we deliberately do NOT stop an existing running # Kernora here. Stopping before the new install is proven leaves a user's # working install dead if anything below fails (deps, clone, copy) — and # under `set -e` that failure is the guaranteed common case on a machine that # already has Claude Code / Kiro installed (see B-1..B-3). The inner # install.sh (step below) stops old processes at the correct point: AFTER # files are copied successfully, immediately before starting the new ones. # Update (D4/M1): a --depth 1 clone cannot --ff-only across to a new tag (no # common ancestor), so "update" = re-clone the pinned tag when the local HEAD # doesn't already match the pin. If it already matches, no work. The D2 verify # block below is the gate either way. NOTE: clone failure is NOT masked (H2) — # no `| tail -1`, so a failed --branch clone aborts under `set -e` on its own. if [ -d "$REPO_STAGING/.git" ] && \ [ "$(git -C "$REPO_STAGING" rev-parse HEAD 2>/dev/null)" = "$NORA_PIN_SHA" ]; then info "Already at pinned release $NORA_PIN_TAG — no download needed." else info "Downloading Kernora $NORA_PIN_TAG..." rm -rf "$REPO_STAGING" git clone --depth 1 --branch "$NORA_PIN_TAG" "$REPO" "$REPO_STAGING" \ || fail "Could not download release $NORA_PIN_TAG. Your existing install is untouched. Check your connection and re-run: curl -fsSL https://kernora.ai/install | bash" ok "Downloaded $NORA_PIN_TAG to ~/.kernora/.repo/" 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 ──────────────────────────────────────────────── # Runs from REPO_STAGING (the clone), NOT KERNORA_APP — see B-2 note above. # The inner install.sh resolves REPO_DIR to its own location (REPO_STAGING) # and copies APP_FILES from there into KERNORA_APP. info "Running setup..." cd "$REPO_STAGING" # ── Integrity gate (fail CLOSED) ───────────────────────────────────────────── # The cloned repo is untrusted code. Verify its HEAD is EXACTLY the pinned # release before running any of it. Three sub-conditions (Stage-5 PE H1): # (1) this runs before the ONLY path to `bash install.sh`; # (2) the rev-parse error is never suppressed; # (3) the pin is validated non-empty AND 40-hex, and an empty HEAD is rejected. # Developer escape hatch: NORA_ALLOW_UNVERIFIED=1 skips the check (off by # default) for a knowingly-diverged ~/.kernora/.repo working tree. if [ "$NORA_ALLOW_UNVERIFIED" = "1" ]; then warn "NORA_ALLOW_UNVERIFIED=1 — integrity check DISABLED. Untrusted code from $REPO_STAGING will run. Do this only on a repo you control." else case "$NORA_PIN_SHA" in *[!0-9a-f]*|"") fail "Installer misconfigured (bad pin fingerprint). Aborting; nothing was installed." ;; esac [ "${#NORA_PIN_SHA}" -eq 40 ] || fail "Installer misconfigured (pin length). Aborting; nothing was installed." GOT="$(git -C "$REPO_STAGING" rev-parse HEAD)" \ || fail "Cannot read the downloaded release. Aborting; nothing was installed." if [ -z "$GOT" ] || [ "$GOT" != "$NORA_PIN_SHA" ]; then fail "Release fingerprint mismatch — expected $NORA_PIN_SHA, got ${GOT:-}. Your existing install is untouched. Re-run: curl -fsSL https://kernora.ai/install | bash" fi ok "Verified release $NORA_PIN_TAG (${NORA_PIN_SHA:0:12}…)" fi 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 ────────────────────────────────────────── # B-4-class fix: the real CLI entry point is kernora_cli.py (copied by the # inner install.sh's APP_FILES loop) — there has never been a root-level # cli.py in this repo. The wrapper previously pointed at a file that does # not exist, so every outro command (kernora status/generate/roi/...) was # ENOENT-dead even on a fully successful install. KERNORA_BIN="$KERNORA_APP/kernora_cli.py" WRAPPER="$KERNORA_HOME/kernora" if [ ! -f "$KERNORA_BIN" ]; then warn "kernora_cli.py missing from ~/.kernora/app — CLI wrapper not created (MCP + dashboard still work)" else cat > "$WRAPPER" << SCRIPT #!/bin/bash # Kernora CLI wrapper — auto-generated by get-kernora.sh exec "$PYTHON" "$KERNORA_BIN" "\$@" 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_BIN" "\$@" 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 fi # end: KERNORA_BIN existence guard # ── 10. Open dashboard ────────────────────────────────────────────────────── # H-3 fix: verify the response is actually Kernora, not just "something is # listening on :2742" — an unrelated pre-existing server on that port would # otherwise make this open the wrong page and print false success below. sleep 1 if curl -sfL http://localhost:2742/ 2>/dev/null | grep -qi kernora; 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 │" if [ -f "$KERNORA_BIN" ]; then echo " │ CLI → nora --help (or kernora --help) │" fi echo " │ Config → ~/.kernora/config.toml │" echo " │ Update → curl -fsSL https://kernora.ai/install | bash │" echo " │ Uninstall → bash ~/.kernora/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 "" # Honesty: only advertise tools/commands this Free install actually shipped. if [ -f "$KERNORA_BIN" ]; then echo " CLI commands (use kernora or nora — same thing):" echo " kernora help Full command list" echo " kernora tour Interactive 9-step walkthrough" echo " kernora generate Emit CLAUDE.md / .cursorrules / steering files" echo " kernora network-check AST audit — verify zero-network hot path" echo " kernora status Daemon + dashboard + DB health" echo "" else echo " CLI: not in this Free build — use the dashboard + MCP tools below." echo "" fi echo " In Claude Code / Cursor / Kiro chat (Free MCP tools):" echo " nora_search / nora_context_for_task / nora_factbook_view" echo " nora_factbook_add / nora_factbook / nora_generate / nora_help" echo " nora_roi / nora_pe_review / nora_provenance" echo "" # Derive live tool count from installed mcp.json autoApprove when present if [ -f "$HOME/.kiro/settings/mcp.json" ] || [ -f "$HOME/.claude/.mcp.json" ] || [ -f "$HOME/.cursor/mcp.json" ]; then _mcp_n=$("$PYTHON" -c " import json,glob n=0 for p in ['$HOME/.kiro/settings/mcp.json']: try: d=json.load(open(p)); n=len(d.get('mcpServers',{}).get('nora',{}).get('autoApprove') or []) except Exception: pass print(n) " 2>/dev/null || echo 0) if [ "${_mcp_n:-0}" -gt 0 ]; then echo " Free MCP surface: ${_mcp_n} tools (autoApprove matches installed nora_mcp)." echo "" fi fi