Last active
March 21, 2026 02:48
-
-
Save markusstrasser/5dfa86257e875c5906ec27526c1c3cd3 to your computer and use it in GitHub Desktop.
AI coding setup — Claude Code, Gemini, Codex, llmx, emb, MCP servers, skills, daily sync + API keys in macOS Keychain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| AI coding setup — Claude Code, Gemini, Codex, llmx, emb, MCP servers, skills, daily sync + API keys in macOS Keychain | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ============================================================================ | |
| # Claude Code + AI Tools + MCP Infrastructure Setup | |
| # Run: curl -sL <gist-url>/setup-friend.sh | bash | |
| # ============================================================================ | |
| BOLD='\033[1m' | |
| DIM='\033[2m' | |
| GREEN='\033[32m' | |
| YELLOW='\033[33m' | |
| BLUE='\033[34m' | |
| RED='\033[31m' | |
| RESET='\033[0m' | |
| step() { echo -e "\n${BOLD}${BLUE}▸ $1${RESET}"; } | |
| ok() { echo -e " ${GREEN}✓${RESET} $1"; } | |
| skip() { echo -e " ${DIM}⊘ $1${RESET}"; } | |
| warn() { echo -e " ${YELLOW}⚠ $1${RESET}"; } | |
| fail() { echo -e " ${RED}✗ $1${RESET}"; } | |
| PROJECTS_DIR="$HOME/Projects" | |
| # ── Prerequisites ──────────────────────────────────────────── | |
| step "Checking prerequisites" | |
| if ! command -v brew &>/dev/null; then | |
| fail "Homebrew not found. Install: https://brew.sh" | |
| exit 1 | |
| fi | |
| ok "Homebrew" | |
| if ! command -v node &>/dev/null; then | |
| step "Installing Node.js via Homebrew" | |
| brew install node | |
| fi | |
| ok "Node.js $(node --version)" | |
| if ! command -v uv &>/dev/null; then | |
| step "Installing uv" | |
| curl -LsSf https://astral.sh/uv/install.sh | sh | |
| export PATH="$HOME/.local/bin:$PATH" | |
| fi | |
| ok "uv $(uv --version 2>&1 | head -1)" | |
| if ! command -v git &>/dev/null; then | |
| fail "git not found" | |
| exit 1 | |
| fi | |
| ok "git" | |
| if ! command -v just &>/dev/null; then | |
| brew install just | |
| ok "just installed" | |
| else | |
| ok "just" | |
| fi | |
| # ── AI CLI Tools ───────────────────────────────────────────── | |
| step "Installing AI CLI tools" | |
| # Claude Code | |
| if ! command -v claude &>/dev/null; then | |
| npm install -g @anthropic-ai/claude-code | |
| ok "Claude Code installed" | |
| else | |
| ok "Claude Code $(claude --version 2>/dev/null | head -1 || echo 'installed')" | |
| fi | |
| # Gemini CLI | |
| if ! command -v gemini &>/dev/null; then | |
| npm install -g @google/gemini-cli | |
| ok "Gemini CLI installed" | |
| else | |
| ok "Gemini CLI installed" | |
| fi | |
| # Codex CLI (OpenAI) | |
| if ! command -v codex &>/dev/null; then | |
| npm install -g @openai/codex | |
| ok "Codex CLI installed" | |
| else | |
| ok "Codex CLI installed" | |
| fi | |
| # ── Infrastructure Repos ───────────────────────────────────── | |
| step "Cloning infrastructure repos" | |
| mkdir -p "$PROJECTS_DIR" | |
| clone_repo() { | |
| local repo="$1" | |
| local dir="$PROJECTS_DIR/$repo" | |
| if [ -d "$dir/.git" ]; then | |
| ok "$repo (already cloned)" | |
| else | |
| git clone "git@github.com:markusstrasser/${repo}.git" "$dir" | |
| ok "$repo cloned" | |
| fi | |
| } | |
| clone_repo "papers-mcp" # research MCP server (paper search, full-text RAG, evidence prep) | |
| clone_repo "meta" # agent infrastructure, orchestrator, measurement scripts | |
| clone_repo "skills" # shared Claude Code skills and hooks | |
| clone_repo "parsers" # platform export parsers (Twitter, iMessage, Gmail, etc.) | |
| clone_repo "biomedical-mcp" # biomedical API server (genes, drugs, variants, pathways — 80 tools) | |
| clone_repo "llmx" # unified LLM CLI (all providers) | |
| clone_repo "emb" # embed, index, search text corpora | |
| # ── Python Tools (uv) ─────────────────────────────────────── | |
| step "Installing Python tools via uv (editable from local clones)" | |
| for tool in llmx emb parsers; do | |
| dir="$PROJECTS_DIR/$tool" | |
| if [ -d "$dir" ]; then | |
| uv tool install --editable "$dir" 2>/dev/null && ok "$tool (editable)" \ | |
| || ok "$tool (already installed)" | |
| else | |
| uv tool install "git+https://github.com/markusstrasser/${tool}.git" 2>/dev/null \ | |
| && ok "$tool (from git)" || warn "$tool install failed" | |
| fi | |
| done | |
| # ── Install MCP Dependencies ───────────────────────────────── | |
| step "Installing MCP server dependencies" | |
| (cd "$PROJECTS_DIR/papers-mcp" && uv sync --quiet) | |
| ok "papers-mcp dependencies" | |
| (cd "$PROJECTS_DIR/meta" && uv sync --quiet) | |
| ok "meta dependencies" | |
| (cd "$PROJECTS_DIR/biomedical-mcp" && uv sync --quiet) | |
| ok "biomedical-mcp dependencies" | |
| # ── API Keys ───────────────────────────────────────────────── | |
| step "Setting up API keys (stored in macOS Keychain)" | |
| echo -e " ${DIM}Keys are encrypted at rest. Manage later with: llmx keys list${RESET}" | |
| KEYCHAIN_SERVICE="llmx" | |
| store_key() { | |
| local key_name="$1" | |
| local display_name="$2" | |
| local signup_url="$3" | |
| local required="${4:-optional}" | |
| if [ -n "${!key_name:-}" ]; then | |
| ok "$display_name — already in environment" | |
| security delete-generic-password -a "$KEYCHAIN_SERVICE" -s "$key_name" 2>/dev/null || true | |
| security add-generic-password -a "$KEYCHAIN_SERVICE" -s "$key_name" -w "${!key_name}" 2>/dev/null | |
| return | |
| fi | |
| existing=$(security find-generic-password -a "$KEYCHAIN_SERVICE" -s "$key_name" -w 2>/dev/null || true) | |
| if [ -n "$existing" ]; then | |
| ok "$display_name — already in Keychain" | |
| return | |
| fi | |
| echo "" | |
| if [ "$required" = "required" ]; then | |
| echo -e " ${BOLD}$display_name${RESET} ${RED}(required)${RESET}" | |
| else | |
| echo -e " ${BOLD}$display_name${RESET} ${DIM}(optional — some tools degrade gracefully without it)${RESET}" | |
| fi | |
| echo -e " ${DIM}Get your key: ${signup_url}${RESET}" | |
| read -rp " Enter $key_name (or press Enter to skip): " value | |
| if [ -n "$value" ]; then | |
| security delete-generic-password -a "$KEYCHAIN_SERVICE" -s "$key_name" 2>/dev/null || true | |
| security add-generic-password -a "$KEYCHAIN_SERVICE" -s "$key_name" -w "$value" | |
| ok "Stored $key_name in Keychain" | |
| else | |
| if [ "$required" = "required" ]; then | |
| warn "Skipped $key_name — you'll need this to use $display_name" | |
| else | |
| skip "Skipped $key_name" | |
| fi | |
| fi | |
| } | |
| # Required | |
| store_key "ANTHROPIC_API_KEY" "Anthropic (Claude API + Claude Code)" \ | |
| "https://console.anthropic.com/settings/keys" "required" | |
| store_key "OPENAI_API_KEY" "OpenAI (GPT models + Codex CLI)" \ | |
| "https://platform.openai.com/api-keys" "required" | |
| store_key "GEMINI_API_KEY" "Google Gemini (powers paper RAG in research MCP)" \ | |
| "https://aistudio.google.com/apikey" "required" | |
| # Research infrastructure | |
| store_key "EXA_API_KEY" "Exa (neural web search + claim verification)" \ | |
| "https://dashboard.exa.ai/api-keys" "required" | |
| store_key "BRAVE_API_KEY" "Brave Search (independent web index, triangulation)" \ | |
| "https://brave.com/search/api/" "optional" | |
| store_key "S2_API_KEY" "Semantic Scholar (paper search, higher rate limits)" \ | |
| "https://www.semanticscholar.org/product/api#api-key-form" "optional" | |
| store_key "PERPLEXITY_API_KEY" "Perplexity (grounded search)" \ | |
| "https://www.perplexity.ai/settings/api" "optional" | |
| store_key "FIRECRAWL_API_KEY" "Firecrawl (web scraping + structured extraction)" \ | |
| "https://www.firecrawl.dev/app/api-keys" "optional" | |
| # Other providers | |
| store_key "OPENROUTER_API_KEY" "OpenRouter (multi-model gateway)" \ | |
| "https://openrouter.ai/keys" "optional" | |
| store_key "XAI_API_KEY" "xAI (Grok)" \ | |
| "https://console.x.ai/" "optional" | |
| store_key "MOONSHOT_API_KEY" "Kimi / Moonshot" \ | |
| "https://platform.moonshot.cn/console/api-keys" "optional" | |
| # ── Shell Integration ──────────────────────────────────────── | |
| step "Setting up shell integration" | |
| SHELL_RC="$HOME/.zshrc" | |
| if [ -n "${BASH_VERSION:-}" ]; then | |
| SHELL_RC="$HOME/.bashrc" | |
| fi | |
| KEYCHAIN_LOADER=' | |
| # Load API keys from macOS Keychain (set by setup script / llmx keys) | |
| _load_keychain_key() { | |
| local key="$1" | |
| if [ -z "${!key:-}" ]; then | |
| local val | |
| val=$(security find-generic-password -a "llmx" -s "$key" -w 2>/dev/null) && export "$key=$val" | |
| fi | |
| } | |
| for _k in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENROUTER_API_KEY EXA_API_KEY BRAVE_API_KEY PERPLEXITY_API_KEY S2_API_KEY XAI_API_KEY MOONSHOT_API_KEY FIRECRAWL_API_KEY; do | |
| _load_keychain_key "$_k" | |
| done | |
| unset _k | |
| ' | |
| if ! grep -q '_load_keychain_key' "$SHELL_RC" 2>/dev/null; then | |
| echo "$KEYCHAIN_LOADER" >> "$SHELL_RC" | |
| ok "Added Keychain loader to $SHELL_RC" | |
| else | |
| skip "Keychain loader already in $SHELL_RC" | |
| fi | |
| # Source it now so MCP config commands below can use the keys | |
| eval "$KEYCHAIN_LOADER" | |
| # ── MCP Servers ────────────────────────────────────────────── | |
| step "Configuring MCP servers for Claude Code" | |
| echo -e " ${DIM}These give Claude research, web search, and code navigation tools${RESET}" | |
| # User-scope MCPs (available in all projects) | |
| # brave-search | |
| claude mcp add brave-search --scope user \ | |
| -e BRAVE_API_KEY='${BRAVE_API_KEY}' \ | |
| -- npx -y @modelcontextprotocol/server-brave-search 2>/dev/null \ | |
| && ok "brave-search MCP" || skip "brave-search MCP (already configured or failed)" | |
| # perplexity | |
| claude mcp add perplexity --scope user \ | |
| -- npx -y @perplexity-ai/mcp-server 2>/dev/null \ | |
| && ok "perplexity MCP" || skip "perplexity MCP (already configured or failed)" | |
| # context7 (library documentation) | |
| claude mcp add context7 --scope user \ | |
| -- npx -y @upstash/context7-mcp 2>/dev/null \ | |
| && ok "context7 MCP" || skip "context7 MCP (already configured or failed)" | |
| # firecrawl (web scraping) | |
| claude mcp add firecrawl --scope user \ | |
| -e FIRECRAWL_API_KEY='${FIRECRAWL_API_KEY}' \ | |
| -- npx -y firecrawl-mcp 2>/dev/null \ | |
| && ok "firecrawl MCP" || skip "firecrawl MCP (already configured or failed)" | |
| # exa (neural search — HTTP MCP, needs API key) | |
| EXA_KEY=$(security find-generic-password -a "llmx" -s "EXA_API_KEY" -w 2>/dev/null || echo "") | |
| if [ -n "$EXA_KEY" ]; then | |
| claude mcp add exa --scope user \ | |
| --transport http \ | |
| --url "https://mcp.exa.ai/mcp?exaApiKey=${EXA_KEY}&tools=web_search_exa,web_search_advanced_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check" \ | |
| 2>/dev/null \ | |
| && ok "exa MCP" || skip "exa MCP (already configured or failed)" | |
| else | |
| warn "exa MCP skipped — no EXA_API_KEY" | |
| fi | |
| # research (papers-mcp — paper search, full-text RAG, evidence prep) | |
| claude mcp add research --scope user \ | |
| -- uv run --directory "$PROJECTS_DIR/papers-mcp" papers-mcp 2>/dev/null \ | |
| && ok "research MCP (papers-mcp)" || skip "research MCP (already configured or failed)" | |
| # biomedical (genes, drugs, variants, pathways — 80 tools across 15 domains) | |
| claude mcp add biomedical --scope user \ | |
| -- uv run --directory "$PROJECTS_DIR/biomedical-mcp" biomedical-mcp 2>/dev/null \ | |
| && ok "biomedical MCP" || skip "biomedical MCP (already configured or failed)" | |
| # ── Skills ─────────────────────────────────────────────────── | |
| step "Linking shared skills" | |
| SKILLS_DIR="$HOME/.claude/skills" | |
| mkdir -p "$SKILLS_DIR" | |
| # Symlink each skill directory | |
| for skill_dir in "$PROJECTS_DIR/skills"/*/; do | |
| skill_name=$(basename "$skill_dir") | |
| # Skip non-skill directories | |
| [[ "$skill_name" == "hooks" || "$skill_name" == "archive" || "$skill_name" == "goals" ]] && continue | |
| target="$SKILLS_DIR/$skill_name" | |
| if [ -L "$target" ] || [ -d "$target" ]; then | |
| skip "$skill_name (already linked)" | |
| else | |
| ln -s "$skill_dir" "$target" | |
| ok "$skill_name" | |
| fi | |
| done | |
| # ── Plugins ────────────────────────────────────────────────── | |
| step "Enabling recommended plugins" | |
| claude plugin add frontend-design@claude-plugins-official 2>/dev/null && ok "frontend-design" || skip "frontend-design (already enabled or failed)" | |
| claude plugin add playground@claude-plugins-official 2>/dev/null && ok "playground" || skip "playground (already enabled or failed)" | |
| # ── Daily Update ───────────────────────────────────────────── | |
| step "Setting up daily repo sync" | |
| SYNC_SCRIPT="$HOME/.local/bin/sync-ai-infra.sh" | |
| mkdir -p "$(dirname "$SYNC_SCRIPT")" | |
| SYNC_URL="https://gist.githubusercontent.com/markusstrasser/c0590ee82d700fab4de10a0312e57cc0/raw/friend-sync.sh" | |
| curl -sL "$SYNC_URL" -o "$SYNC_SCRIPT" | |
| chmod +x "$SYNC_SCRIPT" | |
| ok "Downloaded $SYNC_SCRIPT (auto-updates from gist)" | |
| # launchd plist for daily sync at 06:00 | |
| PLIST_DIR="$HOME/Library/LaunchAgents" | |
| PLIST_FILE="$PLIST_DIR/com.ai-infra.daily-sync.plist" | |
| if [ ! -f "$PLIST_FILE" ]; then | |
| mkdir -p "$PLIST_DIR" | |
| cat > "$PLIST_FILE" << PLIST | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key> | |
| <string>com.ai-infra.daily-sync</string> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>${SYNC_SCRIPT}</string> | |
| </array> | |
| <key>StartCalendarInterval</key> | |
| <dict> | |
| <key>Hour</key> | |
| <integer>6</integer> | |
| <key>Minute</key> | |
| <integer>0</integer> | |
| </dict> | |
| <key>StandardOutPath</key> | |
| <string>${HOME}/.local/log/ai-infra-sync.log</string> | |
| <key>StandardErrorPath</key> | |
| <string>${HOME}/.local/log/ai-infra-sync.log</string> | |
| </dict> | |
| </plist> | |
| PLIST | |
| mkdir -p "$HOME/.local/log" | |
| launchctl load "$PLIST_FILE" 2>/dev/null | |
| ok "Daily sync at 06:00 (launchd)" | |
| else | |
| skip "Daily sync plist already exists" | |
| fi | |
| # ── Summary ────────────────────────────────────────────────── | |
| step "Setup complete!" | |
| echo "" | |
| echo -e " ${BOLD}CLI tools:${RESET}" | |
| echo " claude — Claude Code (AI coding agent)" | |
| echo " gemini — Gemini CLI (free tier)" | |
| echo " codex — Codex CLI (OpenAI coding agent)" | |
| echo " llmx — Unified LLM CLI (all providers)" | |
| echo " emb — Embed, index, search text corpora" | |
| echo " parsers — Parse platform exports (Twitter, Gmail, iMessage, etc.)" | |
| echo "" | |
| echo -e " ${BOLD}MCP servers (available in Claude Code):${RESET}" | |
| echo " research — Paper search, full-text RAG, evidence synthesis" | |
| echo " biomedical — Genes, drugs, variants, pathways (80 tools)" | |
| echo " exa — Neural web search, claim verification" | |
| echo " brave-search — Independent web search index" | |
| echo " perplexity — Grounded AI search" | |
| echo " firecrawl — Web scraping + structured extraction" | |
| echo " context7 — Library documentation lookup" | |
| echo "" | |
| echo -e " ${BOLD}Skills:${RESET} $(ls "$HOME/.claude/skills" 2>/dev/null | wc -l | tr -d ' ') skills linked from ~/Projects/skills/" | |
| echo " researcher, model-review, brainstorm, causal-dag, and more" | |
| echo "" | |
| echo -e " ${BOLD}API keys:${RESET}" | |
| echo " Manage: llmx keys list / set / get / delete" | |
| echo " Loaded: automatically from Keychain on shell start" | |
| echo "" | |
| echo -e " ${BOLD}Daily sync:${RESET}" | |
| echo " Auto: pulls repos + updates tools at 06:00 daily (launchd)" | |
| echo " Manual: ~/.local/bin/sync-ai-infra.sh" | |
| echo " Source: https://gist.github.com/markusstrasser/c0590ee82d700fab4de10a0312e57cc0" | |
| echo "" | |
| echo -e " ${BOLD}Next steps:${RESET}" | |
| echo " 1. Open a new terminal (to load Keychain keys)" | |
| echo " 2. Run: claude — start Claude Code" | |
| echo " 3. Run: llmx 'hello' — test LLM access" | |
| echo " 4. Check: claude mcp list — verify MCP servers are connected" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment