mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-28 09:17:02 +00:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c99ac6c2df
commit
df77d5f8ce
@@ -2,6 +2,13 @@
|
||||
|
||||
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
|
||||
|
||||
## 0.3.22 (2026-04-09)
|
||||
|
||||
- Add: Cursor support — `graphify cursor install` writes `.cursor/rules/graphify.mdc` with `alwaysApply: true` so the graph context is always included; `graphify cursor uninstall` removes it (#137)
|
||||
- Fix: `_rebuild_code()` KeyError — `detected[FileType.CODE]` corrected to `detected['files']['code']` matching `detect()`'s actual return shape; was silently breaking git hooks on every commit (#148)
|
||||
- Fix: `to_json()` crash on NetworkX 3.2.x — `node_link_data(G, edges="links")` now falls back to `node_link_data(G)` on older NetworkX, same shim already used for `node_link_graph` (#149)
|
||||
- Fix: README clarifies `graphifyy` is the only official PyPI package — other `graphify*` packages are not affiliated (#129)
|
||||
|
||||
## 0.3.21 (2026-04-09)
|
||||
|
||||
- Fix: Codex PreToolUse hook now places `systemMessage` at the top level of the output JSON instead of inside `hookSpecificOutput` — matches the strict schema enforced by codex-cli 0.118.0+ which uses `additionalProperties: false` (#138)
|
||||
|
||||
@@ -53,7 +53,7 @@ Every relationship is tagged `EXTRACTED` (found directly in source), `INFERRED`
|
||||
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`.
|
||||
> **Official package:** The PyPI package is named `graphifyy` (install with `pip install graphifyy`). Other packages named `graphify*` on PyPI are not affiliated with this project. The only official repository is [safishamsi/graphify](https://github.com/safishamsi/graphify). The CLI and skill command are still `graphify`.
|
||||
|
||||
### Platform support
|
||||
|
||||
@@ -91,6 +91,7 @@ After building a graph, run this once in your project:
|
||||
| Factory Droid | `graphify droid install` |
|
||||
| Trae | `graphify trae install` |
|
||||
| Trae CN | `graphify trae-cn install` |
|
||||
| Cursor | `graphify cursor install` |
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -160,6 +160,45 @@ Rules:
|
||||
|
||||
_AGENTS_MD_MARKER = "## graphify"
|
||||
|
||||
_CURSOR_RULE_PATH = Path(".cursor") / "rules" / "graphify.mdc"
|
||||
_CURSOR_RULE = """\
|
||||
---
|
||||
description: graphify knowledge graph context
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
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 in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current
|
||||
"""
|
||||
|
||||
|
||||
def _cursor_install(project_dir: Path) -> None:
|
||||
"""Write .cursor/rules/graphify.mdc with alwaysApply: true."""
|
||||
rule_path = (project_dir or Path(".")) / _CURSOR_RULE_PATH
|
||||
rule_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if rule_path.exists():
|
||||
print(f"graphify rule already exists at {rule_path} (no change)")
|
||||
return
|
||||
rule_path.write_text(_CURSOR_RULE, encoding="utf-8")
|
||||
print(f"graphify rule written to {rule_path.resolve()}")
|
||||
print()
|
||||
print("Cursor will now always include the knowledge graph context.")
|
||||
print("Run /graphify . first to build the graph if you haven't already.")
|
||||
|
||||
|
||||
def _cursor_uninstall(project_dir: Path) -> None:
|
||||
"""Remove .cursor/rules/graphify.mdc."""
|
||||
rule_path = (project_dir or Path(".")) / _CURSOR_RULE_PATH
|
||||
if not rule_path.exists():
|
||||
print("No graphify Cursor rule found - nothing to do")
|
||||
return
|
||||
rule_path.unlink()
|
||||
print(f"graphify Cursor rule removed from {rule_path.resolve()}")
|
||||
|
||||
|
||||
# OpenCode tool.execute.before plugin — fires before every tool call.
|
||||
# Injects a graph reminder into bash command output when graph.json exists.
|
||||
_OPENCODE_PLUGIN_JS = """\
|
||||
@@ -483,6 +522,8 @@ def main() -> None:
|
||||
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(" cursor install write .cursor/rules/graphify.mdc (Cursor)")
|
||||
print(" cursor uninstall remove .cursor/rules/graphify.mdc")
|
||||
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)")
|
||||
@@ -526,6 +567,15 @@ def main() -> None:
|
||||
else:
|
||||
print("Usage: graphify claude [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "cursor":
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
_cursor_install(Path("."))
|
||||
elif subcmd == "uninstall":
|
||||
_cursor_uninstall(Path("."))
|
||||
else:
|
||||
print("Usage: graphify cursor [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd in ("codex", "opencode", "claw", "droid", "trae", "trae-cn"):
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
|
||||
+4
-1
@@ -284,7 +284,10 @@ def attach_hyperedges(G: nx.Graph, hyperedges: list) -> None:
|
||||
|
||||
def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str) -> None:
|
||||
node_community = _node_community_map(communities)
|
||||
data = json_graph.node_link_data(G, edges="links")
|
||||
try:
|
||||
data = json_graph.node_link_data(G, edges="links")
|
||||
except TypeError:
|
||||
data = json_graph.node_link_data(G)
|
||||
for node in data["nodes"]:
|
||||
node["community"] = node_community.get(node["id"])
|
||||
for link in data["links"]:
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool:
|
||||
"""
|
||||
try:
|
||||
from graphify.extract import extract
|
||||
from graphify.detect import detect, FileType
|
||||
from graphify.detect import detect
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster, score_all
|
||||
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
|
||||
@@ -26,7 +26,7 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool:
|
||||
from graphify.export import to_json
|
||||
|
||||
detected = detect(watch_path, follow_symlinks=follow_symlinks)
|
||||
code_files = [Path(f) for f in detected[FileType.CODE]]
|
||||
code_files = [Path(f) for f in detected['files']['code']]
|
||||
|
||||
if not code_files:
|
||||
print("[graphify watch] No code files found - nothing to rebuild.")
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "graphifyy"
|
||||
version = "0.3.21"
|
||||
version = "0.3.22"
|
||||
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -225,3 +225,41 @@ def test_opencode_agents_uninstall_removes_plugin(tmp_path):
|
||||
if config_file.exists():
|
||||
config = _json.loads(config_file.read_text())
|
||||
assert not any("graphify.js" in p for p in config.get("plugin", []))
|
||||
|
||||
|
||||
# ── Cursor ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_cursor_install_writes_rule(tmp_path):
|
||||
"""cursor install writes .cursor/rules/graphify.mdc."""
|
||||
from graphify.__main__ import _cursor_install
|
||||
_cursor_install(tmp_path)
|
||||
rule = tmp_path / ".cursor" / "rules" / "graphify.mdc"
|
||||
assert rule.exists()
|
||||
content = rule.read_text()
|
||||
assert "alwaysApply: true" in content
|
||||
assert "graphify-out/GRAPH_REPORT.md" in content
|
||||
|
||||
|
||||
def test_cursor_install_idempotent(tmp_path):
|
||||
"""cursor install does not overwrite an existing rule file."""
|
||||
from graphify.__main__ import _cursor_install
|
||||
_cursor_install(tmp_path)
|
||||
rule = tmp_path / ".cursor" / "rules" / "graphify.mdc"
|
||||
original = rule.read_text()
|
||||
_cursor_install(tmp_path)
|
||||
assert rule.read_text() == original
|
||||
|
||||
|
||||
def test_cursor_uninstall_removes_rule(tmp_path):
|
||||
"""cursor uninstall removes the rule file."""
|
||||
from graphify.__main__ import _cursor_install, _cursor_uninstall
|
||||
_cursor_install(tmp_path)
|
||||
_cursor_uninstall(tmp_path)
|
||||
rule = tmp_path / ".cursor" / "rules" / "graphify.mdc"
|
||||
assert not rule.exists()
|
||||
|
||||
|
||||
def test_cursor_uninstall_noop_if_not_installed(tmp_path):
|
||||
"""cursor uninstall does nothing if rule was never written."""
|
||||
from graphify.__main__ import _cursor_uninstall
|
||||
_cursor_uninstall(tmp_path) # should not raise
|
||||
|
||||
Reference in New Issue
Block a user