From a01e0981edd393af71f78a277dd6b75ede8b308e Mon Sep 17 00:00:00 2001 From: Safi Date: Fri, 17 Apr 2026 08:33:02 +0100 Subject: [PATCH] fix #374 #401 #410 #413 #385: vscode install cmd, core.hooksPath, absolute cache root, Windows PATH docs, Kiro YAML colon --- README.md | 3 +++ graphify/__main__.py | 43 +++++++++++++++++++++++++++++++++++++++++++ graphify/cache.py | 2 +- graphify/hooks.py | 34 ++++++++++++++++++++++++++++++---- graphify/skill.md | 2 +- graphify/watch.py | 1 + 6 files changed, 79 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1aebd3372..53f343176 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ pip install graphifyy && graphify install > The PyPI package is temporarily named `graphifyy` while the `graphify` name is being reclaimed. The CLI and skill command are still `graphify`. +> **Windows:** If `graphify` is not recognized after install, add the Python Scripts folder to your PATH: `%APPDATA%\Python\Python3xx\Scripts` (replace `3xx` with your Python version, e.g. `313`). Or use `pipx install graphifyy` which handles PATH automatically. +> **macOS (externally managed):** Use `pipx install graphifyy` if `pip install` fails with an "externally-managed-environment" error. + Then open Claude Code in any directory and type: ``` diff --git a/graphify/__main__.py b/graphify/__main__.py index 8d9b85617..279551d31 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -87,6 +87,41 @@ def claude_install(project_dir: Path | None = None) -> None: print("codebase questions and rebuild it after code changes.") +_COPILOT_INSTRUCTIONS = """\ +## graphify knowledge graph + +This project has a graphify knowledge graph at graphify-out/. + +- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- After modifying code files, run: python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))" +""" + +_COPILOT_MARKER = "## graphify knowledge graph" + + +def vscode_install(project_dir: Path | None = None) -> None: + """Write graphify context to .github/copilot-instructions.md.""" + base = (project_dir or Path(".")).resolve() + github_dir = base / ".github" + github_dir.mkdir(exist_ok=True) + target = github_dir / "copilot-instructions.md" + + if target.exists(): + content = target.read_text() + if _COPILOT_MARKER in content: + print("graphify already configured in .github/copilot-instructions.md") + return + target.write_text(content.rstrip() + "\n\n" + _COPILOT_INSTRUCTIONS) + else: + target.write_text(_COPILOT_INSTRUCTIONS) + + print(f"graphify instructions written to {target}") + print() + print("GitHub Copilot Chat will now use the knowledge graph when answering") + print("questions about this codebase.") + + def claude_uninstall(project_dir: Path | None = None) -> None: """Remove the graphify section from the local CLAUDE.md.""" target = (project_dir or Path(".")) / "CLAUDE.md" @@ -123,6 +158,7 @@ def main() -> None: print() print("Commands:") print(" install copy skill to ~/.claude/skills/ and register in CLAUDE.md") + print(" vscode install write graphify context to .github/copilot-instructions.md") print(" benchmark [graph.json] measure token reduction vs naive full-corpus approach") print(" hook install install post-commit git hook (auto-rebuilds graph on commit)") print(" hook uninstall remove post-commit git hook") @@ -135,6 +171,13 @@ def main() -> None: cmd = sys.argv[1] if cmd == "install": install() + elif cmd == "vscode": + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + if subcmd == "install": + vscode_install() + else: + print("Usage: graphify vscode install", file=sys.stderr) + sys.exit(1) elif cmd == "claude": subcmd = sys.argv[2] if len(sys.argv) > 2 else "" if subcmd == "install": diff --git a/graphify/cache.py b/graphify/cache.py index 0bde3bb7a..8a1572c89 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -13,7 +13,7 @@ def file_hash(path: Path) -> str: def cache_dir(root: Path = Path(".")) -> Path: """Returns graphify-out/cache/ - creates it if needed.""" - d = Path(root) / "graphify-out" / "cache" + d = Path(root).resolve() / "graphify-out" / "cache" d.mkdir(parents=True, exist_ok=True) return d diff --git a/graphify/hooks.py b/graphify/hooks.py index ae01a23f6..0c86f969a 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -1,5 +1,6 @@ # git hook integration - install/uninstall graphify post-commit hook from __future__ import annotations +import subprocess from pathlib import Path _HOOK_MARKER = "# graphify-hook" @@ -53,6 +54,32 @@ def _git_root(path: Path) -> Path | None: return None +def _hooks_dir(root: Path) -> Path: + """Return the active hooks directory for this repo. + + Respects core.hooksPath if set (e.g. repos using Husky). Falls back to + .git/hooks so we never write hooks into the wrong location. + """ + try: + result = subprocess.run( + ["git", "-C", str(root), "config", "core.hooksPath"], + capture_output=True, text=True, + ) + if result.returncode == 0: + custom = result.stdout.strip() + if custom: + p = Path(custom) + if not p.is_absolute(): + p = root / p + p.mkdir(parents=True, exist_ok=True) + return p + except (OSError, FileNotFoundError): + pass + d = root / ".git" / "hooks" + d.mkdir(exist_ok=True) + return d + + def install(path: Path = Path(".")) -> str: """Install graphify post-commit hook in the nearest git repo. @@ -62,8 +89,7 @@ def install(path: Path = Path(".")) -> str: if root is None: raise RuntimeError(f"No git repository found at or above {path.resolve()}") - hooks_dir = root / ".git" / "hooks" - hooks_dir.mkdir(exist_ok=True) + hooks_dir = _hooks_dir(root) hook_path = hooks_dir / "post-commit" if hook_path.exists(): @@ -85,7 +111,7 @@ def uninstall(path: Path = Path(".")) -> str: if root is None: raise RuntimeError(f"No git repository found at or above {path.resolve()}") - hook_path = root / ".git" / "hooks" / "post-commit" + hook_path = _hooks_dir(root) / "post-commit" if not hook_path.exists(): return "No post-commit hook found - nothing to remove." @@ -110,7 +136,7 @@ def status(path: Path = Path(".")) -> str: root = _git_root(path) if root is None: return "Not in a git repository." - hook_path = root / ".git" / "hooks" / "post-commit" + hook_path = _hooks_dir(root) / "post-commit" if not hook_path.exists(): return "graphify hook: not installed" if _HOOK_MARKER in hook_path.read_text(): diff --git a/graphify/skill.md b/graphify/skill.md index 444f080f1..865b34297 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -1,6 +1,6 @@ --- name: graphify -description: any input (code, docs, papers, images) → knowledge graph → clustered communities → HTML + JSON + audit report +description: "any input (code, docs, papers, images) - knowledge graph - clustered communities - HTML + JSON + audit report" trigger: /graphify --- diff --git a/graphify/watch.py b/graphify/watch.py index 2bf96cbcd..d001a39b6 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -23,6 +23,7 @@ def _rebuild_code(watch_path: Path) -> bool: Returns True on success, False on error. """ + watch_path = Path(watch_path).resolve() try: from graphify.extract import collect_files, extract from graphify.build import build_from_json