add codex/opencode/claw always-on install, fix README commands and clustering docs

This commit is contained in:
Safi
2026-04-06 15:26:58 +01:00
parent 472d9e1e27
commit b22c3605c4
3 changed files with 189 additions and 18 deletions
+30 -13
View File
@@ -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`).
<details>
<summary>Manual install (curl)</summary>
@@ -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:
+85 -5
View File
@@ -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 ""
+74
View File
@@ -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