#!/usr/bin/env python3 """ UserPromptSubmit hook - inject contextual information based on prompt. Injects: - Time-aware context - Current git branch (if in a repo) - Relevant memory items based on prompt keywords - Pending decisions needing attention Output goes to stdout and is added to Claude's context. Keep this fast (<5s) to not slow down prompts. """ import json import os import re import subprocess import sys from datetime import datetime from pathlib import Path # Paths STATE_DIR = Path.home() / ".claude/state/personal-assistant" MEMORY_DIR = STATE_DIR / "memory" def get_time_context() -> str: """Get time-aware greeting context.""" hour = datetime.now().hour if 5 <= hour < 12: period = "morning" elif 12 <= hour < 17: period = "afternoon" elif 17 <= hour < 21: period = "evening" else: period = "night" return f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M')} ({period})" def get_git_context(cwd: str) -> str | None: """Get current git branch if in a repo.""" if not cwd: return None try: result = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, timeout=2, cwd=cwd ) if result.returncode == 0: branch = result.stdout.strip() if branch: return f"Git branch: {branch}" except Exception: pass return None def get_relevant_memory(prompt: str, limit: int = 3) -> list[str]: """Find memory items relevant to the prompt.""" relevant = [] prompt_lower = prompt.lower() # Keywords to look for keywords = set(re.findall(r'\b\w{4,}\b', prompt_lower)) if not keywords: return relevant # Check each memory file for memory_file in ["decisions.json", "preferences.json", "projects.json"]: path = MEMORY_DIR / memory_file if not path.exists(): continue try: with open(path) as f: data = json.load(f) for item in data.get("items", []): content = item.get("content", "").lower() context = item.get("context", "").lower() # Check for keyword matches item_words = set(re.findall(r'\b\w{4,}\b', content + " " + context)) matches = keywords & item_words if len(matches) >= 2: # Require at least 2 matching keywords category = memory_file.replace(".json", "").rstrip("s") relevant.append(f"[{category}] {item.get('content', '')}") if len(relevant) >= limit: return relevant except Exception: continue return relevant def get_pending_decisions(limit: int = 2) -> list[str]: """Get recent pending decisions.""" pending = [] decisions_path = MEMORY_DIR / "decisions.json" if not decisions_path.exists(): return pending try: with open(decisions_path) as f: data = json.load(f) # Get most recent decisions (they might need follow-up) items = data.get("items", []) for item in items[-limit:]: if item.get("status") == "pending": pending.append(f"Pending: {item.get('content', '')}") except Exception: pass return pending def main(): # Read hook input from stdin try: input_data = json.load(sys.stdin) except json.JSONDecodeError: return prompt = input_data.get("prompt", "") cwd = input_data.get("cwd", "") # Skip context injection for very short prompts (likely commands) if len(prompt) < 10: return # Gather context context_parts = [] # Time context (always include) context_parts.append(get_time_context()) # Git context (if in a repo) git_ctx = get_git_context(cwd) if git_ctx: context_parts.append(git_ctx) # Relevant memory (if prompt has substance) if len(prompt) > 20: relevant = get_relevant_memory(prompt) if relevant: context_parts.append("Relevant memory:") context_parts.extend(f" - {item}" for item in relevant) # Pending decisions (occasionally remind) pending = get_pending_decisions() if pending: context_parts.extend(pending) # Output context (will be injected into Claude's context) if context_parts: print("\n".join(context_parts)) if __name__ == "__main__": main()