Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu

## 0.9.43 (2026-08-14)

- Feature: `graphify install poolside` (and `graphify install --platform poolside`) natively installs the graphify skill.
- Feature: OCaml `.ml`/`.mli` extraction via tree-sitter-ocaml (optional `[ocaml]` extra). Extracts modules, top-level and module-level values/functions, types and their variant constructors, `open` imports, and function calls; qualified calls (`Geo.area`) resolve to the value, and cross-file `open`/call targets collapse onto the unique real definition via the corpus stub rewire.
- Fix: a cross-file INFERRED `uses` edge now binds to the symbol whose body actually references the imported name (a module-level function is a valid source; a co-located class that never touches the import gets no edge), instead of fanning out from the import line to every class in the importing file (#2652, thanks @ousamabenyounes). A reference at module top level, with no enclosing symbol, emits no edge.
- Fix: a named `function` declaration nested inside another function now gets its own node, a `contains` edge from the enclosing function, and its own call scope, so calls made from inside it are no longer dropped as dangling (#2653, thanks @himanshupatro-334). Coverage was extended to the arrow idioms too: a function declared inside an arrow-defined component (`const Panel = () => { function handleClick(){} }`) or inside an arrow callback (`useEffect(() => { function h(){} })`) is captured and attributed to the nearest enclosing named scope.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ for example `graphify claude install --project` or `graphify codex install --pro
| Pi coding agent | `graphify install --platform pi` |
| Cursor | `graphify cursor install` |
| Devin CLI | `graphify devin install` |
| Poolside | `graphify install --platform poolside` |
| Google Antigravity | `graphify antigravity install` |

Codex users also need `multi_agent = true` under `[features]` in `~/.codex/config.toml` for parallel extraction. CodeBuddy uses the same Agent tool and PreToolUse hook mechanism as Claude Code. Factory Droid uses the `Task` tool for parallel subagent dispatch. OpenClaw and Aider use sequential extraction (parallel agent support is still early on those platforms). Trae uses the Agent tool for parallel subagent dispatch and does **not** support `PreToolUse` hooks, so AGENTS.md is the always-on mechanism.
Expand Down Expand Up @@ -301,6 +302,7 @@ Run this once in your project after building a graph:
| Agent Skills (cross-framework) | `graphify agents install` (alias `graphify skills install`) |
| Kiro IDE/CLI | `graphify kiro install` |
| Pi coding agent | `graphify pi install` |
| Poolside | `graphify poolside install` |
| Devin CLI | `graphify devin install` |
| Google Antigravity | `graphify antigravity install` |

Expand Down
5 changes: 4 additions & 1 deletion graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ def _run_cli() -> None:
print("Usage: graphify <command>")
print()
print("Commands:")
print(" install [--platform P] copy skill to platform config dir (claude|windows|codebuddy|codex|opencode|aider|amp|agents|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi|devin)")
print(" install [--platform P] copy skill to platform config dir (claude|windows|codebuddy|codex|opencode|aider|amp|agents|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi|devin|poolside)")
print(" uninstall remove graphify from all detected platforms in one shot")
print(" --purge also delete graphify-out/ directory")
print(" path \"A\" \"B\" shortest path between two nodes in graph.json")
Expand Down Expand Up @@ -689,8 +689,11 @@ def _run_cli() -> None:
print(" kiro uninstall remove skill + steering file")
print(" pi install write skill to ~/.pi/agent/skills/graphify/ (Pi coding agent)")
print(" pi uninstall remove skill from ~/.pi/agent/skills/graphify/")

print(" devin install write skill to ~/.config/devin/skills/graphify/ (Devin CLI)")
print(" devin uninstall remove skill from ~/.config/devin/skills/graphify/")
print(" poolside install write graphify skill to ~/.config/poolside/skills/graphify/ (Poolside)")
print(" poolside uninstall remove graphify skill from ~/.config/poolside/skills/graphify/")
print()
return

Expand Down
39 changes: 36 additions & 3 deletions graphify/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ def _platform_skill_destination(platform_name: str, *, project: bool = False, pr
return (project_dir or Path(".")) / ".opencode" / "skills" / "graphify" / "SKILL.md"
return Path.home() / ".config" / "opencode" / "skills" / "graphify" / "SKILL.md"

if platform_name == "poolside":
# The Poolside `pool` CLI discovers skills from the standard skills
# directories: ~/.config/poolside/skills/<name>/ (user scope) and
# .poolside/skills/<name>/ (project scope). No AGENTS.md/hook/plugin
# wiring is needed — pool loads SKILL.md directly from the skills dir.
if project:
return (project_dir or Path(".")) / ".poolside" / "skills" / "graphify" / "SKILL.md"
return Path.home() / ".config" / "poolside" / "skills" / "graphify" / "SKILL.md"

if platform_name == "hermes":
if project:
return (project_dir or Path(".")) / ".hermes" / "skills" / "graphify" / "SKILL.md"
Expand Down Expand Up @@ -338,6 +347,14 @@ def _skill_registration(skill_path: str = "~/.claude/skills/graphify/SKILL.md")
"claude_md": False,
"skill_refs": "opencode",
},
"poolside": {
# Reuses Claude's lean core (skill.md) and the claude references/
# sidecar — the Poolside skill body is identical to Claude's.
"skill_file": "skill.md",
"skill_dst": Path(".config") / "poolside" / "skills" / "graphify" / "SKILL.md",
"claude_md": False,
"skill_refs": "claude",
},
"kilo": {
"skill_file": "skill-kilo.md",
"skill_dst": Path(".config") / "kilo" / "skills" / "graphify" / "SKILL.md",
Expand Down Expand Up @@ -1585,9 +1602,9 @@ def _project_install(platform_name: str, project_dir: Path | None = None, strict
skill_dst = _copy_skill_file("antigravity", project=True, project_dir=project_dir)
_antigravity_finalize(skill_dst, project_dir)
_print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / ".agents"])
elif platform_name in ("copilot", "pi", "kimi", "agents"):
elif platform_name in ("copilot", "pi", "kimi", "agents", "poolside"):
# Skill-only project install: drop SKILL.md (+ references) at the scope
# root. `agents` -> ./.agents/skills/graphify/SKILL.md.
# root (e.g. `.poolside/skills`, `.agents/skills`, `.pi/agent/skills`).
skill_dst = _copy_skill_file(platform_name, project=True, project_dir=project_dir)
_print_project_git_add_hint([_project_scope_root(skill_dst, project_dir)])
else:
Expand Down Expand Up @@ -1618,7 +1635,7 @@ def _project_uninstall(platform_name: str, project_dir: Path | None = None) -> N
_devin_rules_uninstall(project_dir)
if not removed:
print("nothing to remove")
elif platform_name in ("copilot", "pi", "kimi", "agents"):
elif platform_name in ("copilot", "pi", "kimi", "agents", "poolside"):
removed = _remove_skill_file(platform_name, project=True, project_dir=project_dir)
if not removed:
print("nothing to remove")
Expand Down Expand Up @@ -2010,6 +2027,7 @@ def codebuddy_uninstall(project_dir: Path | None = None, *, project: bool = Fals
"kiro",
"opencode",
"pi",
"poolside",
"skills",
"trae",
"trae-cn",
Expand Down Expand Up @@ -2231,6 +2249,21 @@ def dispatch_install_cli(cmd: str) -> bool:
else:
print("Usage: graphify pi [install|uninstall]", file=sys.stderr)
sys.exit(1)
elif cmd == "poolside":
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
if subcmd == "install":
if "--project" in sys.argv[3:]:
_project_install("poolside", Path("."))
else:
install("poolside")
elif subcmd == "uninstall":
if "--project" in sys.argv[3:]:
_project_uninstall("poolside", Path("."))
else:
_remove_skill_file("poolside")
else:
print("Usage: graphify poolside [install|uninstall]", file=sys.stderr)
sys.exit(1)
elif cmd == "amp":
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
if subcmd == "install":
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "graphifyy"
version = "0.9.45"
description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity, Poolside) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
readme = "README.md"
license = "Apache-2.0"
license-files = ["LICENSE", "LICENSE-MIT", "NOTICE"]
keywords = ["claude", "claude-code", "codex", "opencode", "kilo", "cursor", "gemini", "aider", "kiro", "pi", "devin", "knowledge-graph", "rag", "graphrag", "obsidian", "community-detection", "tree-sitter", "leiden", "llm"]
keywords = ["claude", "claude-code", "codex", "opencode", "kilo", "cursor", "gemini", "aider", "kiro", "pi", "devin", "poolside", "knowledge-graph", "rag", "graphrag", "obsidian", "community-detection", "tree-sitter", "leiden", "llm"]
requires-python = ">=3.10"
dependencies = [
"networkx>=3.4",
Expand Down
102 changes: 102 additions & 0 deletions tests/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from unittest.mock import patch
import pytest

import graphify.__main__ as mainmod


PLATFORMS = {
"claude": (".claude/skills/graphify/SKILL.md",),
Expand All @@ -20,6 +22,7 @@
"trae": (".trae/skills/graphify/SKILL.md",),
"trae-cn": (".trae-cn/skills/graphify/SKILL.md",),
"windows": (".claude/skills/graphify/SKILL.md",),
"poolside": (".config/poolside/skills/graphify/SKILL.md",),
}


Expand Down Expand Up @@ -113,6 +116,105 @@ def test_install_positional_platform_opencode(tmp_path, monkeypatch):
assert not (tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md").exists()


def test_install_poolside(tmp_path):
_install(tmp_path, "poolside")
assert (
tmp_path / ".config" / "poolside" / "skills" / "graphify" / "SKILL.md"
).exists()
# Reuses the claude bundle: references/ sidecar lands alongside SKILL.md.
assert (
tmp_path / ".config" / "poolside" / "skills" / "graphify" / "references"
/ "extraction-spec.md"
).exists()
assert (
tmp_path / ".config" / "poolside" / "skills" / "graphify" / ".graphify_version"
).read_text() == mainmod.__version__


def test_install_positional_platform_poolside(tmp_path, monkeypatch):
from graphify.__main__ import main
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(sys, "argv", ["graphify", "install", "poolside"])
with patch("graphify.__main__.Path.home", return_value=tmp_path):
main()
assert (
tmp_path / ".config" / "poolside" / "skills" / "graphify" / "SKILL.md"
).exists()
# A poolside install must not touch the claude tree.
assert not (tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md").exists()


def test_poolside_install_ships_claude_body(tmp_path):
"""`graphify install poolside` ships the same Claude core (skill.md) body."""
import graphify

pkg = Path(graphify.__file__).parent
_install(tmp_path, "poolside")
installed = (
tmp_path / ".config" / "poolside" / "skills" / "graphify" / "SKILL.md"
)
assert installed.read_bytes() == (pkg / "skill.md").read_bytes()
# and the references/ sidecar ships too (no dead pointers in the body).
assert (installed.parent / "references" / "query.md").exists()


def test_poolside_subcommand_global_install_and_uninstall(tmp_path, monkeypatch):
from graphify.__main__ import main
monkeypatch.chdir(tmp_path)
with patch("graphify.__main__.Path.home", return_value=tmp_path):
monkeypatch.setattr(sys, "argv", ["graphify", "poolside", "install"])
main()
skill = tmp_path / ".config" / "poolside" / "skills" / "graphify" / "SKILL.md"
assert skill.exists()
assert (skill.parent / ".graphify_version").read_text() == mainmod.__version__

monkeypatch.setattr(sys, "argv", ["graphify", "poolside", "uninstall"])
main()
assert not skill.exists()
assert not (skill.parent / ".graphify_version").exists()
# The empty skill tree is walked away.
assert not (tmp_path / ".config" / "poolside").exists()


def test_poolside_project_install_and_uninstall_are_project_scoped(tmp_path, monkeypatch, capsys):
"""`graphify poolside install --project` lands in .poolside/ and spares the user skill."""
from graphify.__main__ import main
home = tmp_path / "home"
project = tmp_path / "project"
project.mkdir()
user_skill = home / ".config" / "poolside" / "skills" / "graphify" / "SKILL.md"
user_skill.parent.mkdir(parents=True)
user_skill.write_text("user skill")
monkeypatch.chdir(project)
monkeypatch.setattr(sys, "argv", ["graphify", "poolside", "install", "--project"])
with patch("graphify.__main__.Path.home", return_value=home):
main()
assert (project / ".poolside" / "skills" / "graphify" / "SKILL.md").exists()
assert user_skill.exists(), "project install must not delete the user-global skill"
assert "git add .poolside/" in capsys.readouterr().out


def test_poolside_project_uninstall_spares_global(tmp_path, monkeypatch):
"""`graphify poolside uninstall --project` removes only the project tree."""
from graphify.__main__ import main
home = tmp_path / "home"
project = tmp_path / "proj"
project.mkdir()
user_skill = home / ".config" / "poolside" / "skills" / "graphify" / "SKILL.md"
user_skill.parent.mkdir(parents=True)
user_skill.write_text("user skill")
proj_skill = project / ".poolside" / "skills" / "graphify" / "SKILL.md"
proj_skill.parent.mkdir(parents=True)
proj_skill.write_text("project skill")
monkeypatch.chdir(project)
monkeypatch.setattr(sys, "argv", ["graphify", "poolside", "uninstall", "--project"])
with patch("graphify.__main__.Path.home", return_value=home):
main()
assert user_skill.exists(), "project uninstall must not delete the user-global skill"
assert not proj_skill.exists()
assert not (project / ".poolside").exists()


def test_install_project_claude_writes_project_scope(tmp_path, monkeypatch, capsys):
from graphify.__main__ import main
home = tmp_path / "home"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_install_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,13 +315,13 @@ def test_gemini_install_references_all_resolve(tmp_path):


def test_claude_twins_ride_the_claude_bundle(tmp_path):
"""antigravity and kimi reuse claude's split bundle, so they go progressive too.
"""antigravity, kimi, and poolside reuse claude's split bundle, so they go progressive too.

gemini has no _PLATFORM_CONFIG entry but installs claude's skill.md body
verbatim; that body is the lean progressive core, so gemini must ride the
claude references bundle as well or it ships a SKILL.md with dead pointers.
"""
for platform in ("antigravity", "kimi", "gemini"):
for platform in ("antigravity", "kimi", "poolside", "gemini"):
refs_src = mainmod._packaged_skill_refs_dir(platform)
assert refs_src is not None
assert refs_src == PKG_DIR / "skills" / "claude" / "references"
Expand Down