Navegador

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

Keyboard Shortcuts

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