Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:56:10 +08:00
commit 400ca062d1
48 changed files with 18674 additions and 0 deletions

230
hooks/pre_tool_use_state_sync.py Executable file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""
PreToolUse State Sync - Git-Powered File State Awareness
Intercepts file operations (Write/Edit/NotebookEdit) and checks if files
changed externally since Claude last read them.
Uses git as source of truth for current state.
**Token Overhead:** ~200-500 tokens (only when files changed externally)
**Blocking:** No (provides feedback, always continues)
Hook Protocol:
- Input: JSON via stdin with tool invocation data
- Output: JSON via stdout with feedback
- IMPORTANT: Never blocks (always {"continue": true})
"""
import json
import subprocess
import sys
from pathlib import Path
from datetime import datetime
def get_file_git_status(file_path: str) -> tuple[bool, str]:
"""
Check if file has uncommitted changes or differs from last commit.
Returns: (has_changes, status_description)
"""
try:
# Check git status for this specific file
result = subprocess.run(
["git", "status", "--short", file_path],
capture_output=True,
text=True,
timeout=1,
)
status = result.stdout.strip()
if not status:
# File unchanged
return False, "unchanged"
# Parse git status codes
# M = Modified (staged)
# _M = Modified (unstaged)
# ?? = Untracked
# A = Added
# D = Deleted
if status.startswith("M") or status.startswith(" M"):
return True, "modified"
elif status.startswith("??"):
return True, "untracked"
elif status.startswith("A"):
return True, "added"
elif status.startswith("D"):
return True, "deleted"
else:
return True, status[:2]
except Exception as e:
print(f"DEBUG: Failed to check git status: {e}", file=sys.stderr)
return False, "unknown"
def get_file_diff_summary(file_path: str) -> str:
"""
Get summary of changes in file since last commit.
Returns: Diff summary (lines added/removed)
"""
try:
# Get diff stat
result = subprocess.run(
["git", "diff", "HEAD", file_path, "--stat"],
capture_output=True,
text=True,
timeout=2,
)
diff_stat = result.stdout.strip()
if not diff_stat:
return "No changes"
# Extract line changes from stat
# Example: "file.py | 10 +++++-----"
return diff_stat
except Exception as e:
print(f"DEBUG: Failed to get diff summary: {e}", file=sys.stderr)
return "Unknown changes"
def check_file_tracked_in_cache(file_path: str) -> tuple[bool, str | None]:
"""
Check if Claude has read this file in current session.
Uses simple cache file to track reads.
Returns: (was_read, last_read_hash)
"""
try:
cache_dir = Path.home() / ".claude" / "plugins" / "contextune" / ".cache"
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / "read_files.json"
if not cache_file.exists():
return False, None
with open(cache_file) as f:
cache = json.load(f)
file_info = cache.get(file_path)
if file_info:
return True, file_info.get("git_hash")
return False, None
except Exception as e:
print(f"DEBUG: Failed to check read cache: {e}", file=sys.stderr)
return False, None
def main():
"""PreToolUse hook entry point."""
try:
# Read hook data from stdin
hook_data = json.loads(sys.stdin.read())
tool_name = hook_data.get("tool_name", "")
tool_input = hook_data.get("tool_input", {})
# DEBUG logging
print(f"DEBUG: PreToolUse state sync for tool: {tool_name}", file=sys.stderr)
# Only intercept file operation tools
if tool_name not in ["Write", "Edit", "NotebookEdit"]:
# Not a file operation, continue without feedback
response = {"continue": True}
print(json.dumps(response))
return
# Get file path from tool input
file_path = tool_input.get("file_path")
if not file_path:
# No file path, continue
response = {"continue": True}
print(json.dumps(response))
return
print(f"DEBUG: Checking state for file: {file_path}", file=sys.stderr)
# Check git status
has_changes, status = get_file_git_status(file_path)
if not has_changes:
# File unchanged, continue without feedback
response = {"continue": True}
print(json.dumps(response))
return
# File has external changes!
print(f"DEBUG: File has external changes: {status}", file=sys.stderr)
# Get diff summary
diff_summary = get_file_diff_summary(file_path)
# Build feedback message
feedback = f"""⚠️ File State Change Detected
**File:** `{file_path}`
**Status:** {status.upper()}
**Git Says:** File has uncommitted changes since last commit
**Diff Summary:**
```
{diff_summary}
```
**Recommendation:**
- ✅ Re-read the file to see current state
- ✅ Use Read tool before {tool_name} to avoid conflicts
**Git Source of Truth:**
The file's current state may differ from what you have in context.
Git tracking shows external modifications.
**Example:**
```bash
# Check current state
git diff {file_path}
# See what changed
git log -1 --oneline -- {file_path}
```
**Note:** Continuing with your {tool_name} operation, but verify file state first!
"""
# Log the detection
print(f"DEBUG: Providing state sync feedback for {file_path}", file=sys.stderr)
# IMPORTANT: Never block, always continue with feedback
response = {
"continue": True,
"feedback": feedback,
"suppressOutput": False # Show feedback to Claude
}
print(json.dumps(response))
except Exception as e:
# Never fail the hook - always continue
print(f"DEBUG: PreToolUse state sync error: {e}", file=sys.stderr)
response = {"continue": True}
print(json.dumps(response))
if __name__ == "__main__":
main()