#!/usr/bin/env bash # install.sh — install mempalace-toolkit executables + companion agent skill # # Idempotent. Safe to re-run after container recreate. set -euo pipefail # ── locate self ────────────────────────────────────── SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # ── targets ────────────────────────────────────────── BIN_SRC="${SCRIPT_DIR}/bin" BIN_DEST="${HOME}/.local/bin" SKILL_SRC="${SCRIPT_DIR}/SKILL.md" SKILL_DEST_DIR="${HOME}/.agents/skills/opencode-mempalace-bridge" SKILL_DEST="${SKILL_DEST_DIR}/SKILL.md" # pi coding-agent extension (optional — only linked if pi is installed) PI_EXT_SRC="${SCRIPT_DIR}/extensions/pi/mempalace.ts" PI_EXT_DEST_DIR="${HOME}/.pi/agent/extensions" PI_EXT_DEST="${PI_EXT_DEST_DIR}/mempalace.ts" # pi keybindings (generic mosh/tmux newline fix — safe on any machine) PI_KEYS_SRC="${SCRIPT_DIR}/extensions/pi/keybindings.json" PI_KEYS_DEST="${HOME}/.pi/agent/keybindings.json" # pi settings template (NOT symlinked — pi rewrites this file at runtime) PI_SETTINGS_EXAMPLE="${SCRIPT_DIR}/extensions/pi/settings.example.json" PI_SETTINGS_DEST="${HOME}/.pi/agent/settings.json" # ── args ───────────────────────────────────────────── ACTION="install" ASSUME_YES="no" while [[ $# -gt 0 ]]; do case "$1" in --uninstall) ACTION="uninstall"; shift ;; -y|--yes) ASSUME_YES="yes"; shift ;; -h|--help) cat <&2; exit 2 ;; esac done # ── helpers ────────────────────────────────────────── ok() { printf ' \e[32m✓\e[0m %s\n' "$*"; } note() { printf '==> %s\n' "$*"; } warn() { printf ' \e[33m!\e[0m %s\n' "$*" >&2; } err() { printf ' \e[31m✗\e[0m %s\n' "$*" >&2; } confirm() { [[ "$ASSUME_YES" == "yes" ]] && return 0 read -r -p "Proceed? [y/N] " ans [[ "$ans" =~ ^[Yy]$ ]] } link_if_into_repo() { # Return 0 if $1 is a symlink pointing into $SCRIPT_DIR local target [[ -L "$1" ]] || return 1 target=$(readlink -f "$1") [[ "$target" == "$SCRIPT_DIR"/* ]] } # ── install ────────────────────────────────────────── install_bin() { mkdir -p "$BIN_DEST" note "Symlinking bin/ executables into $BIN_DEST" local count=0 local skipped=0 for src in "$BIN_SRC"/*; do [[ -x "$src" && -f "$src" ]] || continue local name; name=$(basename "$src") local dest="$BIN_DEST/$name" if [[ -e "$dest" || -L "$dest" ]]; then if link_if_into_repo "$dest"; then ok "Already linked: $name" count=$((count+1)) continue else # Tell the user exactly what's in the way and how to fix it. local what="real file" if [[ -L "$dest" ]]; then local current_target current_target=$(readlink "$dest") what="symlink → $current_target" fi warn "Skipping $name — $dest already exists ($what)" printf ' If stale (e.g. an old cli_utils install), remove and re-run:\n' printf ' rm %q && %q/install.sh\n' "$dest" "$SCRIPT_DIR" skipped=$((skipped+1)) continue fi fi ln -s "$src" "$dest" ok "Linked $name → $src" count=$((count+1)) done echo ok "Installed $count executable(s)" if (( skipped > 0 )); then warn "$skipped executable(s) skipped — see notes above" fi } install_skill() { note "Linking companion agent skill" mkdir -p "$SKILL_DEST_DIR" # Drop a marker file so sibling tooling (deploy-skills.sh, agents-sync.zsh, # and any future reconciler) can tell at a glance that this skill directory # is owned by an external repo and shouldn't be clobbered. The convention # is two lines: "# skill-source: " + "# url: ". # Any colocated skill from any repo can adopt the same convention. local marker="$SKILL_DEST_DIR/.skill-source" if [[ ! -e "$marker" ]]; then cat > "$marker" </dev/null \ && grep -q 'mempalace-mcp' "$opencode_config" 2>/dev/null; then ok "mempalace MCP server registered in opencode.json" return 0 fi warn "mempalace MCP server NOT registered in $opencode_config" printf ' Without this, opencode loads the skill but no mempalace_* tools\n' printf ' are reachable at runtime, so search/diary/KG calls silently fail.\n' printf ' Add this entry under the top-level "mcp" object:\n' printf ' "mempalace": { "type": "local", "command": ["mempalace-mcp"] }\n' printf ' Or open a fresh config with:\n' printf ' mempalace mcp # prints current recommended snippets\n' printf ' Full details (including Claude Code one-liner) in:\n' printf ' README.md#registering-mempalace-with-opencode-or-other-mcp-clients\n' return 0 } install_pi_extension() { # The pi coding-agent extension is optional: link it only if pi is # already installed on this machine (its ~/.pi/agent/extensions/ # directory exists). Otherwise silently skip — mempalace-toolkit is # useful on opencode-only boxes too. if [[ ! -d "$PI_EXT_DEST_DIR" ]]; then note "pi not detected at $PI_EXT_DEST_DIR — skipping pi extension" printf ' (install pi first if you want the pi↔mempalace bridge:\n' printf ' https://github.com/mariozechner/pi-coding-agent)\n' return 0 fi note "Linking pi extension into $PI_EXT_DEST_DIR" if [[ -e "$PI_EXT_DEST" || -L "$PI_EXT_DEST" ]]; then if link_if_into_repo "$PI_EXT_DEST"; then ok "pi extension already linked" return 0 fi # Non-symlink (or foreign symlink) in the way — back it up rather # than clobber. User may have local edits they want to preserve. local backup="${PI_EXT_DEST}.bak.$(date +%Y%m%d-%H%M%S)" mv "$PI_EXT_DEST" "$backup" warn "Existing $PI_EXT_DEST backed up to $backup" fi ln -s "$PI_EXT_SRC" "$PI_EXT_DEST" ok "Linked mempalace.ts → $PI_EXT_SRC" printf ' Restart pi to load the extension (it reads ~/.pi/agent/extensions/\n' printf ' at startup only).\n' } install_pi_keybindings() { # Generic mosh/tmux newline fix. Non-destructive: if a real # keybindings.json exists we back it up rather than clobber. [[ -d "$PI_EXT_DEST_DIR" ]] || return 0 # no pi → no keybindings note "Linking pi keybindings → $PI_KEYS_DEST" if [[ -e "$PI_KEYS_DEST" || -L "$PI_KEYS_DEST" ]]; then if link_if_into_repo "$PI_KEYS_DEST"; then ok "pi keybindings already linked" return 0 fi local backup="${PI_KEYS_DEST}.bak.$(date +%Y%m%d-%H%M%S)" mv "$PI_KEYS_DEST" "$backup" warn "Existing $PI_KEYS_DEST backed up to $backup" fi ln -s "$PI_KEYS_SRC" "$PI_KEYS_DEST" ok "Linked keybindings.json → $PI_KEYS_SRC" } # ── Verify ~/.pi/agent/settings.json exists ────────────────────────── # If pi is installed but settings.json is missing, `pi` refuses to start # without `--provider ... --model ...` on every invocation. The toolkit # ships extensions/pi/settings.example.json as a template with a working # Bedrock (eu-west-1) stanza — copy + edit for your region/account. # # NOT symlinked: pi rewrites settings.json at runtime (lastChangelogVersion # bumps on upgrade), which would dirty the repo and cause merge noise. # Template-only install is the right trade-off. check_pi_settings() { [[ -d "$PI_EXT_DEST_DIR" ]] || return 0 # no pi → nothing to check if [[ -f "$PI_SETTINGS_DEST" ]]; then ok "pi settings.json present at $PI_SETTINGS_DEST" return 0 fi warn "pi settings.json NOT found at $PI_SETTINGS_DEST" printf ' Without it, pi must be invoked with --provider/--model on every run.\n' printf ' Bootstrap from the shipped template:\n' printf ' cp %q %q\n' "$PI_SETTINGS_EXAMPLE" "$PI_SETTINGS_DEST" printf ' $EDITOR %q # adjust region prefix + model IDs\n' "$PI_SETTINGS_DEST" printf ' See extensions/pi/README.md for the eu./us./anthropic: prefix rules.\n' return 0 } # ── Verify AWS env vars are present for Bedrock-backed pi ──────────── # Only meaningful if pi's settings.json selects amazon-bedrock. We do a # best-effort grep rather than parsing JSON — false positives are cheap # (one extra probe) and the check is gated on pi being installed at all. check_aws_env() { [[ -d "$PI_EXT_DEST_DIR" ]] || return 0 # no pi → nothing to check # Only warn if settings.json selects amazon-bedrock. If pi uses a # non-Bedrock provider (bare anthropic, openai, ...) AWS creds are # irrelevant and this probe would be noise. If settings.json doesn't # exist yet, check_pi_settings already told the user to bootstrap it # — we can't know which provider they'll pick, so stay quiet here. [[ -f "$PI_SETTINGS_DEST" ]] || return 0 grep -q '"amazon-bedrock"' "$PI_SETTINGS_DEST" 2>/dev/null || return 0 if [[ -n "${AWS_PROFILE:-}" && -n "${AWS_REGION:-}" ]]; then ok "AWS env present (AWS_PROFILE=$AWS_PROFILE, AWS_REGION=$AWS_REGION)" return 0 fi warn "AWS_PROFILE and/or AWS_REGION not set in this shell" printf ' pi with defaultProvider=amazon-bedrock needs both to invoke Bedrock.\n' printf ' Recommended layout (matches the tor-ms22 dotfiles pattern):\n' printf ' ~/.config/pi/.env # AWS_PROFILE=..., AWS_REGION=...\n' printf ' ~/.oh-my-zsh/custom/pi-env.zsh # set -a; source ~/.config/pi/.env; set +a\n' printf ' See extensions/pi/README.md#environment-setup for the template.\n' return 0 } do_install() { echo echo "mempalace-toolkit installer" echo "Repository: $SCRIPT_DIR" echo echo "==> Installation plan:" echo " Symlink executables in bin/ into $BIN_DEST" echo " Symlink SKILL.md into $SKILL_DEST" if [[ -d "$PI_EXT_DEST_DIR" ]]; then echo " Symlink extensions/pi/mempalace.ts into $PI_EXT_DEST" echo " Symlink extensions/pi/keybindings.json into $PI_KEYS_DEST" fi echo confirm || { echo "Aborted."; exit 0; } echo install_bin echo install_skill echo install_pi_extension echo install_pi_keybindings echo check_path echo check_wake_up_protocol echo check_opencode_mcp echo check_pi_settings echo check_aws_env echo ok "Done." echo echo "Next: ./bin/mempalace-session --dry-run" echo " or: ./bin/mempalace-docs /path/to/project --dry-run" } # ── uninstall ──────────────────────────────────────── do_uninstall() { echo echo "mempalace-toolkit uninstaller" echo "Repository: $SCRIPT_DIR" echo confirm || { echo "Aborted."; exit 0; } echo note "Removing executable symlinks from $BIN_DEST" local removed=0 for src in "$BIN_SRC"/*; do [[ -x "$src" && -f "$src" ]] || continue local name; name=$(basename "$src") local dest="$BIN_DEST/$name" if link_if_into_repo "$dest"; then rm "$dest" ok "Removed $name" removed=$((removed+1)) fi done ok "Removed $removed executable symlink(s)" echo note "Removing skill symlink" if link_if_into_repo "$SKILL_DEST"; then rm "$SKILL_DEST" ok "Removed skill symlink" else ok "No skill symlink to remove" fi echo note "Removing pi extension symlink" if link_if_into_repo "$PI_EXT_DEST"; then rm "$PI_EXT_DEST" ok "Removed pi extension symlink" else ok "No pi extension symlink to remove" fi echo note "Removing pi keybindings symlink" if link_if_into_repo "$PI_KEYS_DEST"; then rm "$PI_KEYS_DEST" ok "Removed pi keybindings symlink" else ok "No pi keybindings symlink to remove" fi # Remove the marker and the now-empty skill directory, but only if # the marker was written by us and the directory has nothing else in it. local marker="$SKILL_DEST_DIR/.skill-source" if [[ -f "$marker" ]] && grep -q '^# skill-source: mempalace-toolkit$' "$marker" 2>/dev/null; then rm "$marker" ok "Removed $marker" fi if [[ -d "$SKILL_DEST_DIR" ]] && [[ -z "$(ls -A "$SKILL_DEST_DIR" 2>/dev/null)" ]]; then rmdir "$SKILL_DEST_DIR" ok "Removed empty $SKILL_DEST_DIR" fi echo ok "Done." } case "$ACTION" in install) do_install ;; uninstall) do_uninstall ;; esac