buddymon/lib/state.sh
pyr0ball f3d1f45253 feat: initial Buddymon plugin
Claude Code plugin — collectible creatures discovered through coding.

- Bug monsters spawn from error output (NullWraith, RacePhantom, ShadowBit, 11 total)
- 5 Buddymon with affinities, challenges, and evolution chains
- SessionStart hook injects active buddy + challenge into system context
- PostToolUse hook detects error patterns, new languages, and commit events
- Stop hook tallies XP and checks challenge completion
- Single /buddymon command with start/assign/fight/catch/roster subcommands
- Local state in ~/.claude/buddymon/ (roster, encounters, active, session)
2026-04-01 15:11:46 -07:00

199 lines
4.5 KiB
Bash

#!/usr/bin/env bash
# Buddymon state management — read/write ~/.claude/buddymon/ JSON files
# Source this file from hook handlers: source "${CLAUDE_PLUGIN_ROOT}/lib/state.sh"
BUDDYMON_DIR="${HOME}/.claude/buddymon"
ROSTER_FILE="${BUDDYMON_DIR}/roster.json"
ENCOUNTERS_FILE="${BUDDYMON_DIR}/encounters.json"
ACTIVE_FILE="${BUDDYMON_DIR}/active.json"
SESSION_FILE="${BUDDYMON_DIR}/session.json"
buddymon_init() {
mkdir -p "${BUDDYMON_DIR}"
if [[ ! -f "${ROSTER_FILE}" ]]; then
cat > "${ROSTER_FILE}" << 'EOF'
{
"_version": 1,
"owned": {},
"starter_chosen": false
}
EOF
fi
if [[ ! -f "${ENCOUNTERS_FILE}" ]]; then
cat > "${ENCOUNTERS_FILE}" << 'EOF'
{
"_version": 1,
"history": [],
"active_encounter": null
}
EOF
fi
if [[ ! -f "${ACTIVE_FILE}" ]]; then
cat > "${ACTIVE_FILE}" << 'EOF'
{
"_version": 1,
"buddymon_id": null,
"challenge": null,
"session_xp": 0
}
EOF
fi
if [[ ! -f "${SESSION_FILE}" ]]; then
buddymon_session_reset
fi
}
buddymon_session_reset() {
local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
cat > "${SESSION_FILE}" << EOF
{
"_version": 1,
"started_at": "${ts}",
"xp_earned": 0,
"tools_used": 0,
"files_touched": [],
"languages_seen": [],
"errors_encountered": [],
"commits_this_session": 0,
"challenge_accepted": false,
"challenge_completed": false
}
EOF
}
buddymon_get_active() {
if [[ -f "${ACTIVE_FILE}" ]]; then
python3 -c "import json; d=json.load(open('${ACTIVE_FILE}')); print(d.get('buddymon_id',''))" 2>/dev/null
fi
}
buddymon_get_session_xp() {
if [[ -f "${ACTIVE_FILE}" ]]; then
python3 -c "import json; d=json.load(open('${ACTIVE_FILE}')); print(d.get('session_xp', 0))" 2>/dev/null
else
echo "0"
fi
}
buddymon_get_roster_entry() {
local id="$1"
if [[ -f "${ROSTER_FILE}" ]]; then
python3 -c "
import json
d=json.load(open('${ROSTER_FILE}'))
entry=d.get('owned',{}).get('${id}')
if entry: print(json.dumps(entry))
" 2>/dev/null
fi
}
buddymon_add_xp() {
local amount="$1"
python3 << EOF
import json, os
active_file = '${ACTIVE_FILE}'
roster_file = '${ROSTER_FILE}'
# Update session XP
with open(active_file) as f:
active = json.load(f)
active['session_xp'] = active.get('session_xp', 0) + ${amount}
buddy_id = active.get('buddymon_id')
with open(active_file, 'w') as f:
json.dump(active, f, indent=2)
# Update roster
if buddy_id and os.path.exists(roster_file):
with open(roster_file) as f:
roster = json.load(f)
if buddy_id in roster.get('owned', {}):
roster['owned'][buddy_id]['xp'] = roster['owned'][buddy_id].get('xp', 0) + ${amount}
with open(roster_file, 'w') as f:
json.dump(roster, f, indent=2)
EOF
}
buddymon_set_active_encounter() {
local encounter_json="$1"
python3 << EOF
import json
enc_file = '${ENCOUNTERS_FILE}'
with open(enc_file) as f:
data = json.load(f)
data['active_encounter'] = ${encounter_json}
with open(enc_file, 'w') as f:
json.dump(data, f, indent=2)
EOF
}
buddymon_clear_active_encounter() {
python3 << EOF
import json
enc_file = '${ENCOUNTERS_FILE}'
with open(enc_file) as f:
data = json.load(f)
data['active_encounter'] = None
with open(enc_file, 'w') as f:
json.dump(data, f, indent=2)
EOF
}
buddymon_log_encounter() {
local encounter_json="$1"
python3 << EOF
import json
from datetime import datetime, timezone
enc_file = '${ENCOUNTERS_FILE}'
with open(enc_file) as f:
data = json.load(f)
entry = ${encounter_json}
entry['timestamp'] = datetime.now(timezone.utc).isoformat()
data.setdefault('history', []).append(entry)
with open(enc_file, 'w') as f:
json.dump(data, f, indent=2)
EOF
}
buddymon_get_active_encounter() {
if [[ -f "${ENCOUNTERS_FILE}" ]]; then
python3 -c "
import json
d=json.load(open('${ENCOUNTERS_FILE}'))
e=d.get('active_encounter')
if e: print(json.dumps(e))
" 2>/dev/null
fi
}
buddymon_starter_chosen() {
python3 -c "import json; d=json.load(open('${ROSTER_FILE}')); print('true' if d.get('starter_chosen') else 'false')" 2>/dev/null
}
buddymon_add_to_roster() {
local buddy_json="$1"
python3 << EOF
import json
roster_file = '${ROSTER_FILE}'
with open(roster_file) as f:
roster = json.load(f)
entry = ${buddy_json}
bid = entry.get('id')
if bid and bid not in roster.get('owned', {}):
entry.setdefault('xp', 0)
entry.setdefault('level', 1)
roster.setdefault('owned', {})[bid] = entry
with open(roster_file, 'w') as f:
json.dump(roster, f, indent=2)
print('added')
else:
print('exists')
EOF
}