#!/usr/bin/env bash # install.sh — Wire .agents/ into global tool configs. # Run with --host to also install llama-server, VS Code, Docker, and extensions. # Idempotent: safe to re-run. Creates dirs, symlinks, and config entries. # Run once per machine after cloning dotfiles. set -euo pipefail INSTALL_HOST=false for arg in "$@"; do case "$arg" in --host) INSTALL_HOST=true ;; esac; done DOTFILES_AGENTS="$(cd "$(dirname "$0")" && pwd)/.agents" log() { printf '\033[0;32m✓\033[0m %s\n' "$1"; } warn() { printf '\033[0;33m⚠\033[0m %s\n' "$1"; } skip() { printf '\033[0;34m–\033[0m %s\n' "$1"; } # ── 1. Copilot global hooks ────────────────────────────────────────────────── # Generate ~/.copilot/hooks/hooks.json with absolute paths so the hooks # work from any workspace — no per-project symlinks or stubs needed. COPILOT_HOOKS_DIR="$HOME/.copilot/hooks" COPILOT_HOOK_FILE="$COPILOT_HOOKS_DIR/hooks.json" mkdir -p "$COPILOT_HOOKS_DIR" # Migrate: remove old symlink if present if [[ -L "$COPILOT_HOOK_FILE" ]]; then rm "$COPILOT_HOOK_FILE" log "Removed old Copilot hook symlink (migrating to generated file)" fi EXPECTED_PRE="$DOTFILES_AGENTS/hooks/pre-tool-use.sh" if [[ -f "$COPILOT_HOOK_FILE" ]] && \ node -e "const c=JSON.parse(require('fs').readFileSync('$COPILOT_HOOK_FILE','utf8')); process.exit(c.hooks&&c.hooks.PreToolUse&&c.hooks.PreToolUse[0].command==='$EXPECTED_PRE'?0:1);" 2>/dev/null; then skip "Copilot global hooks already up-to-date: $COPILOT_HOOK_FILE" else node -e " const fs = require('fs'); const d = '$DOTFILES_AGENTS/hooks'; const hooks = { UserPromptSubmit: [{type:'command',command:d+'/user-prompt-submit.sh',timeout:5}], SessionStart: [{type:'command',command:d+'/session-start.sh',timeout:10}], PreToolUse: [{type:'command',command:d+'/pre-tool-use.sh',timeout:5}], PostToolUse: [{type:'command',command:d+'/post-tool-use.sh',timeout:5}], PreCompact: [{type:'command',command:d+'/pre-compact.sh',timeout:10}], Stop: [{type:'command',command:d+'/stop.sh',timeout:5}] }; fs.writeFileSync('$COPILOT_HOOK_FILE', JSON.stringify({hooks}, null, 2) + '\n'); " log "Copilot global hooks generated with absolute paths: $COPILOT_HOOK_FILE" fi # ── 2. OpenCode global plugin ──────────────────────────────────────────────── OC_PLUGINS_DIR="$HOME/.config/opencode/plugins" OC_PLUGIN_TARGET="$DOTFILES_AGENTS/frameworks/opencode/plugin.ts" OC_PLUGIN_LINK="$OC_PLUGINS_DIR/plugin.ts" mkdir -p "$OC_PLUGINS_DIR" if [[ -L "$OC_PLUGIN_LINK" && "$(readlink "$OC_PLUGIN_LINK")" == "$OC_PLUGIN_TARGET" ]]; then skip "OpenCode plugin symlink already set: $OC_PLUGIN_LINK" else ln -sf "$OC_PLUGIN_TARGET" "$OC_PLUGIN_LINK" log "OpenCode plugin symlink: $OC_PLUGIN_LINK → $OC_PLUGIN_TARGET" fi # ── 3. OpenCode global agents dir ─────────────────────────────────────────── OC_AGENTS_DIR="$HOME/.config/opencode/agents" OC_AGENTS_SOURCE="$DOTFILES_AGENTS/agents" mkdir -p "$OC_AGENTS_DIR" for src in "$OC_AGENTS_SOURCE"/*.md; do name="$(basename "$src")" link="$OC_AGENTS_DIR/$name" if [[ "$name" == "AGENTS.md" ]]; then continue; fi # not a slash-command agent if [[ -L "$link" && "$(readlink "$link")" == "$src" ]]; then skip "OpenCode agent symlink already set: $link" else ln -sf "$src" "$link" log "OpenCode agent symlink: $link → $src" fi done # ── 3a. OpenCode global AGENTS.md ─────────────────────────────────────────── OC_AGENTS_TARGET="$DOTFILES_AGENTS/AGENTS.md" OC_AGENTS_LINK="$HOME/.config/opencode/AGENTS.md" if [[ -L "$OC_AGENTS_LINK" && "$(readlink "$OC_AGENTS_LINK")" == "$OC_AGENTS_TARGET" ]]; then skip "OpenCode AGENTS.md symlink already set: $OC_AGENTS_LINK" else ln -sf "$OC_AGENTS_TARGET" "$OC_AGENTS_LINK" log "OpenCode AGENTS.md symlink: $OC_AGENTS_LINK → $OC_AGENTS_TARGET" fi # ── 4. OpenCode global config (opencode.json) ──────────────────────────────── OC_CONFIG_SOURCE="$DOTFILES_AGENTS/frameworks/opencode/opencode.json" OC_CONFIG_LINK="$HOME/.config/opencode/opencode.json" mkdir -p "$(dirname "$OC_CONFIG_LINK")" if [[ -L "$OC_CONFIG_LINK" && "$(readlink "$OC_CONFIG_LINK")" == "$OC_CONFIG_SOURCE" ]]; then skip "OpenCode config symlink already set: $OC_CONFIG_LINK" else ln -sf "$OC_CONFIG_SOURCE" "$OC_CONFIG_LINK" log "OpenCode config symlink: $OC_CONFIG_LINK → $OC_CONFIG_SOURCE" fi # ── 5. Build llama-server (requires --host) ────────────────────────────────── if [[ "$INSTALL_HOST" != "true" ]]; then skip "llama-server build skipped (use --host to install)" else if [[ -x /opt/llama-server/llama-server ]]; then skip "llama-server already installed at /opt/llama-server/llama-server" else sudo apt-get install -y cmake build-essential nvidia-cuda-toolkit libgomp1 git ( git clone --depth 1 --branch b9279 https://github.com/ggml-org/llama.cpp.git /tmp/llama-build cd /tmp/llama-build cmake -B build \ -DGGML_CUDA=ON \ -DCMAKE_BUILD_TYPE=Release \ -DLLAMA_BUILD_SERVER=ON \ -DLLAMA_BUILD_TESTS=OFF \ -DLLAMA_BUILD_EXAMPLES=OFF cmake --build build --config Release -j$(nproc) sudo mkdir -p /opt/llama-server sudo cp build/bin/llama-server /opt/llama-server/ sudo cp -P build/bin/libggml*.so* /opt/llama-server/ sudo cp -P build/bin/libllama*.so* /opt/llama-server/ sudo cp -P build/bin/libmtmd*.so* /opt/llama-server/ 2>/dev/null || true echo "/opt/llama-server" | sudo tee /etc/ld.so.conf.d/llama-server.conf sudo ldconfig rm -rf /tmp/llama-build ) log "llama-server built and installed to /opt/llama-server/" fi fi # ── 6. Llama-server host config (requires --host) ─────────────────────────── if [[ "$INSTALL_HOST" != "true" ]]; then skip "Llama-server host config skipped (use --host to install)" else # ── 6a. Model downloads (requires --host) ────────────────────────────────── if ! command -v huggingface-cli >/dev/null 2>&1; then warn "huggingface-cli not found — skipping model downloads (install via 'pip install huggingface_hub')" else _hf_download() { local repo="$1" file="$2" dir="$3" local dest="$dir/$file" if [[ -f "$dest" ]]; then skip "Model already present: $dest" else mkdir -p "$dir" huggingface-cli download "$repo" "$file" --local-dir "$dir" >/dev/null log "Downloaded model: $repo/$file → $dest" fi } _hf_download "Jackrong/Qwopus3.6-27B-v2-MTP-GGUF" "Qwopus3.6-27B-v2-MTP-Q4_K_M.gguf" "$HOME/models" _hf_download "Jackrong/Qwopus3.5-9B-Coder-MTP-GGUF" "Qwopus3.5-9B-Coder-MTP-Q8_0.gguf" "$HOME/models" _hf_download "bartowski/agentica-org_DeepCoder-14B-Preview-GGUF" "agentica-org_DeepCoder-14B-Preview-Q5_K_M.gguf" "$HOME/models" _hf_download "byteshape/Qwen3.6-35B-A3B-MTP-GGUF" "Qwen3.6-35B-A3B-IQ3_S-3.06bpw.gguf" "$HOME/models" _hf_download "Jackrong/Qwopus3.6-35B-A3B-v1-MTP-GGUF" "Qwopus3.6-35B-A3B-v1-MTP-Q4_K_M.gguf" "$HOME/models" _hf_download "mradermacher/OmniCoder-2-9B-GGUF" "OmniCoder-2-9B.Q8_0.gguf" "$HOME/models/OmniCoder-2-9B.Q8_0" _hf_download "mradermacher/OmniCoder-2-9B-GGUF" "mmproj-Q8_0.gguf" "$HOME/models/OmniCoder-2-9B.Q8_0" _hf_download "bartowski/Qwen_Qwen3-14B-GGUF" "Qwen_Qwen3-14B-Q4_K_M.gguf" "$HOME/models" _hf_download "bartowski/Qwen_Qwen3.6-27B-GGUF" "Qwen_Qwen3.6-27B-Q4_K_M.gguf" "$HOME/models" fi PRESETS_SRC="$DOTFILES_AGENTS/llama-server/presets.ini" PRESETS_DST="$HOME/models/presets.ini" mkdir -p "$HOME/models" if diff -q "$PRESETS_SRC" "$PRESETS_DST" >/dev/null 2>&1; then skip "presets.ini already up-to-date: $PRESETS_DST" else cp "$PRESETS_SRC" "$PRESETS_DST" log "Installed presets.ini → $PRESETS_DST" fi SVC_SRC="$DOTFILES_AGENTS/llama-server/llama-server.service" SVC_DST="/etc/systemd/system/llama-server.service" if diff -q "$SVC_SRC" "$SVC_DST" >/dev/null 2>&1; then skip "llama-server.service already up-to-date: $SVC_DST" else cp "$SVC_SRC" "$SVC_DST" log "Installed llama-server.service → $SVC_DST" fi PATH_SRC="$DOTFILES_AGENTS/llama-server/llama-server-presets.path" PATH_DST="/etc/systemd/system/llama-server-presets.path" if diff -q "$PATH_SRC" "$PATH_DST" >/dev/null 2>&1; then skip "llama-server-presets.path already up-to-date: $PATH_DST" else cp "$PATH_SRC" "$PATH_DST" log "Installed llama-server-presets.path → $PATH_DST" fi PSVC_SRC="$DOTFILES_AGENTS/llama-server/llama-server-presets.service" PSVC_DST="/etc/systemd/system/llama-server-presets.service" if diff -q "$PSVC_SRC" "$PSVC_DST" >/dev/null 2>&1; then skip "llama-server-presets.service already up-to-date: $PSVC_DST" else cp "$PSVC_SRC" "$PSVC_DST" log "Installed llama-server-presets.service → $PSVC_DST" fi START_SRC="$DOTFILES_AGENTS/llama-server/start.sh" START_DST="/opt/llama-server/start.sh" mkdir -p "$(dirname "$START_DST")" if diff -q "$START_SRC" "$START_DST" >/dev/null 2>&1; then skip "start.sh already up-to-date: $START_DST" else cp "$START_SRC" "$START_DST" log "Installed start.sh → $START_DST" fi fi # ── 7. VS Code global MCP ──────────────────────────────────────────────────── # Primary remote/server path; falls back to local if running VS Code locally. VSCODE_MCP_PATHS=( "$HOME/.vscode-server/data/User/mcp.json" "$HOME/.vscode/data/User/mcp.json" "$HOME/Library/Application Support/Code/User/mcp.json" ) for VSCODE_MCP in "${VSCODE_MCP_PATHS[@]}"; do if [[ -d "$(dirname "$VSCODE_MCP")" ]]; then MCP_KEY="all-agents" MCP_SERVER_CMD="node" MCP_SERVER_ARGS="[\"--experimental-strip-types\", \"$DOTFILES_AGENTS/mcp/index.ts\"]" node -e " const fs = require('fs'); const path = '$VSCODE_MCP'; const config = fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, 'utf8')) : {}; config.servers = config.servers || {}; let changed = false; if (!config.servers['$MCP_KEY']) { config.servers['$MCP_KEY'] = { type: 'stdio', command: '$MCP_SERVER_CMD', args: $MCP_SERVER_ARGS }; changed = true; } if (!config.servers['exa']) { config.servers['exa'] = { type: 'http', url: 'https://mcp.exa.ai/mcp' }; changed = true; } if (changed) { fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n'); console.log('VS Code MCP config updated: ' + path); } else { process.stdout.write(''); } " log "VS Code MCP entries ensured: $VSCODE_MCP" break fi done # ── 8. VS Code global prompts dir ─────────────────────────────────────────── for VSCODE_PROMPTS_DIR in \ "$HOME/.vscode-server/data/User/prompts" \ "$HOME/.vscode/data/User/prompts"; do if [[ -d "$(dirname "$(dirname "$VSCODE_PROMPTS_DIR")")" ]]; then mkdir -p "$VSCODE_PROMPTS_DIR" log "VS Code prompts dir ensured: $VSCODE_PROMPTS_DIR" break fi done # ── 9. MCP server dependencies ─────────────────────────────────────────────── MCP_DIR="$DOTFILES_AGENTS/mcp" if [[ ! -d "$MCP_DIR/node_modules/@modelcontextprotocol" ]]; then log "Installing MCP server dependencies (npm install in $MCP_DIR)..." npm install --prefix "$MCP_DIR" --silent log "MCP server dependencies installed" else skip "MCP server node_modules already present" fi # ── 10. VS Code, Docker, extensions (requires --host) ──────────────────────── if [[ "$INSTALL_HOST" != "true" ]]; then skip "VS Code, Docker, extensions skipped (use --host to install)" else # ── 10a. VS Code ────────────────────────────────────────────────────── if command -v code >/dev/null 2>&1; then skip "VS Code already installed" else log "Installing VS Code..." sudo apt-get update sudo apt-get install -y wget gpg wget -qO- https://packages.microsoft.com/keys/microsoft.asc | \ gpg --dearmor -o /usr/share/keyrings/packages.microsoft.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | \ sudo tee /etc/apt/sources.list.d/vscode.list sudo apt-get update sudo apt-get install -y code log "VS Code installed" fi # ── 10b. Docker ────────────────────────────────────────────────────── if command -v docker >/dev/null 2>&1; then skip "Docker already installed" else log "Installing Docker..." sudo apt-get update sudo apt-get install -y ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin log "Docker installed" fi # ── 10c. VS Code extensions ────────────────────────────────────────── _install_ext() { local ext_id="$1" if code --list-extensions 2>/dev/null | grep -qi "^${ext_id}$"; then skip "VS Code extension already installed: $ext_id" else code --install-extension "$ext_id" >/dev/null 2>&1 log "VS Code extension installed: $ext_id" fi } _install_ext "ms-vscode-remote.vscode-remote-extensionpack" _install_ext "ms-azuretools.vscode-docker" _install_ext "streetsidesoftware.code-spell-checker" _install_ext "EditorConfig.EditorConfig" _install_ext "dbaeumer.vscode-eslint" _install_ext "mhutchie.git-graph" _install_ext "bierner.github-markdown-preview" _install_ext "esbenp.prettier-vscode" _install_ext "yoavbls.pretty-ts-errors" fi # ── 11. SSH keys, agent, forwarding & .bashrc (requires --host) ─────────────── if [[ "$INSTALL_HOST" != "true" ]]; then skip "SSH & .bashrc setup skipped (use --host to install)" else # ── 11a. SSH directory & keys ────────────────────────────────────────── mkdir -p "$HOME/.ssh" if ! ls "$HOME/.ssh/"* >/dev/null 2>&1; then warn "No SSH keys found in ~/.ssh — please generate one manually:" warn " ssh-keygen -t ed25519 -C \"your@email.com\"" warn " ssh-add ~/.ssh/id_ed25519" else # Fix permissions on existing keys for key in "$HOME/.ssh"/*; do if [[ -f "$key" ]] && [[ ! -d "$key" ]]; then basename_key="$(basename "$key")" # Private keys (id_* without .pub/.cer) and config should be 600 if [[ "$basename_key" =~ ^id_ ]] && [[ ! "$basename_key" =~ \.(pub|cer)$ ]]; then chmod 600 "$key" log "Fixed SSH key permissions: $key (600)" elif [[ "$basename_key" == "config" ]]; then chmod 600 "$key" log "Fixed SSH config permissions: $key (600)" elif [[ "$basename_key" =~ \.(pub|cer)$ ]]; then chmod 644 "$key" log "Fixed SSH public key permissions: $key (644)" fi fi done chmod 700 "$HOME/.ssh" log "SSH directory permissions set (700)" fi # ── 11b. SSH agent & forwarding & PATH in .bashrc ───────────────────── BASHRC="$HOME/.bashrc" SSH_AGENT_MARKER="# --- install.sh: ssh-agent ---" if ! grep -qF "$SSH_AGENT_MARKER" "$BASHRC" 2>/dev/null; then cat >> "$BASHRC" << 'BASHRC_EOF' # --- install.sh: ssh-agent --- # Start ssh-agent if not already running if [ -z "$SSH_AUTH_SOCK" ]; then eval "$(ssh-agent -s)" >/dev/null 2>&1 fi # --- install.sh: ssh forwarding --- # SSH agent forwarding: use 'ssh -A host' to forward agent to remote host. # To auto-forward on specific hosts, add to ~/.ssh/config: # Host example.com # ForwardAgent yes # --- install.sh: PATH additions --- # llama-server export PATH="/opt/llama-server:$PATH" # WSL lib (for CUDA/cuDNN libraries in WSL environments) if [ -d "/usr/lib/wsl/lib" ]; then export LD_LIBRARY_PATH="/usr/lib/wsl/lib:$LD_LIBRARY_PATH" fi BASHRC_EOF log "Added ssh-agent, forwarding notes, and PATH to $BASHRC" else skip "ssh-agent snippet already in $BASHRC" fi fi # ── Done ───────────────────────────────────────────────────────────────────── printf '\n\033[0;32minstall.sh complete.\033[0m\n' printf 'Next steps:\n' printf ' 1. Restart OpenCode to pick up the new global plugin.\n' printf ' 2. Reload VS Code / reconnect to reload MCP servers.\n' printf ' 3. Smoke test: /research slash prompt fires; a denied terminal command is blocked.\n'