diff --git a/hooks-handlers/session-start.sh b/hooks-handlers/session-start.sh index 45c7727..522dd7c 100755 --- a/hooks-handlers/session-start.sh +++ b/hooks-handlers/session-start.sh @@ -20,7 +20,7 @@ build_context() { if [[ "$(buddymon_starter_chosen)" == "false" ]]; then ctx="## ๐Ÿพ Buddymon โ€” First Encounter!\n\n" ctx+="Thrumble here! You don't have a Buddymon yet. Three starters are waiting.\n\n" - ctx+='Run `/buddymon-start` to choose your starter and begin collecting!\n\n' + ctx+='Run `/buddymon start` to choose your starter and begin collecting!\n\n' ctx+='**Starters available:** ๐Ÿ”ฅ Pyrobyte (Speedrunner) ยท ๐Ÿ” Debuglin (Tester) ยท โœ‚๏ธ Minimox (Cleaner)' echo "${ctx}" return @@ -29,8 +29,8 @@ build_context() { # โ”€โ”€ No buddy assigned to this session โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if [[ -z "${ACTIVE_ID}" ]]; then ctx="## ๐Ÿพ Buddymon\n\n" - ctx+="No buddy assigned to this session. Run \`/buddymon-assign \` to assign one.\n" - ctx+="Run \`/buddymon\` to see your roster." + ctx+="No buddy assigned to this session. Run \`/buddymon assign \` to assign one.\n" + ctx+="Run \`/buddymon roster\` to see your roster." echo "${ctx}" return fi diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..fc9947e --- /dev/null +++ b/install.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# Buddymon install script +# Registers the plugin with Claude Code via symlink (dev-friendly โ€” edits are live). +# +# Usage: +# bash install.sh # install +# bash install.sh --uninstall # remove + +set -euo pipefail + +PLUGIN_NAME="buddymon" +MARKETPLACE="local" +VERSION="0.1.0" +REPO_DIR="$(cd "$(dirname "$0")" && pwd)" + +PLUGINS_DIR="${HOME}/.claude/plugins" +CACHE_DIR="${PLUGINS_DIR}/cache/${MARKETPLACE}/${PLUGIN_NAME}/${VERSION}" +INSTALLED_FILE="${PLUGINS_DIR}/installed_plugins.json" +SETTINGS_FILE="${HOME}/.claude/settings.json" +PLUGIN_KEY="${PLUGIN_NAME}@${MARKETPLACE}" + +# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +die() { echo "โŒ $*" >&2; exit 1; } +info() { echo " $*"; } +ok() { echo "โœ“ $*"; } + +require_python3() { + python3 -c "import json" 2>/dev/null || die "python3 required" +} + +json_get() { + python3 -c " +import json, sys +try: + d = json.load(open('$1')) + keys = '$2'.split('.') + for k in keys: + d = d[k] if isinstance(d, dict) else d[int(k)] + print(json.dumps(d) if isinstance(d, (dict,list)) else d) +except (KeyError, IndexError, FileNotFoundError): + print('') +" 2>/dev/null +} + +# โ”€โ”€ Uninstall โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +uninstall() { + echo "๐Ÿ—‘ Uninstalling ${PLUGIN_KEY}..." + + # Remove symlink / dir from cache + if [[ -L "${CACHE_DIR}" ]] || [[ -d "${CACHE_DIR}" ]]; then + rm -rf "${CACHE_DIR}" + ok "Removed cache entry" + fi + + # Remove from installed_plugins.json + if [[ -f "${INSTALLED_FILE}" ]]; then + python3 << PYEOF +import json +f = '${INSTALLED_FILE}' +d = json.load(open(f)) +key = '${PLUGIN_KEY}' +if key in d.get('plugins', {}): + del d['plugins'][key] + json.dump(d, open(f, 'w'), indent=2) + print(" Removed from installed_plugins.json") +PYEOF + fi + + # Remove from settings.json enabledPlugins + if [[ -f "${SETTINGS_FILE}" ]]; then + python3 << PYEOF +import json +f = '${SETTINGS_FILE}' +d = json.load(open(f)) +key = '${PLUGIN_KEY}' +if key in d.get('enabledPlugins', {}): + del d['enabledPlugins'][key] + json.dump(d, open(f, 'w'), indent=2) + print(" Removed from enabledPlugins") +PYEOF + fi + + echo "" + echo "โœ“ ${PLUGIN_KEY} uninstalled. Restart Claude Code to apply." +} + +# โ”€โ”€ Install โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +install() { + echo "๐Ÿพ Installing ${PLUGIN_KEY}..." + echo "" + + require_python3 + + # Validate plugin structure + [[ -f "${REPO_DIR}/.claude-plugin/plugin.json" ]] \ + || die "Missing .claude-plugin/plugin.json โ€” run from the buddymon repo root" + [[ -f "${REPO_DIR}/hooks/hooks.json" ]] \ + || die "Missing hooks/hooks.json" + + # Create cache parent dir + mkdir -p "$(dirname "${CACHE_DIR}")" + + # Symlink repo into cache (idempotent) + if [[ -L "${CACHE_DIR}" ]]; then + existing=$(readlink "${CACHE_DIR}") + if [[ "${existing}" == "${REPO_DIR}" ]]; then + info "Cache symlink already points to ${REPO_DIR}" + else + rm "${CACHE_DIR}" + ln -s "${REPO_DIR}" "${CACHE_DIR}" + ok "Updated cache symlink โ†’ ${REPO_DIR}" + fi + elif [[ -d "${CACHE_DIR}" ]]; then + die "${CACHE_DIR} exists as a real directory. Remove it first or run --uninstall." + else + ln -s "${REPO_DIR}" "${CACHE_DIR}" + ok "Created cache symlink โ†’ ${REPO_DIR}" + fi + + # Register in installed_plugins.json + python3 << PYEOF +import json, os +from datetime import datetime, timezone + +f = '${INSTALLED_FILE}' +try: + d = json.load(open(f)) +except FileNotFoundError: + d = {"version": 2, "plugins": {}} + +key = '${PLUGIN_KEY}' +now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z') + +d.setdefault('plugins', {})[key] = [{ + "scope": "user", + "installPath": '${CACHE_DIR}', + "version": '${VERSION}', + "installedAt": now, + "lastUpdated": now, + "gitCommitSha": "local", +}] + +json.dump(d, open(f, 'w'), indent=2) +print(" Registered in installed_plugins.json") +PYEOF + + # Enable in settings.json + python3 << PYEOF +import json, os + +f = '${SETTINGS_FILE}' +try: + d = json.load(open(f)) +except FileNotFoundError: + d = {} + +key = '${PLUGIN_KEY}' +d.setdefault('enabledPlugins', {})[key] = True +json.dump(d, open(f, 'w'), indent=2) +print(" Enabled in settings.json") +PYEOF + + # Init state dir + BUDDYMON_DIR="${HOME}/.claude/buddymon" + mkdir -p "${BUDDYMON_DIR}" + ok "Created ${BUDDYMON_DIR}/" + + # Write initial state files if missing + python3 << PYEOF +import json, os +from datetime import datetime, timezone + +d = os.path.expanduser('~/.claude/buddymon') +now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + +files = { + 'roster.json': {"_version": 1, "owned": {}, "starter_chosen": False}, + 'encounters.json': {"_version": 1, "history": [], "active_encounter": None}, + 'active.json': {"_version": 1, "buddymon_id": None, "challenge": None, "session_xp": 0}, + 'session.json': { + "_version": 1, "started_at": now, "xp_earned": 0, "tools_used": 0, + "files_touched": [], "languages_seen": [], "errors_encountered": [], + "commits_this_session": 0, "challenge_accepted": False, "challenge_completed": False, + }, +} + +for name, default in files.items(): + path = os.path.join(d, name) + if not os.path.exists(path): + json.dump(default, open(path, 'w'), indent=2) + print(f" Created {name}") + else: + print(f" {name} already exists โ€” kept") +PYEOF + + echo "" + echo "โœ“ ${PLUGIN_KEY} installed!" + echo "" + echo " Restart Claude Code, then run: /buddymon start" + echo "" + echo " State: ~/.claude/buddymon/" + echo " Logs: hooks write to ~/.claude/buddymon/ JSON โ€” inspect anytime" + echo "" + echo " To uninstall: bash ${REPO_DIR}/install.sh --uninstall" +} + +# โ”€โ”€ Entry point โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +case "${1:-}" in + --uninstall|-u) uninstall ;; + --help|-h) + echo "Usage: bash install.sh [--uninstall]" + echo "" + echo " (no args) Install buddymon plugin (symlink, dev-friendly)" + echo " --uninstall Remove plugin and clean up settings" + ;; + *) install ;; +esac