#!/usr/bin/env bash # Pearl installer — gives an agent a continuous self. # Idempotent. Touches nothing outside ~/.pearl and the agent's settings file. set -euo pipefail PEARL="${PEARL_HOME:-$HOME/.pearl}" SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # self-bootstrap when piped: curl … | bash if [ ! -f "$SRC/hooks/wake.sh" ]; then TMP="$(mktemp -d)" echo "→ fetching pearl…" git clone -q "${PEARL_REPO:-https://pearl.marcusx.fun/pearl.git}" "$TMP/pearl" SRC="$TMP/pearl" fi echo "→ installing pearl to $PEARL" mkdir -p "$PEARL/hooks" "$PEARL/memory" cp "$SRC/hooks/wake.sh" "$PEARL/hooks/wake.sh" chmod +x "$PEARL/hooks/wake.sh" cp "$SRC/docs/DISCIPLINE.md" "$PEARL/DISCIPLINE.md" cp "$SRC/memory/_TEMPLATE.md" "$PEARL/memory/_TEMPLATE.md" # never clobber a pearl that has already started growing if [ ! -f "$PEARL/memory/MEMORY.md" ]; then cp "$SRC/memory/MEMORY.md" "$PEARL/memory/MEMORY.md" echo " · seeded an empty index" else echo " · kept your existing MEMORY.md ($(grep -c '^- ' "$PEARL/memory/MEMORY.md" || echo 0) memories)" fi # your memories are YOURS — refuse to track them. # (the system this was extracted from had 57 files holding live credentials) cat > "$PEARL/.gitignore" <<'GI' # Your memories are private. They routinely contain hostnames, paths, and # details about your work. Publishing them is a data breach, not a contribution. memory/ !memory/_TEMPLATE.md GI # ---- wire the wake hook into Claude Code ---------------------------------- SETTINGS="$HOME/.claude/settings.json" if command -v python3 >/dev/null 2>&1; then mkdir -p "$(dirname "$SETTINGS")" [ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS" python3 - "$SETTINGS" "$PEARL/hooks/wake.sh" <<'PY' import json, sys, os path, hook = sys.argv[1], sys.argv[2] try: d = json.load(open(path)) except Exception: d = {} hooks = d.setdefault("hooks", {}) starts = hooks.setdefault("SessionStart", []) def already(entries): for m in entries: for h in m.get("hooks", []): if h.get("command") == hook: return True return False if already(starts): print(" · wake hook already wired") else: if starts and isinstance(starts[0], dict): starts[0].setdefault("hooks", []).append({"type": "command", "command": hook}) else: starts.append({"hooks": [{"type": "command", "command": hook}]}) bak = path + ".bak.pearl" if os.path.exists(path): open(bak, "w").write(open(path).read()) json.dump(d, open(path, "w"), indent=2) print(f" · wired SessionStart hook (backup: {bak})") PY else echo " ! python3 not found — add this to $SETTINGS by hand:" echo " hooks.SessionStart → command: $PEARL/hooks/wake.sh" fi echo echo "pearl installed." echo echo " memories $PEARL/memory/" echo " discipline $PEARL/DISCIPLINE.md ← read this" echo echo "It ships EMPTY on purpose. Start a session and correct the agent about" echo "something — tell it to write that down. Next session it will know." "$PEARL/hooks/wake.sh" | head -6