dotfiles/.agents/install.sh
Brydon DeWitt 9544b4e2ab chore: wire exa mcp server into global vscode config via install.sh
install.sh now ensures both all-agents and exa are present in the VS
Code global mcp.json on every machine.
2026-05-23 15:02:55 -04:00

180 lines
8.1 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# install.sh — Wire ~/dotfiles/.agents/ into global tool configs.
# Idempotent: safe to re-run. Creates dirs, symlinks, and config entries.
# Run once per machine after cloning dotfiles.
set -euo pipefail
DOTFILES_AGENTS="$(cd "$(dirname "$0")" && pwd)"
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/agent-support.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/agent-support.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/agent-support.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 MCP entry ────────────────────────────────────────────
OC_CONFIG="$HOME/.config/opencode/opencode.json"
MCP_KEY="all-agents"
MCP_CMD="[\"node\", \"--experimental-strip-types\", \"$DOTFILES_AGENTS/mcp/index.ts\"]"
if [[ ! -f "$OC_CONFIG" ]]; then
warn "No OpenCode config at $OC_CONFIG — creating minimal config with MCP entry."
printf '{\n "$schema": "https://opencode.ai/config.json",\n "mcp": {\n "%s": {\n "type": "local",\n "command": %s\n }\n }\n}\n' "$MCP_KEY" "$MCP_CMD" > "$OC_CONFIG"
log "Created $OC_CONFIG with all-agents MCP entry"
elif node -e "const c=JSON.parse(require('fs').readFileSync('$OC_CONFIG','utf8')); process.exit(c.mcp && c.mcp['$MCP_KEY'] ? 0 : 1)" 2>/dev/null; then
skip "OpenCode MCP entry '$MCP_KEY' already present in $OC_CONFIG"
else
# Merge the MCP entry using node — jq may not be available everywhere
node -e "
const fs = require('fs');
const path = '$OC_CONFIG';
const config = JSON.parse(fs.readFileSync(path, 'utf8'));
config.mcp = config.mcp || {};
config.mcp['$MCP_KEY'] = { type: 'local', command: $MCP_CMD };
fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n');
console.log('Merged all-agents MCP entry into ' + path);
"
log "OpenCode MCP entry merged: $OC_CONFIG"
fi
# ── 5. 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_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
# ── 6. 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
# ── 7. 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'