Injects contextual information when user submits a prompt: - Current time with period (morning/afternoon/evening/night) - Git branch if in a repository - Relevant memory items based on prompt keywords (2+ matches) - Pending decisions needing attention Design: - Skips short prompts (<10 chars) to not slow down commands - 5s timeout to keep prompts responsive - Lightweight keyword matching for memory relevance Also updates general-instructions.json with git workflow notes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
174 lines
4.5 KiB
Python
Executable File
174 lines
4.5 KiB
Python
Executable File
#!/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()
|