#!/usr/bin/env bash # install.sh — Wire .agents/ into global tool configs. # Run with --host to also install llama-server config and systemd services. # 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. Llama-server host config (requires --host) ─────────────────────────── if [[ "$INSTALL_HOST" != "true" ]]; then skip "Llama-server host config skipped (use --host to install)" else # ── 5a. 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 # ── 6. 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 # ── 7. 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 # ── 8. 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 # ── 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'