From b22c3605c42b4046bb1d9cfe2997314bc9ade5c1 Mon Sep 17 00:00:00 2001 From: Safi Date: Mon, 6 Apr 2026 15:26:58 +0100 Subject: [PATCH] add codex/opencode/claw always-on install, fix README commands and clustering docs --- README.md | 43 ++++++++++++++------- graphify/__main__.py | 90 ++++++++++++++++++++++++++++++++++++++++--- tests/test_install.py | 74 +++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 20b0116a..9b66f67c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ graphify-out/ graphify runs in two passes. First, a deterministic AST pass extracts structure from code files (classes, functions, imports, call graphs, docstrings, rationale comments) with no LLM needed. Second, Claude subagents run in parallel over docs, papers, and images to extract concepts, relationships, and design rationale. The results are merged into a NetworkX graph, clustered with Leiden community detection, and exported as interactive HTML, queryable JSON, and a plain-language audit report. +**Clustering is graph-topology-based — no embeddings.** Leiden finds communities by edge density. The semantic similarity edges that Claude extracts (`semantically_similar_to`, marked INFERRED) are already in the graph, so they influence community detection directly. The graph structure is the similarity signal — no separate embedding step or vector database needed. + Every relationship is tagged `EXTRACTED` (found directly in source), `INFERRED` (reasonable inference, with a confidence score), or `AMBIGUOUS` (flagged for review). You always know what was found vs guessed. ## Install @@ -54,23 +56,22 @@ Then open your AI coding assistant and type: /graphify . ``` -### Make Claude always use the graph (recommended) +### Make your assistant always use the graph (recommended) After building a graph, run this once in your project: -```bash -graphify claude install -``` +| Platform | Command | +|----------|---------| +| Claude Code | `graphify claude install` | +| Codex | `graphify codex install` | +| OpenCode | `graphify opencode install` | +| OpenClaw | `graphify claw install` | -This does two things: +**Claude Code** does two things: writes a `CLAUDE.md` section telling Claude to read `graphify-out/GRAPH_REPORT.md` before answering architecture questions, and installs a **PreToolUse hook** (`settings.json`) that fires before every Glob and Grep call. If a knowledge graph exists, Claude sees: _"graphify: Knowledge graph exists. Read GRAPH_REPORT.md for god nodes and community structure before searching raw files."_ — so Claude navigates via the graph instead of grepping through every file. -1. **CLAUDE.md rules** - tells Claude to read `graphify-out/GRAPH_REPORT.md` before answering architecture questions, and to rebuild the graph after editing code files. +**Codex, OpenCode, OpenClaw** write the same rules to `AGENTS.md` in your project root. These platforms don't support PreToolUse hooks, so AGENTS.md is the always-on mechanism. -2. **PreToolUse hook** (`settings.json`) - fires automatically before every Glob and Grep call. If a knowledge graph exists, Claude sees: _"graphify: Knowledge graph exists. Read GRAPH_REPORT.md for god nodes and community structure before searching raw files."_ This means Claude navigates via the graph instead of grepping through every file - faster answers, fewer wasted tool calls, and responses grounded in the actual structure of your codebase rather than keyword matches. - -Without this, Claude will grep raw files by default even when a graph exists. With it, the graph becomes the first thing Claude reaches for. - -Uninstall with `graphify claude uninstall`. +Uninstall with the matching uninstall command (e.g. `graphify claude uninstall`).
Manual install (curl) @@ -97,12 +98,18 @@ When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` /graphify ./raw # run on a specific folder /graphify ./raw --mode deep # more aggressive INFERRED edge extraction /graphify ./raw --update # re-extract only changed files, merge into existing graph +/graphify ./raw --cluster-only # rerun clustering on existing graph, no re-extraction +/graphify ./raw --no-viz # skip HTML, just produce report + JSON /graphify ./raw --obsidian # also generate Obsidian vault (opt-in) /graphify add https://arxiv.org/abs/1706.03762 # fetch a paper, save, update graph /graphify add https://x.com/karpathy/status/... # fetch a tweet +/graphify add https://... --author "Name" # tag the original author +/graphify add https://... --contributor "Name" # tag who added it to the corpus /graphify query "what connects attention to the optimizer?" +/graphify query "what connects attention to the optimizer?" --dfs # trace a specific path +/graphify query "what connects attention to the optimizer?" --budget 1500 # cap at N tokens /graphify path "DigestAuth" "Response" /graphify explain "SwinTransformer" @@ -111,10 +118,20 @@ When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` /graphify ./raw --svg # export graph.svg /graphify ./raw --graphml # export graph.graphml (Gephi, yEd) /graphify ./raw --neo4j # generate cypher.txt for Neo4j +/graphify ./raw --neo4j-push bolt://localhost:7687 # push directly to a running Neo4j instance /graphify ./raw --mcp # start MCP stdio server -graphify hook install # git hooks - rebuilds graph on commit and branch switch -graphify claude install # always-on: CLAUDE.md + PreToolUse hook for this project +# git hooks - platform-agnostic, rebuild graph on commit and branch switch +graphify hook install +graphify hook uninstall +graphify hook status + +# always-on assistant instructions - platform-specific +graphify claude install # CLAUDE.md + PreToolUse hook (Claude Code) +graphify claude uninstall +graphify codex install # AGENTS.md (Codex) +graphify opencode install # AGENTS.md (OpenCode) +graphify claw install # AGENTS.md (OpenClaw) ``` Works with any mix of file types: diff --git a/graphify/__main__.py b/graphify/__main__.py index cb81d39b..951b661f 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -107,6 +107,71 @@ Rules: _CLAUDE_MD_MARKER = "## graphify" +# AGENTS.md section for Codex, OpenCode, and OpenClaw. +# All three platforms read AGENTS.md in the project root for persistent instructions. +_AGENTS_MD_SECTION = """\ +## graphify + +This project has a graphify knowledge graph at graphify-out/. + +Rules: +- 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 in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current +""" + +_AGENTS_MD_MARKER = "## graphify" + + +def _agents_install(project_dir: Path, platform: str) -> None: + """Write the graphify section to the local AGENTS.md (Codex/OpenCode/OpenClaw).""" + target = (project_dir or Path(".")) / "AGENTS.md" + + if target.exists(): + content = target.read_text() + if _AGENTS_MD_MARKER in content: + print(f"graphify already configured in AGENTS.md") + return + new_content = content.rstrip() + "\n\n" + _AGENTS_MD_SECTION + else: + new_content = _AGENTS_MD_SECTION + + target.write_text(new_content) + print(f"graphify section written to {target.resolve()}") + print() + print(f"{platform.capitalize()} will now check the knowledge graph before answering") + print("codebase questions and rebuild it after code changes.") + print() + print("Note: unlike Claude Code, there is no PreToolUse hook equivalent for") + print(f"{platform.capitalize()} — the AGENTS.md rules are the always-on mechanism.") + + +def _agents_uninstall(project_dir: Path) -> None: + """Remove the graphify section from the local AGENTS.md.""" + target = (project_dir or Path(".")) / "AGENTS.md" + + if not target.exists(): + print("No AGENTS.md found in current directory - nothing to do") + return + + content = target.read_text() + if _AGENTS_MD_MARKER not in content: + print("graphify section not found in AGENTS.md - nothing to do") + return + + cleaned = re.sub( + r"\n*## graphify\n.*?(?=\n## |\Z)", + "", + content, + flags=re.DOTALL, + ).rstrip() + if cleaned: + target.write_text(cleaned + "\n") + print(f"graphify section removed from {target.resolve()}") + else: + target.unlink() + print(f"AGENTS.md was empty after removal - deleted {target.resolve()}") + def claude_install(project_dir: Path | None = None) -> None: """Write the graphify section to the local CLAUDE.md.""" @@ -213,11 +278,17 @@ def main() -> None: print("Commands:") print(" install [--platform P] copy skill to platform config dir (claude|codex|opencode|claw)") 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") - print(" hook status check if hook is installed") - print(" claude install write graphify section to local CLAUDE.md") - print(" claude uninstall remove graphify section from local CLAUDE.md") + print(" hook install install post-commit/post-checkout git hooks (all platforms)") + print(" hook uninstall remove git hooks") + print(" hook status check if git hooks are installed") + print(" claude install write graphify section to CLAUDE.md + PreToolUse hook (Claude Code)") + print(" claude uninstall remove graphify section from CLAUDE.md + PreToolUse hook") + print(" codex install write graphify section to AGENTS.md (Codex)") + print(" codex uninstall remove graphify section from AGENTS.md") + print(" opencode install write graphify section to AGENTS.md (OpenCode)") + print(" opencode uninstall remove graphify section from AGENTS.md") + print(" claw install write graphify section to AGENTS.md (OpenClaw)") + print(" claw uninstall remove graphify section from AGENTS.md") print() return @@ -245,6 +316,15 @@ def main() -> None: else: print("Usage: graphify claude [install|uninstall]", file=sys.stderr) sys.exit(1) + elif cmd in ("codex", "opencode", "claw"): + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + if subcmd == "install": + _agents_install(Path("."), cmd) + elif subcmd == "uninstall": + _agents_uninstall(Path(".")) + else: + print(f"Usage: graphify {cmd} [install|uninstall]", file=sys.stderr) + sys.exit(1) elif cmd == "hook": from graphify.hooks import install as hook_install, uninstall as hook_uninstall, status as hook_status subcmd = sys.argv[2] if len(sys.argv) > 2 else "" diff --git a/tests/test_install.py b/tests/test_install.py index 5cee5904..ac8f24b0 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -83,3 +83,77 @@ def test_claude_install_registers_claude_md(tmp_path): def test_codex_install_does_not_write_claude_md(tmp_path): _install(tmp_path, "codex") assert not (tmp_path / ".claude" / "CLAUDE.md").exists() + + +# --- always-on AGENTS.md install/uninstall tests --- + +def _agents_install(tmp_path, platform): + from graphify.__main__ import _agents_install as _install_fn + _install_fn(tmp_path, platform) + + +def _agents_uninstall(tmp_path): + from graphify.__main__ import _agents_uninstall as _uninstall_fn + _uninstall_fn(tmp_path) + + +def test_codex_agents_install_writes_agents_md(tmp_path): + _agents_install(tmp_path, "codex") + agents_md = tmp_path / "AGENTS.md" + assert agents_md.exists() + assert "graphify" in agents_md.read_text() + assert "GRAPH_REPORT.md" in agents_md.read_text() + + +def test_opencode_agents_install_writes_agents_md(tmp_path): + _agents_install(tmp_path, "opencode") + assert (tmp_path / "AGENTS.md").exists() + + +def test_claw_agents_install_writes_agents_md(tmp_path): + _agents_install(tmp_path, "claw") + assert (tmp_path / "AGENTS.md").exists() + + +def test_agents_install_idempotent(tmp_path): + """Installing twice does not duplicate the section.""" + _agents_install(tmp_path, "codex") + _agents_install(tmp_path, "codex") + content = (tmp_path / "AGENTS.md").read_text() + assert content.count("## graphify") == 1 + + +def test_agents_install_appends_to_existing(tmp_path): + """Installs into an existing AGENTS.md without overwriting other content.""" + agents_md = tmp_path / "AGENTS.md" + agents_md.write_text("# Existing rules\n\nDo not break things.\n") + _agents_install(tmp_path, "codex") + content = agents_md.read_text() + assert "Do not break things." in content + assert "## graphify" in content + + +def test_agents_uninstall_removes_section(tmp_path): + _agents_install(tmp_path, "codex") + _agents_uninstall(tmp_path) + agents_md = tmp_path / "AGENTS.md" + # File deleted when it only contained graphify section + assert not agents_md.exists() + + +def test_agents_uninstall_preserves_other_content(tmp_path): + """Uninstall keeps pre-existing content.""" + agents_md = tmp_path / "AGENTS.md" + agents_md.write_text("# Existing rules\n\nDo not break things.\n") + _agents_install(tmp_path, "codex") + _agents_uninstall(tmp_path) + assert agents_md.exists() + content = agents_md.read_text() + assert "Do not break things." in content + assert "## graphify" not in content + + +def test_agents_uninstall_no_op_when_not_installed(tmp_path, capsys): + _agents_uninstall(tmp_path) + out = capsys.readouterr().out + assert "nothing to do" in out