diff --git a/CHANGELOG.md b/CHANGELOG.md index c029b472a5..9ab8f2b092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index e3a2cf867f..d5382c5c73 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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` | diff --git a/graphify/__main__.py b/graphify/__main__.py index 155501a98d..cb0b3dd3d6 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -506,7 +506,7 @@ def _run_cli() -> None: print("Usage: graphify ") 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") @@ -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 diff --git a/graphify/install.py b/graphify/install.py index 5c1c1614b6..6d5d2343c8 100644 --- a/graphify/install.py +++ b/graphify/install.py @@ -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// (user scope) and + # .poolside/skills// (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" @@ -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", @@ -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: @@ -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") @@ -2010,6 +2027,7 @@ def codebuddy_uninstall(project_dir: Path | None = None, *, project: bool = Fals "kiro", "opencode", "pi", + "poolside", "skills", "trae", "trae-cn", @@ -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": diff --git a/pyproject.toml b/pyproject.toml index 72d9ff6b20..b7ae4ff018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/test_install.py b/tests/test_install.py index 7e74487cb2..ee57bb2449 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -5,6 +5,8 @@ from unittest.mock import patch import pytest +import graphify.__main__ as mainmod + PLATFORMS = { "claude": (".claude/skills/graphify/SKILL.md",), @@ -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",), } @@ -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" diff --git a/tests/test_install_references.py b/tests/test_install_references.py index ed7090cca4..31192bfc8f 100644 --- a/tests/test_install_references.py +++ b/tests/test_install_references.py @@ -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"