Navegador

navegador / hooks / claude-hook.py
Source Blame History 98 lines
4be3595… lmata 1 #!/usr/bin/env python3
4be3595… lmata 2 """
4be3595… lmata 3 Navegador hook for Claude Code.
4be3595… lmata 4
4be3595… lmata 5 Fires on PostToolUse to keep the knowledge graph in sync as Claude works:
4be3595… lmata 6 - Tracks files Claude edits or creates
4be3595… lmata 7 - Re-ingests changed files into the graph
4be3595… lmata 8 - Logs decisions and notes Claude produces (when it writes to DECISIONS.md)
4be3595… lmata 9
4be3595… lmata 10 Install:
4be3595… lmata 11 Copy to your project root as .claude/hooks/navegador.py
4be3595… lmata 12 Add to .claude/settings.json:
4be3595… lmata 13
4be3595… lmata 14 {
4be3595… lmata 15 "hooks": {
4be3595… lmata 16 "PostToolUse": [
4be3595… lmata 17 {
4be3595… lmata 18 "matcher": "Edit|Write|Bash",
4be3595… lmata 19 "hooks": [{ "type": "command", "command": "python3 .claude/hooks/navegador.py" }]
4be3595… lmata 20 }
4be3595… lmata 21 ]
4be3595… lmata 22 }
4be3595… lmata 23 }
4be3595… lmata 24 """
4be3595… lmata 25
4be3595… lmata 26 import json
4be3595… lmata 27 import os
4be3595… lmata 28 import subprocess
4be3595… lmata 29 import sys
4be3595… lmata 30
4be3595… lmata 31 NAV_DB = os.environ.get("NAVEGADOR_DB", ".navegador/graph.db")
4be3595… lmata 32 NAV_CMD = os.environ.get("NAVEGADOR_CMD", "navegador")
4be3595… lmata 33
4be3595… lmata 34 # File extensions that navegador can ingest
4be3595… lmata 35 INGESTABLE = {".py", ".ts", ".tsx", ".js", ".jsx"}
4be3595… lmata 36
4be3595… lmata 37
4be3595… lmata 38 def run_nav(*args) -> str:
4be3595… lmata 39 result = subprocess.run(
4be3595… lmata 40 [NAV_CMD, "--db", NAV_DB, *args],
4be3595… lmata 41 capture_output=True, text=True,
4be3595… lmata 42 )
4be3595… lmata 43 return result.stdout.strip()
4be3595… lmata 44
4be3595… lmata 45
4be3595… lmata 46 def main():
4be3595… lmata 47 try:
4be3595… lmata 48 payload = json.loads(sys.stdin.read())
4be3595… lmata 49 except Exception:
4be3595… lmata 50 sys.exit(0)
4be3595… lmata 51
4be3595… lmata 52 tool = payload.get("tool_name", "")
4be3595… lmata 53 inp = payload.get("tool_input", {})
4be3595… lmata 54
4be3595… lmata 55 # Re-ingest any source file Claude edited
4be3595… lmata 56 if tool in ("Edit", "Write"):
4be3595… lmata 57 file_path = inp.get("file_path", "")
4be3595… lmata 58 ext = os.path.splitext(file_path)[1]
4be3595… lmata 59 if ext in INGESTABLE and os.path.exists(file_path):
4be3595… lmata 60 # Ingest just the repo containing this file (fast on small repos)
4be3595… lmata 61 repo_root = _find_repo_root(file_path)
4be3595… lmata 62 if repo_root:
4be3595… lmata 63 run_nav("ingest", repo_root)
4be3595… lmata 64
4be3595… lmata 65 # Watch for DECISIONS.md updates — extract new entries into the graph
4be3595… lmata 66 if tool in ("Edit", "Write"):
4be3595… lmata 67 if inp.get("file_path", "").endswith("DECISIONS.md"):
4be3595… lmata 68 _sync_decisions()
4be3595… lmata 69
4be3595… lmata 70
4be3595… lmata 71 def _find_repo_root(path: str) -> str | None:
4be3595… lmata 72 """Walk up to find the git root."""
4be3595… lmata 73 d = os.path.dirname(os.path.abspath(path))
4be3595… lmata 74 while d != os.path.dirname(d):
4be3595… lmata 75 if os.path.exists(os.path.join(d, ".git")):
4be3595… lmata 76 return d
4be3595… lmata 77 d = os.path.dirname(d)
4be3595… lmata 78 return None
4be3595… lmata 79
4be3595… lmata 80
4be3595… lmata 81 def _sync_decisions():
4be3595… lmata 82 """Parse DECISIONS.md and upsert Decision nodes."""
4be3595… lmata 83 if not os.path.exists("DECISIONS.md"):
4be3595… lmata 84 return
4be3595… lmata 85 content = open("DECISIONS.md").read()
4be3595… lmata 86 # Simple heuristic: ## headings are decision names
4be3595… lmata 87 import re
4be3595… lmata 88 for match in re.finditer(r"^##\s+(.+)", content, re.MULTILINE):
4be3595… lmata 89 name = match.group(1).strip()
4be3595… lmata 90 # Find the body until the next heading
4be3595… lmata 91 start = match.end()
4be3595… lmata 92 next_h = re.search(r"^##", content[start:], re.MULTILINE)
4be3595… lmata 93 body = content[start: start + next_h.start() if next_h else len(content)].strip()
4be3595… lmata 94 run_nav("add", "decision", name, "--desc", body[:500])
4be3595… lmata 95
4be3595… lmata 96
4be3595… lmata 97 if __name__ == "__main__":
4be3595… lmata 98 main()

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button