model config, downloads, llama-server service and a README.md to explain --host
252 lines
11 KiB
Bash
Executable File
252 lines
11 KiB
Bash
Executable File
#!/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"
|
||
DOTFILES_CONFIG="$(cd "$(dirname "$0")" && pwd)/config"
|
||
|
||
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_CONFIG/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_CONFIG/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_CONFIG/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_CONFIG/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_CONFIG/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_CONFIG/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'
|