Each Claude Code session now gets its own state file at: ~/.claude/buddymon/sessions/<pgrp>.json Contains: buddymon_id, session_xp, challenge — all session-local. Global active.json keeps the default buddymon_id for new sessions. /buddymon assign writes to the session file only, so assigning in one terminal window doesn't affect other open sessions. Each window can have its own buddy assigned independently. SessionStart creates the session file (inheriting global default). SessionStop reads XP from it, writes to roster, then removes it.
161 lines
5.7 KiB
Bash
Executable file
161 lines
5.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Buddymon SessionStart hook
|
|
# Initializes state, loads active buddy, injects session context via additionalContext
|
|
|
|
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$(realpath "$0")")")}"
|
|
source "${PLUGIN_ROOT}/lib/state.sh"
|
|
|
|
buddymon_init
|
|
|
|
# Per-session state — keyed by process group ID so parallel sessions are isolated.
|
|
SESSION_KEY=$(python3 -c "import os; print(os.getpgrp())")
|
|
SESSION_FILE="${BUDDYMON_DIR}/sessions/${SESSION_KEY}.json"
|
|
mkdir -p "${BUDDYMON_DIR}/sessions"
|
|
|
|
# Create session file if missing, inheriting buddy from global active.json
|
|
if [[ ! -f "${SESSION_FILE}" ]]; then
|
|
python3 << PYEOF
|
|
import json, os
|
|
active = {}
|
|
try:
|
|
active = json.load(open('${BUDDYMON_DIR}/active.json'))
|
|
except Exception:
|
|
pass
|
|
session_state = {
|
|
"buddymon_id": active.get("buddymon_id"),
|
|
"challenge": active.get("challenge"),
|
|
"session_xp": 0,
|
|
}
|
|
json.dump(session_state, open('${SESSION_FILE}', 'w'), indent=2)
|
|
PYEOF
|
|
fi
|
|
|
|
ACTIVE_ID=$(python3 -c "import json; d=json.load(open('${SESSION_FILE}')); print(d.get('buddymon_id',''))" 2>/dev/null)
|
|
SESSION_XP=$(python3 -c "import json; d=json.load(open('${SESSION_FILE}')); print(d.get('session_xp',0))" 2>/dev/null)
|
|
|
|
# Load catalog for buddy display info
|
|
CATALOG="${PLUGIN_ROOT}/lib/catalog.json"
|
|
|
|
build_context() {
|
|
local ctx=""
|
|
|
|
# ── No starter chosen yet ─────────────────────────────────────────────
|
|
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+='**Starters available:** 🔥 Pyrobyte (Speedrunner) · 🔍 Debuglin (Tester) · ✂️ Minimox (Cleaner)'
|
|
echo "${ctx}"
|
|
return
|
|
fi
|
|
|
|
# ── 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 <name>\` to assign one.\n"
|
|
ctx+="Run \`/buddymon roster\` to see your roster."
|
|
echo "${ctx}"
|
|
return
|
|
fi
|
|
|
|
# ── Active buddy ───────────────────────────────────────────────────────
|
|
local buddy_display buddy_affinity buddy_level buddy_xp
|
|
buddy_display=$(python3 -c "
|
|
import json
|
|
catalog = json.load(open('${CATALOG}'))
|
|
bid = '${ACTIVE_ID}'
|
|
b = catalog.get('buddymon', {}).get(bid) or catalog.get('evolutions', {}).get(bid)
|
|
if b:
|
|
print(b.get('display', bid))
|
|
" 2>/dev/null)
|
|
|
|
local roster_entry
|
|
roster_entry=$(buddymon_get_roster_entry "${ACTIVE_ID}")
|
|
if [[ -n "${roster_entry}" ]]; then
|
|
buddy_level=$(echo "${roster_entry}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('level',1))")
|
|
buddy_xp=$(echo "${roster_entry}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('xp',0))")
|
|
else
|
|
buddy_level=1
|
|
buddy_xp=0
|
|
fi
|
|
|
|
buddy_display="${buddy_display:-${ACTIVE_ID}}"
|
|
|
|
# XP bar (20 chars wide)
|
|
local xp_needed=$(( buddy_level * 100 ))
|
|
local xp_filled=$(( buddy_xp * 20 / xp_needed ))
|
|
[[ ${xp_filled} -gt 20 ]] && xp_filled=20
|
|
local xp_bar=""
|
|
for ((i=0; i<xp_filled; i++)); do xp_bar+="█"; done
|
|
for ((i=xp_filled; i<20; i++)); do xp_bar+="░"; done
|
|
|
|
ctx="## 🐾 Buddymon Active: ${buddy_display}\n"
|
|
ctx+="**Lv.${buddy_level}** XP: [${xp_bar}] ${buddy_xp}/${xp_needed}\n\n"
|
|
|
|
# Active challenge
|
|
local challenge
|
|
challenge=$(python3 -c "
|
|
import json
|
|
f = '${ACTIVE_FILE}'
|
|
d = json.load(open(f))
|
|
ch = d.get('challenge')
|
|
if ch:
|
|
print(ch.get('name','?') + ' — ' + ch.get('description',''))
|
|
" 2>/dev/null)
|
|
|
|
if [[ -n "${challenge}" ]]; then
|
|
ctx+="**Challenge:** 🔥 ${challenge}\n\n"
|
|
fi
|
|
|
|
# Active encounter carry-over
|
|
local enc
|
|
enc=$(buddymon_get_active_encounter)
|
|
if [[ -n "${enc}" ]]; then
|
|
local enc_id enc_display enc_strength
|
|
enc_id=$(echo "${enc}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))")
|
|
enc_display=$(echo "${enc}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('display','?'))")
|
|
enc_strength=$(echo "${enc}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('current_strength',100))")
|
|
ctx+="⚠️ **Unresolved encounter from last session:** ${enc_display} (strength: ${enc_strength}%)\n"
|
|
ctx+="Run \`/buddymon fight\` or \`/buddymon catch\` to resolve it.\n\n"
|
|
fi
|
|
|
|
ctx+="*Bug monsters appear from error output. Use \`/buddymon fight\` or \`/buddymon catch\`.*"
|
|
|
|
echo "${ctx}"
|
|
}
|
|
|
|
# Assign a fresh challenge if none is set
|
|
python3 << PYEOF
|
|
import json, random
|
|
catalog = json.load(open('${CATALOG}'))
|
|
active_file = '${ACTIVE_FILE}'
|
|
active = json.load(open(active_file))
|
|
buddy_id = active.get('buddymon_id')
|
|
if buddy_id and not active.get('challenge'):
|
|
pool = catalog.get('buddymon', {}).get(buddy_id, {}).get('challenges', [])
|
|
if pool:
|
|
active['challenge'] = random.choice(pool)
|
|
json.dump(active, open(active_file, 'w'), indent=2)
|
|
PYEOF
|
|
|
|
CONTEXT=$(build_context)
|
|
|
|
# Escape for JSON
|
|
CONTEXT_JSON=$(python3 -c "
|
|
import json, sys
|
|
print(json.dumps(sys.argv[1]))" "${CONTEXT}" 2>/dev/null)
|
|
|
|
if [[ -z "${CONTEXT_JSON}" ]]; then
|
|
CONTEXT_JSON='"🐾 Buddymon loaded."'
|
|
fi
|
|
|
|
cat << EOF
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "SessionStart",
|
|
"additionalContext": ${CONTEXT_JSON}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
exit 0
|