mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-24 06:26:11 +00:00
watch: auto-rebuild graph on code changes without LLM, notify on doc/image changes
This commit is contained in:
+11
-4
@@ -23,7 +23,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti
|
||||
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
|
||||
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
|
||||
/graphify <path> --mcp # start MCP stdio server for agent access
|
||||
/graphify <path> --watch # watch folder, notify when files change
|
||||
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
|
||||
/graphify add <url> # fetch URL, save to ./raw, update graph
|
||||
/graphify add <url> --author "Name" # tag who wrote it
|
||||
/graphify add <url> --contributor "Name" # tag who added it to the corpus
|
||||
@@ -1100,15 +1100,22 @@ Supported URL types (auto-detected):
|
||||
|
||||
## For --watch
|
||||
|
||||
Start a background watcher that monitors a folder and auto-reruns `--update` when files change.
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Every time a supported file is added or modified, graphify waits `debounce` seconds (default 3) after the last change, then runs the `--update` pipeline automatically. Press Ctrl+C to stop.
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
|
||||
For the personal inspo use case: leave this running in a terminal. Drop tweets, screenshots, papers, and notes into the folder throughout the day - the graph updates itself.
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
Debounce (default 3s): waits until file activity stops before triggering, so a wave of parallel agent writes doesn't trigger a rebuild per file.
|
||||
|
||||
Press Ctrl+C to stop.
|
||||
|
||||
For agentic workflows: run `--watch` in a background terminal. Code changes from agent waves are picked up automatically between waves. If agents are also writing docs or notes, you'll need a manual `/graphify --update` after those waves.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+99
-5
@@ -1,5 +1,6 @@
|
||||
# monitor a folder and auto-trigger --update when files change
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -11,20 +12,100 @@ _WATCHED_EXTENSIONS = {
|
||||
".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg",
|
||||
}
|
||||
|
||||
_CODE_EXTENSIONS = {
|
||||
".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c", ".rb", ".swift", ".kt",
|
||||
".cs", ".scala", ".php", ".cc", ".cxx", ".hpp", ".h", ".kts",
|
||||
}
|
||||
|
||||
def _run_update(watch_path: Path) -> None:
|
||||
"""Write a flag file and print a notification when files change."""
|
||||
|
||||
def _rebuild_code(watch_path: Path) -> bool:
|
||||
"""Re-run AST extraction + build + cluster + report for code files. No LLM needed.
|
||||
|
||||
Returns True on success, False on error.
|
||||
"""
|
||||
try:
|
||||
from graphify.extract import collect_files, extract
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster, score_all
|
||||
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
|
||||
from graphify.report import generate
|
||||
from graphify.export import to_json
|
||||
|
||||
code_files = []
|
||||
for ext in _CODE_EXTENSIONS:
|
||||
code_files.extend(watch_path.rglob(f"*{ext}"))
|
||||
code_files = [
|
||||
f for f in code_files
|
||||
if not any(part.startswith(".") for part in f.parts)
|
||||
and "graphify-out" not in f.parts
|
||||
and "__pycache__" not in f.parts
|
||||
]
|
||||
|
||||
if not code_files:
|
||||
print("[graphify watch] No code files found - nothing to rebuild.")
|
||||
return False
|
||||
|
||||
result = extract(code_files)
|
||||
|
||||
detection = {
|
||||
"files": {"code": [str(f) for f in code_files], "document": [], "paper": [], "image": []},
|
||||
"total_files": len(code_files),
|
||||
"total_words": sum(len(f.read_text(errors="ignore").split()) for f in code_files),
|
||||
}
|
||||
|
||||
G = build_from_json(result)
|
||||
communities = cluster(G)
|
||||
cohesion = score_all(G, communities)
|
||||
gods = god_nodes(G)
|
||||
surprises = surprising_connections(G, communities)
|
||||
labels = {cid: "Community " + str(cid) for cid in communities}
|
||||
questions = suggest_questions(G, communities, labels)
|
||||
|
||||
out = watch_path / "graphify-out"
|
||||
out.mkdir(exist_ok=True)
|
||||
|
||||
report = generate(G, communities, cohesion, labels, gods, surprises, detection,
|
||||
{"input": 0, "output": 0}, str(watch_path), suggested_questions=questions)
|
||||
(out / "GRAPH_REPORT.md").write_text(report)
|
||||
to_json(G, communities, str(out / "graph.json"))
|
||||
|
||||
# clear stale needs_update flag if present
|
||||
flag = out / "needs_update"
|
||||
if flag.exists():
|
||||
flag.unlink()
|
||||
|
||||
print(f"[graphify watch] Rebuilt: {G.number_of_nodes()} nodes, "
|
||||
f"{G.number_of_edges()} edges, {len(communities)} communities")
|
||||
print(f"[graphify watch] graph.json and GRAPH_REPORT.md updated in {out}")
|
||||
return True
|
||||
|
||||
except Exception as exc:
|
||||
print(f"[graphify watch] Rebuild failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def _notify_only(watch_path: Path) -> None:
|
||||
"""Write a flag file and print a notification (fallback for non-code-only corpora)."""
|
||||
flag = watch_path / "graphify-out" / "needs_update"
|
||||
flag.parent.mkdir(parents=True, exist_ok=True)
|
||||
flag.write_text("1")
|
||||
print(f"\n[graphify watch] New or changed files detected in {watch_path}")
|
||||
print("[graphify watch] Non-code files changed - semantic re-extraction requires LLM.")
|
||||
print("[graphify watch] Run `/graphify --update` in Claude Code to update the graph.")
|
||||
print(f"[graphify watch] Flag written to {flag}")
|
||||
|
||||
|
||||
def _has_non_code(changed_paths: list[Path]) -> bool:
|
||||
return any(p.suffix.lower() not in _CODE_EXTENSIONS for p in changed_paths)
|
||||
|
||||
|
||||
def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
"""
|
||||
Watch watch_path for new or modified files and re-run graphify --update.
|
||||
Watch watch_path for new or modified files and auto-update the graph.
|
||||
|
||||
For code-only changes: re-runs AST extraction + rebuild immediately (no LLM).
|
||||
For doc/paper/image changes: writes a needs_update flag and notifies the user
|
||||
to run /graphify --update (LLM extraction required).
|
||||
|
||||
debounce: seconds to wait after the last change before triggering (avoids
|
||||
running on every keystroke when many files are saved at once).
|
||||
@@ -37,6 +118,7 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
|
||||
last_trigger: float = 0.0
|
||||
pending: bool = False
|
||||
changed: list[Path] = []
|
||||
|
||||
class Handler(FileSystemEventHandler):
|
||||
def on_any_event(self, event):
|
||||
@@ -48,8 +130,12 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
return
|
||||
if any(part.startswith(".") for part in path.parts):
|
||||
return
|
||||
if "graphify-out" in path.parts:
|
||||
return
|
||||
last_trigger = time.monotonic()
|
||||
pending = True
|
||||
if path not in changed:
|
||||
changed.append(path)
|
||||
|
||||
handler = Handler()
|
||||
observer = Observer()
|
||||
@@ -57,14 +143,22 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
observer.start()
|
||||
|
||||
print(f"[graphify watch] Watching {watch_path.resolve()} - press Ctrl+C to stop")
|
||||
print(f"[graphify watch] Debounce: {debounce}s - will update {debounce}s after last change")
|
||||
print(f"[graphify watch] Code changes rebuild graph automatically. "
|
||||
f"Doc/image changes require /graphify --update.")
|
||||
print(f"[graphify watch] Debounce: {debounce}s")
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
if pending and (time.monotonic() - last_trigger) >= debounce:
|
||||
pending = False
|
||||
_run_update(watch_path)
|
||||
batch = list(changed)
|
||||
changed.clear()
|
||||
print(f"\n[graphify watch] {len(batch)} file(s) changed")
|
||||
if _has_non_code(batch):
|
||||
_notify_only(watch_path)
|
||||
else:
|
||||
_rebuild_code(watch_path)
|
||||
except KeyboardInterrupt:
|
||||
print("\n[graphify watch] Stopped.")
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user