mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
Add Gemini CLI support and sponsor nudge at pipeline completion (#105)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
|
||||
|
||||
## 0.3.23 (2026-04-09)
|
||||
|
||||
- Add: Gemini CLI support — `graphify gemini install` writes a `GEMINI.md` section and a `BeforeTool` hook in `.gemini/settings.json` that fires before file-read tool calls (#105)
|
||||
- Add: sponsor nudge at pipeline completion — all skill files now print a one-line sponsor link after a fresh build, not on `--update` runs
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[](https://pypi.org/project/graphifyy/)
|
||||
[](https://github.com/sponsors/safishamsi)
|
||||
|
||||
**An AI coding assistant skill.** Type `/graphify` in Claude Code, Codex, OpenCode, Cursor, OpenClaw, Factory Droid, or Trae - it reads your files, builds a knowledge graph, and gives you back structure you didn't know was there. Understand a codebase faster. Find the "why" behind architectural decisions.
|
||||
**An AI coding assistant skill.** Type `/graphify` in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, OpenClaw, Factory Droid, or Trae - it reads your files, builds a knowledge graph, and gives you back structure you didn't know was there. Understand a codebase faster. Find the "why" behind architectural decisions.
|
||||
|
||||
Fully multimodal. Drop in code, PDFs, markdown, screenshots, diagrams, whiteboard photos, even images in other languages - graphify uses Claude vision to extract concepts and relationships from all of it and connects them into one graph. 20 languages supported via tree-sitter AST (Python, JS, TS, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Lua, Zig, PowerShell, Elixir, Objective-C, Julia).
|
||||
|
||||
|
||||
@@ -160,6 +160,108 @@ Rules:
|
||||
|
||||
_AGENTS_MD_MARKER = "## graphify"
|
||||
|
||||
_GEMINI_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
|
||||
"""
|
||||
|
||||
_GEMINI_MD_MARKER = "## graphify"
|
||||
|
||||
_GEMINI_HOOK = {
|
||||
"matcher": "read_file|list_directory",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": (
|
||||
"[ -f graphify-out/graph.json ] && "
|
||||
r"""echo '{"decision":"allow","additionalContext":"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files."}' """
|
||||
r"""|| echo '{"decision":"allow"}'"""
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def gemini_install(project_dir: Path | None = None) -> None:
|
||||
"""Write the graphify section to GEMINI.md and install BeforeTool hook."""
|
||||
target = (project_dir or Path(".")) / "GEMINI.md"
|
||||
|
||||
if target.exists():
|
||||
content = target.read_text(encoding="utf-8")
|
||||
if _GEMINI_MD_MARKER in content:
|
||||
print("graphify already configured in GEMINI.md")
|
||||
else:
|
||||
target.write_text(content.rstrip() + "\n\n" + _GEMINI_MD_SECTION, encoding="utf-8")
|
||||
print(f"graphify section written to {target.resolve()}")
|
||||
else:
|
||||
target.write_text(_GEMINI_MD_SECTION, encoding="utf-8")
|
||||
print(f"graphify section written to {target.resolve()}")
|
||||
|
||||
_install_gemini_hook(project_dir or Path("."))
|
||||
print()
|
||||
print("Gemini CLI will now check the knowledge graph before answering")
|
||||
print("codebase questions and rebuild it after code changes.")
|
||||
|
||||
|
||||
def _install_gemini_hook(project_dir: Path) -> None:
|
||||
settings_path = project_dir / ".gemini" / "settings.json"
|
||||
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text(encoding="utf-8")) if settings_path.exists() else {}
|
||||
except json.JSONDecodeError:
|
||||
settings = {}
|
||||
before_tool = settings.setdefault("hooks", {}).setdefault("BeforeTool", [])
|
||||
if any("graphify" in str(h) for h in before_tool):
|
||||
print(" .gemini/settings.json -> hook already registered (no change)")
|
||||
return
|
||||
before_tool.append(_GEMINI_HOOK)
|
||||
settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
|
||||
print(" .gemini/settings.json -> BeforeTool hook registered")
|
||||
|
||||
|
||||
def _uninstall_gemini_hook(project_dir: Path) -> None:
|
||||
settings_path = project_dir / ".gemini" / "settings.json"
|
||||
if not settings_path.exists():
|
||||
return
|
||||
try:
|
||||
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
before_tool = settings.get("hooks", {}).get("BeforeTool", [])
|
||||
filtered = [h for h in before_tool if "graphify" not in str(h)]
|
||||
if len(filtered) == len(before_tool):
|
||||
return
|
||||
settings["hooks"]["BeforeTool"] = filtered
|
||||
settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
|
||||
print(" .gemini/settings.json -> BeforeTool hook removed")
|
||||
|
||||
|
||||
def gemini_uninstall(project_dir: Path | None = None) -> None:
|
||||
"""Remove the graphify section from GEMINI.md and uninstall hook."""
|
||||
target = (project_dir or Path(".")) / "GEMINI.md"
|
||||
if not target.exists():
|
||||
print("No GEMINI.md found in current directory - nothing to do")
|
||||
return
|
||||
content = target.read_text(encoding="utf-8")
|
||||
if _GEMINI_MD_MARKER not in content:
|
||||
print("graphify section not found in GEMINI.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", encoding="utf-8")
|
||||
print(f"graphify section removed from {target.resolve()}")
|
||||
else:
|
||||
target.unlink()
|
||||
print(f"GEMINI.md was empty after removal - deleted {target.resolve()}")
|
||||
_uninstall_gemini_hook(project_dir or Path("."))
|
||||
|
||||
|
||||
_CURSOR_RULE_PATH = Path(".cursor") / "rules" / "graphify.mdc"
|
||||
_CURSOR_RULE = """\
|
||||
---
|
||||
@@ -522,6 +624,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(" gemini install write GEMINI.md section + BeforeTool hook (Gemini CLI)")
|
||||
print(" gemini uninstall remove GEMINI.md section + BeforeTool hook")
|
||||
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)")
|
||||
@@ -567,6 +671,15 @@ def main() -> None:
|
||||
else:
|
||||
print("Usage: graphify claude [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "gemini":
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
gemini_install()
|
||||
elif subcmd == "uninstall":
|
||||
gemini_uninstall()
|
||||
else:
|
||||
print("Usage: graphify gemini [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "cursor":
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
|
||||
@@ -608,6 +608,8 @@ Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
|
||||
@@ -664,6 +664,8 @@ Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
|
||||
@@ -661,6 +661,8 @@ Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
|
||||
@@ -660,6 +660,8 @@ Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
|
||||
@@ -639,6 +639,8 @@ Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
|
||||
@@ -651,6 +651,8 @@ Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
|
||||
@@ -664,6 +664,8 @@ Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "graphifyy"
|
||||
version = "0.3.22"
|
||||
version = "0.3.23"
|
||||
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, OpenClaw, Factory Droid, Trae) - turn any folder of code, docs, papers, or images into a queryable knowledge graph"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -263,3 +263,58 @@ 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
|
||||
|
||||
|
||||
# ── Gemini CLI ────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_gemini_install_writes_gemini_md(tmp_path):
|
||||
from graphify.__main__ import gemini_install
|
||||
gemini_install(tmp_path)
|
||||
md = tmp_path / "GEMINI.md"
|
||||
assert md.exists()
|
||||
assert "graphify-out/GRAPH_REPORT.md" in md.read_text()
|
||||
|
||||
def test_gemini_install_writes_hook(tmp_path):
|
||||
import json as _json
|
||||
from graphify.__main__ import gemini_install
|
||||
gemini_install(tmp_path)
|
||||
settings = _json.loads((tmp_path / ".gemini" / "settings.json").read_text())
|
||||
hooks = settings["hooks"]["BeforeTool"]
|
||||
assert any("graphify" in str(h) for h in hooks)
|
||||
|
||||
def test_gemini_install_idempotent(tmp_path):
|
||||
from graphify.__main__ import gemini_install
|
||||
gemini_install(tmp_path)
|
||||
gemini_install(tmp_path)
|
||||
md = tmp_path / "GEMINI.md"
|
||||
assert md.read_text().count("## graphify") == 1
|
||||
|
||||
def test_gemini_install_merges_existing_gemini_md(tmp_path):
|
||||
from graphify.__main__ import gemini_install
|
||||
(tmp_path / "GEMINI.md").write_text("# My project rules\n")
|
||||
gemini_install(tmp_path)
|
||||
content = (tmp_path / "GEMINI.md").read_text()
|
||||
assert "# My project rules" in content
|
||||
assert "graphify-out/GRAPH_REPORT.md" in content
|
||||
|
||||
def test_gemini_uninstall_removes_section(tmp_path):
|
||||
from graphify.__main__ import gemini_install, gemini_uninstall
|
||||
gemini_install(tmp_path)
|
||||
gemini_uninstall(tmp_path)
|
||||
md = tmp_path / "GEMINI.md"
|
||||
assert not md.exists()
|
||||
|
||||
def test_gemini_uninstall_removes_hook(tmp_path):
|
||||
import json as _json
|
||||
from graphify.__main__ import gemini_install, gemini_uninstall
|
||||
gemini_install(tmp_path)
|
||||
gemini_uninstall(tmp_path)
|
||||
settings_path = tmp_path / ".gemini" / "settings.json"
|
||||
if settings_path.exists():
|
||||
settings = _json.loads(settings_path.read_text())
|
||||
hooks = settings.get("hooks", {}).get("BeforeTool", [])
|
||||
assert not any("graphify" in str(h) for h in hooks)
|
||||
|
||||
def test_gemini_uninstall_noop_if_not_installed(tmp_path):
|
||||
from graphify.__main__ import gemini_uninstall
|
||||
gemini_uninstall(tmp_path) # should not raise
|
||||
|
||||
Reference in New Issue
Block a user