#!/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" # ── 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' } 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" fi echo confirm || { echo "Aborted."; exit 0; } echo install_bin echo install_skill echo install_pi_extension echo check_path echo check_wake_up_protocol echo check_opencode_mcp 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 # 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