diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d21da2..6aeb4c7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## 0.3.14 (2026-04-08) + +- Fix: `graphify codex install` now also writes a PreToolUse hook to `.codex/hooks.json` so the graph reminder fires before every Bash tool call (#86) +- Fix: `--update` now prunes ghost nodes from deleted files before merging new extraction (#51) + ## 0.3.13 (2026-04-08) - Fix: PreToolUse hook now outputs `additionalContext` JSON so Claude actually sees the graph reminder before Glob/Grep calls (#83) diff --git a/graphify/__main__.py b/graphify/__main__.py index c54d5663..6ea4e659 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -150,6 +150,65 @@ Rules: _AGENTS_MD_MARKER = "## graphify" +_CODEX_HOOK = { + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": ( + "[ -f graphify-out/graph.json ] && " + r"""echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files."}}' """ + "|| true" + ), + } + ], + } + ] + } +} + + +def _install_codex_hook(project_dir: Path) -> None: + """Add graphify PreToolUse hook to .codex/hooks.json.""" + hooks_path = project_dir / ".codex" / "hooks.json" + hooks_path.parent.mkdir(parents=True, exist_ok=True) + + if hooks_path.exists(): + try: + existing = json.loads(hooks_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + existing = {} + else: + existing = {} + + pre_tool = existing.setdefault("hooks", {}).setdefault("PreToolUse", []) + if any("graphify" in str(h) for h in pre_tool): + print(f" .codex/hooks.json -> hook already registered (no change)") + return + + pre_tool.extend(_CODEX_HOOK["hooks"]["PreToolUse"]) + hooks_path.write_text(json.dumps(existing, indent=2), encoding="utf-8") + print(f" .codex/hooks.json -> PreToolUse hook registered") + + +def _uninstall_codex_hook(project_dir: Path) -> None: + """Remove graphify PreToolUse hook from .codex/hooks.json.""" + hooks_path = project_dir / ".codex" / "hooks.json" + if not hooks_path.exists(): + return + try: + existing = json.loads(hooks_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return + pre_tool = existing.get("hooks", {}).get("PreToolUse", []) + filtered = [h for h in pre_tool if "graphify" not in str(h)] + existing["hooks"]["PreToolUse"] = filtered + hooks_path.write_text(json.dumps(existing, indent=2), encoding="utf-8") + print(f" .codex/hooks.json -> PreToolUse hook removed") + def _agents_install(project_dir: Path, platform: str) -> None: """Write the graphify section to the local AGENTS.md (Codex/OpenCode/OpenClaw).""" @@ -166,12 +225,17 @@ def _agents_install(project_dir: Path, platform: str) -> None: target.write_text(new_content, encoding="utf-8") print(f"graphify section written to {target.resolve()}") + + if platform == "codex": + _install_codex_hook(project_dir or Path(".")) + 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.") + if platform != "codex": + 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: @@ -363,6 +427,8 @@ def main() -> None: _agents_install(Path("."), cmd) elif subcmd == "uninstall": _agents_uninstall(Path(".")) + if cmd == "codex": + _uninstall_codex_hook(Path(".")) else: print(f"Usage: graphify {cmd} [install|uninstall]", file=sys.stderr) sys.exit(1) diff --git a/graphify/skill.md b/graphify/skill.md index 853f3ac9..35ca6816 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -740,6 +740,14 @@ G_existing = json_graph.node_link_graph(existing_data, edges='links') new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text()) G_new = build_from_json(new_extraction) +# Prune nodes from deleted files +incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text()) +deleted = set(incremental.get('deleted_files', [])) +if deleted: + to_remove = [n for n, d in G_existing.nodes(data=True) if d.get('source_file') in deleted] + G_existing.remove_nodes_from(to_remove) + print(f'Pruned {len(to_remove)} ghost nodes from {len(deleted)} deleted file(s)') + # Merge: new nodes/edges into existing graph G_existing.update(G_new) print(f'Merged: {G_existing.number_of_nodes()} nodes, {G_existing.number_of_edges()} edges') diff --git a/pyproject.toml b/pyproject.toml index 56ef1952..b93f3e75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.3.13" +version = "0.3.14" 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" }