#!/bin/bash # # Claude Code Hook: user-prompt-submit (v2 - with offline support) # Runs BEFORE each user message is processed # Injects relevant context from the database into the conversation # FALLBACK: Uses local cache when API is unavailable # # Expected environment variables: # CLAUDE_PROJECT_ID - UUID of the current project # JWT_TOKEN - Authentication token for API # CLAUDE_API_URL - API base URL (default: http://172.16.3.30:8001) # CONTEXT_RECALL_ENABLED - Set to "false" to disable (default: true) # MIN_RELEVANCE_SCORE - Minimum score for context (default: 5.0) # MAX_CONTEXTS - Maximum number of contexts to retrieve (default: 10) # # Load configuration if exists CONFIG_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/context-recall-config.env" if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE" fi # Default values API_URL="${CLAUDE_API_URL:-http://172.16.3.30:8001}" ENABLED="${CONTEXT_RECALL_ENABLED:-true}" MIN_SCORE="${MIN_RELEVANCE_SCORE:-5.0}" MAX_ITEMS="${MAX_CONTEXTS:-10}" # Local storage paths CLAUDE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CACHE_DIR="$CLAUDE_DIR/context-cache" QUEUE_DIR="$CLAUDE_DIR/context-queue" # Exit early if disabled if [ "$ENABLED" != "true" ]; then exit 0 fi # Detect project ID from git repo if not set if [ -z "$CLAUDE_PROJECT_ID" ]; then # Try to get from git config PROJECT_ID=$(git config --local claude.projectid 2>/dev/null) if [ -z "$PROJECT_ID" ]; then # Try to derive from git remote URL GIT_REMOTE=$(git config --get remote.origin.url 2>/dev/null) if [ -n "$GIT_REMOTE" ]; then # Hash the remote URL to create a consistent ID PROJECT_ID=$(echo -n "$GIT_REMOTE" | md5sum | cut -d' ' -f1) fi fi else PROJECT_ID="$CLAUDE_PROJECT_ID" fi # Exit if no project ID available if [ -z "$PROJECT_ID" ]; then # Silent exit - no context available exit 0 fi # Create cache directory if it doesn't exist PROJECT_CACHE_DIR="$CACHE_DIR/$PROJECT_ID" mkdir -p "$PROJECT_CACHE_DIR" 2>/dev/null # Try to sync any queued contexts first (opportunistic) if [ -d "$QUEUE_DIR/pending" ] && [ -n "$JWT_TOKEN" ]; then bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1 & fi # Build API request URL RECALL_URL="${API_URL}/api/conversation-contexts/recall" QUERY_PARAMS="project_id=${PROJECT_ID}&limit=${MAX_ITEMS}&min_relevance_score=${MIN_SCORE}" # Try to fetch context from API (with timeout and error handling) API_AVAILABLE=false if [ -n "$JWT_TOKEN" ]; then CONTEXT_RESPONSE=$(curl -s --max-time 3 \ "${RECALL_URL}?${QUERY_PARAMS}" \ -H "Authorization: Bearer ${JWT_TOKEN}" \ -H "Accept: application/json" 2>/dev/null) if [ $? -eq 0 ] && [ -n "$CONTEXT_RESPONSE" ]; then # Check if response is valid JSON (not an error) echo "$CONTEXT_RESPONSE" | python3 -c "import sys, json; json.load(sys.stdin)" 2>/dev/null if [ $? -eq 0 ]; then API_AVAILABLE=true # Save to cache for offline use echo "$CONTEXT_RESPONSE" > "$PROJECT_CACHE_DIR/latest.json" echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > "$PROJECT_CACHE_DIR/last_updated" fi fi fi # Fallback to local cache if API unavailable if [ "$API_AVAILABLE" = "false" ]; then if [ -f "$PROJECT_CACHE_DIR/latest.json" ]; then CONTEXT_RESPONSE=$(cat "$PROJECT_CACHE_DIR/latest.json") CACHE_AGE="unknown" if [ -f "$PROJECT_CACHE_DIR/last_updated" ]; then CACHE_AGE=$(cat "$PROJECT_CACHE_DIR/last_updated") fi echo "" >&2 else # No cache available, exit silently exit 0 fi fi # Parse and format context CONTEXT_COUNT=$(echo "$CONTEXT_RESPONSE" | grep -o '"id"' | wc -l) if [ "$CONTEXT_COUNT" -gt 0 ]; then if [ "$API_AVAILABLE" = "true" ]; then echo "" else echo "" fi echo "" echo "## 📚 Previous Context" echo "" if [ "$API_AVAILABLE" = "false" ]; then echo "⚠️ **Offline Mode** - Using cached context (API unavailable)" echo "" fi echo "The following context has been automatically recalled:" echo "" # Extract and format each context entry echo "$CONTEXT_RESPONSE" | python3 -c " import sys, json try: contexts = json.load(sys.stdin) if isinstance(contexts, list): for i, ctx in enumerate(contexts, 1): title = ctx.get('title', 'Untitled') summary = ctx.get('dense_summary', '') score = ctx.get('relevance_score', 0) ctx_type = ctx.get('context_type', 'unknown') print(f'### {i}. {title} (Score: {score}/10)') print(f'*Type: {ctx_type}*') print() print(summary) print() print('---') print() except: pass " 2>/dev/null echo "" if [ "$API_AVAILABLE" = "true" ]; then echo "*Context automatically injected to maintain continuity across sessions.*" else echo "*Context from local cache - new context will sync when API is available.*" fi echo "" fi # Exit successfully exit 0