2026-05-08 11:35:10 -04:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Bootstrap or re-sync the OpenCode harness on this Mac.
|
|
|
|
|
#
|
|
|
|
|
# Idempotent — re-run after pulling changes to opencode.json or the
|
|
|
|
|
# Phoenix bridge plugin. Each step checks before doing work.
|
|
|
|
|
#
|
|
|
|
|
# What this does:
|
|
|
|
|
# 1. Verify Homebrew is present
|
|
|
|
|
# 2. Install node, uv, opencode, jq (skips if already at latest)
|
|
|
|
|
# 3. Pre-cache Playwright's chromium so the first MCP call is instant
|
2026-05-10 21:14:43 -04:00
|
|
|
# 4. Install Serena (uv tool — LSP-backed code navigation MCP)
|
|
|
|
|
# 5. Wire basic-memory's storage to the Obsidian vault's AI-memory folder
|
|
|
|
|
# 6. Install github-mcp-server + check for GITHUB_PERSONAL_ACCESS_TOKEN
|
|
|
|
|
# 7. Install task-master-ai (workflow MCP)
|
|
|
|
|
# 8. Install the Phoenix bridge plugin's OTel deps
|
|
|
|
|
# 9. Generate ~/.config/opencode/opencode.json from the repo's
|
2026-05-08 11:35:10 -04:00
|
|
|
# opencode.json with relative plugin paths rewritten to absolute,
|
|
|
|
|
# so opencode loads the plugin regardless of where it's launched.
|
|
|
|
|
#
|
|
|
|
|
# Usage: ./install.sh (from this directory)
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
cd "$(dirname "$0")"
|
|
|
|
|
HERE="$(pwd)"
|
|
|
|
|
|
|
|
|
|
# --- Pretty printing ---------------------------------------------------------
|
|
|
|
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
|
|
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
|
|
|
|
info() { printf ' → %s\n' "$*"; }
|
|
|
|
|
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
|
|
|
|
fail() { printf ' \033[31m✗\033[0m %s\n' "$*"; exit 1; }
|
|
|
|
|
|
|
|
|
|
# --- 1. Homebrew -------------------------------------------------------------
|
2026-05-10 21:14:43 -04:00
|
|
|
bold "[1/9] Homebrew"
|
2026-05-08 11:35:10 -04:00
|
|
|
if ! command -v brew >/dev/null 2>&1; then
|
|
|
|
|
fail "brew not found. Install from https://brew.sh, then re-run."
|
|
|
|
|
fi
|
|
|
|
|
ok "brew $(brew --version | head -1 | awk '{print $2}')"
|
|
|
|
|
|
|
|
|
|
# --- 2. CLI deps -------------------------------------------------------------
|
2026-05-10 21:14:43 -04:00
|
|
|
bold "[2/9] CLI dependencies"
|
2026-05-08 11:35:10 -04:00
|
|
|
brew_install_if_missing() {
|
|
|
|
|
local pkg="$1"
|
|
|
|
|
local bin="${2:-$1}"
|
|
|
|
|
if command -v "$bin" >/dev/null 2>&1; then
|
|
|
|
|
ok "$pkg already installed ($(command -v "$bin"))"
|
|
|
|
|
else
|
|
|
|
|
info "installing $pkg"
|
|
|
|
|
brew install "$pkg"
|
|
|
|
|
ok "$pkg installed"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
brew_install_if_missing node node
|
|
|
|
|
brew_install_if_missing uv uv
|
|
|
|
|
brew_install_if_missing jq jq
|
|
|
|
|
# opencode is in a tap; check the binary, not the formula name.
|
|
|
|
|
if command -v opencode >/dev/null 2>&1; then
|
|
|
|
|
ok "opencode already installed ($(command -v opencode))"
|
|
|
|
|
else
|
|
|
|
|
info "tapping sst/tap and installing opencode"
|
|
|
|
|
brew install sst/tap/opencode
|
|
|
|
|
ok "opencode installed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 3. Playwright browsers --------------------------------------------------
|
2026-05-10 21:14:43 -04:00
|
|
|
bold "[3/9] Playwright browser cache"
|
2026-05-08 11:35:10 -04:00
|
|
|
PW_CACHE="${HOME}/Library/Caches/ms-playwright"
|
|
|
|
|
if [[ -d "$PW_CACHE" ]] && find "$PW_CACHE" -name "chrome" -o -name "Chromium*" 2>/dev/null | grep -q .; then
|
|
|
|
|
ok "browsers already cached at $PW_CACHE"
|
|
|
|
|
else
|
|
|
|
|
info "downloading chromium (~200 MB) — first run only"
|
|
|
|
|
npx -y @playwright/mcp@latest --help >/dev/null 2>&1 || true
|
|
|
|
|
ok "browsers cached"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-10 21:14:43 -04:00
|
|
|
# --- 4. Serena (LSP-backed semantic code navigation MCP) --------------------
|
|
|
|
|
# Installed once as a uv tool so opencode can launch it as `serena
|
|
|
|
|
# start-mcp-server ...` without paying uvx's resolution cost on every
|
|
|
|
|
# session start. --prerelease=allow is required because serena-agent
|
|
|
|
|
# ships pre-1.0 versions.
|
|
|
|
|
bold "[4/9] Serena MCP"
|
|
|
|
|
if uv tool list 2>/dev/null | awk '{print $1}' | grep -qx 'serena-agent'; then
|
|
|
|
|
ok "serena-agent already installed ($(serena --version 2>/dev/null | head -1 || echo 'version unknown'))"
|
|
|
|
|
else
|
|
|
|
|
info "installing serena-agent via uv tool (~30s first run)"
|
|
|
|
|
uv tool install -p 3.13 serena-agent@latest --prerelease=allow
|
|
|
|
|
ok "serena-agent installed"
|
|
|
|
|
fi
|
|
|
|
|
if ! command -v serena >/dev/null 2>&1; then
|
|
|
|
|
warn "serena binary not on PATH — uv tool's bin dir may not be exported."
|
|
|
|
|
warn "Add this to your shell rc: export PATH=\"\$HOME/.local/bin:\$PATH\""
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 5. basic-memory storage ------------------------------------------------
|
|
|
|
|
# basic-memory defaults its project home to ~/basic-memory. We point that
|
|
|
|
|
# at a folder inside the Obsidian vault via symlink so the AI's notes
|
|
|
|
|
# show up in Obsidian's graph and search. Symlink (not env var) chosen
|
|
|
|
|
# because it's stable across basic-memory's evolving config schema.
|
|
|
|
|
bold "[5/9] basic-memory storage"
|
|
|
|
|
AI_MEM_PATH="${HOME}/Documents/obsidian/AI-memory"
|
|
|
|
|
if [[ ! -d "$AI_MEM_PATH" ]]; then
|
|
|
|
|
info "creating $AI_MEM_PATH"
|
|
|
|
|
mkdir -p "$AI_MEM_PATH"
|
|
|
|
|
ok "AI-memory directory created"
|
|
|
|
|
else
|
|
|
|
|
ok "AI-memory directory exists at $AI_MEM_PATH"
|
|
|
|
|
fi
|
|
|
|
|
if [[ -L "${HOME}/basic-memory" ]]; then
|
|
|
|
|
link_target="$(readlink "${HOME}/basic-memory")"
|
|
|
|
|
if [[ "$link_target" == "$AI_MEM_PATH" ]]; then
|
|
|
|
|
ok "~/basic-memory already linked to AI-memory"
|
|
|
|
|
else
|
|
|
|
|
warn "~/basic-memory points to $link_target — leaving as-is. basic-memory MCP will write there, not to AI-memory."
|
|
|
|
|
fi
|
|
|
|
|
elif [[ -e "${HOME}/basic-memory" ]]; then
|
|
|
|
|
warn "~/basic-memory exists and is not a symlink. Move or remove it for AI-memory linkage."
|
|
|
|
|
else
|
|
|
|
|
info "linking ~/basic-memory -> $AI_MEM_PATH"
|
|
|
|
|
ln -s "$AI_MEM_PATH" "${HOME}/basic-memory"
|
|
|
|
|
ok "symlink created"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 6. github-mcp-server (GitHub MCP, classic-PAT auto-scope-filtered) -----
|
|
|
|
|
# Homebrew formula tracks upstream releases; the binary is a Go single-file.
|
|
|
|
|
# We launch it via opencode.json's mcp.github entry with --read-only and a
|
|
|
|
|
# narrowed --toolsets allowlist; auto-scope-filtering on classic PATs (the
|
|
|
|
|
# Jan 2026 GitHub feature) cuts ~23k tokens of tool-list overhead — significant
|
|
|
|
|
# for a local 70B's effective context. Token itself is NOT in opencode.json
|
|
|
|
|
# (it's git-tracked); github-mcp-server inherits GITHUB_PERSONAL_ACCESS_TOKEN
|
|
|
|
|
# from the user's shell env.
|
|
|
|
|
bold "[6/9] github-mcp-server"
|
|
|
|
|
brew_install_if_missing github-mcp-server github-mcp-server
|
|
|
|
|
if [[ -z "${GITHUB_PERSONAL_ACCESS_TOKEN:-}" ]]; then
|
|
|
|
|
warn "GITHUB_PERSONAL_ACCESS_TOKEN is not set in your shell environment."
|
|
|
|
|
warn " github-mcp-server will fail to connect on opencode startup."
|
|
|
|
|
warn " Fix:"
|
|
|
|
|
warn " 1. Create a classic PAT (starts with ghp_) at"
|
|
|
|
|
warn " https://github.com/settings/tokens with the scopes you want"
|
|
|
|
|
warn " exposed (auto-filtering hides tools whose scopes the PAT lacks)."
|
|
|
|
|
warn " 2. Add to ~/.zshrc:"
|
|
|
|
|
warn " export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxxxxxxxxxxx"
|
|
|
|
|
warn " 3. Re-source the shell (or open a new terminal) before launching opencode."
|
|
|
|
|
else
|
|
|
|
|
ok "GITHUB_PERSONAL_ACCESS_TOKEN is set in this shell"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 7. claude-task-master (workflow MCP, npm global) -----------------------
|
|
|
|
|
# task-master-ai: workflow/task-gate MCP. File-based (.taskmaster/ in each
|
|
|
|
|
# project), no DB, no external service. opencode.json launches it via
|
|
|
|
|
# `npx -y task-master-ai`; the global install also provides the `task-master`
|
|
|
|
|
# CLI (`task-master init` to scaffold a project's tasks).
|
|
|
|
|
bold "[7/9] claude-task-master"
|
|
|
|
|
if command -v task-master >/dev/null 2>&1; then
|
|
|
|
|
ok "task-master-ai already installed ($(task-master --version 2>/dev/null | head -1 || echo 'version unknown'))"
|
|
|
|
|
else
|
|
|
|
|
info "installing task-master-ai globally via npm"
|
|
|
|
|
npm install -g task-master-ai
|
|
|
|
|
ok "task-master-ai installed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- 8. Phoenix bridge plugin deps ------------------------------------------
|
|
|
|
|
bold "[8/9] Phoenix bridge plugin deps"
|
2026-05-08 11:35:10 -04:00
|
|
|
if [[ -d ".opencode/plugin/node_modules" && -f ".opencode/plugin/package-lock.json" ]]; then
|
|
|
|
|
# Re-run npm install if package.json is newer than the lockfile, otherwise skip.
|
|
|
|
|
if [[ ".opencode/plugin/package.json" -nt ".opencode/plugin/package-lock.json" ]]; then
|
|
|
|
|
info "package.json is newer than lockfile — running npm install"
|
|
|
|
|
( cd .opencode/plugin && npm install )
|
|
|
|
|
ok "deps updated"
|
|
|
|
|
else
|
|
|
|
|
ok "deps already installed"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
info "installing OTel deps (one-time, ~40 MB)"
|
|
|
|
|
( cd .opencode/plugin && npm install )
|
|
|
|
|
ok "deps installed"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-10 21:14:43 -04:00
|
|
|
# --- 9. Generate ~/.config/opencode/opencode.json ---------------------------
|
2026-05-08 11:35:10 -04:00
|
|
|
# The repo's opencode.json uses relative plugin paths so it stays valid
|
|
|
|
|
# in-place. Rewriting them to absolute paths here makes opencode find the
|
|
|
|
|
# plugin regardless of which directory it was launched from. Re-run this
|
|
|
|
|
# script after editing opencode.json.
|
2026-05-10 21:14:43 -04:00
|
|
|
bold "[9/9] Deploy global config"
|
2026-05-08 11:35:10 -04:00
|
|
|
mkdir -p "${HOME}/.config/opencode"
|
|
|
|
|
src="${HERE}/opencode.json"
|
|
|
|
|
dst="${HOME}/.config/opencode/opencode.json"
|
|
|
|
|
|
|
|
|
|
# If the user previously had a symlink from the old install.sh, replace it.
|
|
|
|
|
if [[ -L "$dst" ]]; then
|
|
|
|
|
info "removing stale symlink at $dst"
|
|
|
|
|
rm "$dst"
|
|
|
|
|
fi
|
|
|
|
|
# And the old .opencode dir symlink — no longer needed now that plugin
|
|
|
|
|
# paths are absolute.
|
|
|
|
|
if [[ -L "${HOME}/.config/opencode/.opencode" ]]; then
|
|
|
|
|
info "removing stale ~/.config/opencode/.opencode symlink"
|
|
|
|
|
rm "${HOME}/.config/opencode/.opencode"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-10 21:14:43 -04:00
|
|
|
# Rewrite any relative path (./foo, ../foo) to an absolute path rooted at
|
|
|
|
|
# this directory. Applies to both the top-level `plugin` array and to any
|
|
|
|
|
# string inside `mcp.<name>.command[]` (used for serena's --context arg
|
|
|
|
|
# pointing at serena-ide-trim.yml). Absolute paths and npm-package refs
|
|
|
|
|
# pass through untouched.
|
2026-05-08 11:35:10 -04:00
|
|
|
jq --arg here "$HERE" '
|
2026-05-10 21:14:43 -04:00
|
|
|
def rewrite($h):
|
|
|
|
|
if type == "string" and (startswith("./") or startswith("../"))
|
|
|
|
|
then ($h + "/" + ltrimstr("./") | gsub("/\\./"; "/"))
|
|
|
|
|
else .
|
|
|
|
|
end;
|
|
|
|
|
.plugin = ((.plugin // []) | map(rewrite($here)))
|
|
|
|
|
| .mcp = (
|
|
|
|
|
(.mcp // {})
|
|
|
|
|
| with_entries(
|
|
|
|
|
if (.value | type) == "object" and (.value | has("command"))
|
|
|
|
|
then .value.command |= map(rewrite($here))
|
|
|
|
|
else .
|
|
|
|
|
end
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-05-08 11:35:10 -04:00
|
|
|
' "$src" > "$dst.tmp"
|
|
|
|
|
mv "$dst.tmp" "$dst"
|
|
|
|
|
ok "wrote $dst"
|
|
|
|
|
info "plugin paths resolved to:"
|
|
|
|
|
jq -r '.plugin[]?' "$dst" | sed 's/^/ /'
|
2026-05-10 21:14:43 -04:00
|
|
|
info "mcp.serena context resolved to:"
|
|
|
|
|
jq -r '.mcp.serena.command | map(select(test("\\.yml$"))) | .[]?' "$dst" | sed 's/^/ /'
|
2026-05-08 11:35:10 -04:00
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
bold "Done."
|
|
|
|
|
cat <<EOF
|
|
|
|
|
|
|
|
|
|
Re-run this script after editing opencode.json — the deployed copy at
|
|
|
|
|
~/.config/opencode/opencode.json is generated, not symlinked.
|
|
|
|
|
|
|
|
|
|
Next steps:
|
|
|
|
|
- Verify the model server: curl -s http://framework:11434/v1/models | jq '.data[].id'
|
|
|
|
|
- Verify Phoenix is up: curl -sf http://framework:6006/
|
|
|
|
|
- Run opencode and send one prompt. A trace should appear at
|
|
|
|
|
http://framework:6006 within a couple of seconds.
|
|
|
|
|
- If nothing appears, check ~/.local/share/opencode/log/*.log for a
|
|
|
|
|
line starting with "[phoenix-bridge]".
|
|
|
|
|
EOF
|