diff --git a/hooks-handlers/post-tool-use.py b/hooks-handlers/post-tool-use.py index 460b569..284ba69 100755 --- a/hooks-handlers/post-tool-use.py +++ b/hooks-handlers/post-tool-use.py @@ -281,7 +281,6 @@ def main(): if tool_name == "Bash": output = "" if isinstance(tool_response, dict): - # CC may use any of these keys; combine all text fields parts = [ tool_response.get("output", ""), tool_response.get("content", ""), @@ -292,7 +291,6 @@ def main(): elif isinstance(tool_response, str): output = tool_response elif isinstance(tool_response, list): - # Array of content blocks: [{"type": "text", "text": "..."}] output = "\n".join( b.get("text", "") for b in tool_response if isinstance(b, dict) and b.get("type") == "text" @@ -325,10 +323,9 @@ def main(): "defeatable": monster.get("defeatable", True), "xp_reward": monster.get("xp_reward", 50), "weakened_by": [], + "announced": False, } set_active_encounter(encounter) - msg = format_encounter_message(monster, strength, buddy_display) - messages.append(msg) # Commit detection command = tool_input.get("command", "") diff --git a/hooks-handlers/user-prompt-submit.py b/hooks-handlers/user-prompt-submit.py new file mode 100644 index 0000000..3b8a923 --- /dev/null +++ b/hooks-handlers/user-prompt-submit.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Buddymon UserPromptSubmit hook. + +Fires on every user message. Checks for an unannounced active encounter +and surfaces it exactly once via additionalContext, then marks it announced +so the dedup loop breaks. Exits silently if nothing is pending. +""" + +import json +import os +import sys +from pathlib import Path + +PLUGIN_ROOT = os.environ.get("CLAUDE_PLUGIN_ROOT", str(Path(__file__).parent.parent)) +BUDDYMON_DIR = Path.home() / ".claude" / "buddymon" +CATALOG_FILE = Path(PLUGIN_ROOT) / "lib" / "catalog.json" + + +def load_json(path): + try: + with open(path) as f: + return json.load(f) + except Exception: + return {} + + +def save_json(path, data): + try: + with open(path, "w") as f: + json.dump(data, f, indent=2) + except Exception: + pass + + +def main(): + try: + json.load(sys.stdin) + except Exception: + pass + + roster = load_json(BUDDYMON_DIR / "roster.json") + if not roster.get("starter_chosen", False): + sys.exit(0) + + enc_file = BUDDYMON_DIR / "encounters.json" + enc_data = load_json(enc_file) + enc = enc_data.get("active_encounter") + + if not enc or enc.get("announced", False): + sys.exit(0) + + # Mark announced FIRST — prevents re-announce even if output delivery fails + enc["announced"] = True + enc_data["active_encounter"] = enc + save_json(enc_file, enc_data) + + # Resolve buddy display name + active = load_json(BUDDYMON_DIR / "active.json") + buddy_id = active.get("buddymon_id") + buddy_display = "your buddy" + if buddy_id: + catalog = load_json(CATALOG_FILE) + b = (catalog.get("buddymon", {}).get(buddy_id) + or catalog.get("evolutions", {}).get(buddy_id)) + if b: + buddy_display = b.get("display", buddy_id) + else: + catalog = load_json(CATALOG_FILE) + + monster = catalog.get("bug_monsters", {}).get(enc.get("id", ""), {}) + rarity = monster.get("rarity", "common") + rarity_stars = { + "very_common": "★☆☆☆☆", "common": "★★☆☆☆", + "uncommon": "★★★☆☆", "rare": "★★★★☆", "legendary": "★★★★★", + } + stars = rarity_stars.get(rarity, "★★☆☆☆") + strength = enc.get("current_strength", 50) + defeatable = enc.get("defeatable", True) + catchable = enc.get("catchable", True) + flavor = monster.get("flavor", "") + + catchable_str = "[catchable · catch only]" if not defeatable else f"[{rarity} · {'catchable' if catchable else ''}]" + + lines = [ + f"\n💀 **{enc['display']} appeared!** {catchable_str}", + f" Strength: {strength}% · Rarity: {stars}", + ] + if flavor: + lines.append(f" *{flavor}*") + if not defeatable: + lines.append(" ⚠️ CANNOT BE DEFEATED — catch only") + lines += [ + "", + f" **{buddy_display}** is ready to battle!", + "", + " `[FIGHT]` Fix the bug → `/buddymon fight` to claim XP", + " `[CATCH]` Weaken first (test/repro/comment) → `/buddymon catch`", + " `[FLEE]` Ignore → monster grows stronger", + ] + + msg = "\n".join(lines) + print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": msg, + } + })) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/hooks/hooks.json b/hooks/hooks.json index 7d4de6c..7a88b48 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -12,6 +12,17 @@ ] } ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks-handlers/user-prompt-submit.py", + "timeout": 5 + } + ] + } + ], "PostToolUse": [ { "matcher": "Bash|Edit|Write|MultiEdit",