From 7bd7723cde7b51be9ce2794215e64050c49e9c4e Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Sun, 16 Aug 2026 22:56:33 -0700 Subject: [PATCH 1/3] feat(install): add poolside platform graphify install poolside (and --platform poolside) installs the skill natively into ~/.config/poolside/skills/ (user) or .poolside/skills/ (project), reusing the Claude skill.md body and references/ sidecar. Adds graphify poolside install|uninstall subcommands with scope-safe --project variants. --- CHANGELOG.md | 1 + README.md | 2 + graphify/__main__.py | 6 +- graphify/install.py | 43 ++++++++++++- pyproject.toml | 4 +- tests/test_install.py | 102 +++++++++++++++++++++++++++++++ tests/test_install_references.py | 4 +- 7 files changed, 154 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8188748ae9..d58651da2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.43 (unreleased) +- Feature: `graphify install poolside` (and `graphify install --platform poolside`) natively installs the graphify skill — the same Claude-core `SKILL.md` plus `references/` sidecar previously obtainable only by `graphify install` + rsyncing `~/.claude/skills/graphify` into `~/.config/poolside/skills/graphify`. The Poolside `pool` CLI discovers skills from `~/.config/poolside/skills//` (user) and `.poolside/skills//` (project), so `graphify install poolside` / `graphify poolside install` and their `--project` variants now land the skill natively with no manual copy. `graphify poolside uninstall` removes it (scope-safe, like Pi/Copilot). - 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 bf09a07f82..41b9f236d8 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,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` (also `graphify poolside install`) | | 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. @@ -300,6 +301,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 924ae986d3..0f19671d60 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -507,7 +507,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") @@ -692,6 +692,10 @@ def _run_cli() -> None: 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 fafa8d208a..40e768528b 100644 --- a/graphify/install.py +++ b/graphify/install.py @@ -80,6 +80,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" @@ -349,6 +358,18 @@ 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 byte-identical to Claude's, so + # `graphify install poolside` nukes the old `graphify install` + rsync + # workflow and writes the skill natively to the poolside skills dir. + # Skill-only: no always-on md, hook, or plugin (pool reads SKILL.md from + # the skills directory directly). + "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", @@ -1588,9 +1609,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: @@ -1621,7 +1642,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") @@ -2013,6 +2034,7 @@ def codebuddy_uninstall(project_dir: Path | None = None, *, project: bool = Fals "kiro", "opencode", "pi", + "poolside", "skills", "trae", "trae-cn", @@ -2234,6 +2256,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 72be356a10..dd2e923bb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,11 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" version = "0.9.43" -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 8cea17b4c3..efa7366f66 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",), } @@ -67,6 +70,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" From d9f9bae8c72971b9c205485979541c6c1393df6a Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Sun, 16 Aug 2026 23:12:54 -0700 Subject: [PATCH 2/3] chore: touch-up some wording --- CHANGELOG.md | 2 +- README.md | 2 +- graphify/__main__.py | 5 ++--- graphify/install.py | 6 +----- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d58651da2f..2d79a7952c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.43 (unreleased) -- Feature: `graphify install poolside` (and `graphify install --platform poolside`) natively installs the graphify skill — the same Claude-core `SKILL.md` plus `references/` sidecar previously obtainable only by `graphify install` + rsyncing `~/.claude/skills/graphify` into `~/.config/poolside/skills/graphify`. The Poolside `pool` CLI discovers skills from `~/.config/poolside/skills//` (user) and `.poolside/skills//` (project), so `graphify install poolside` / `graphify poolside install` and their `--project` variants now land the skill natively with no manual copy. `graphify poolside uninstall` removes it (scope-safe, like Pi/Copilot). +- 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 41b9f236d8..49c997dba0 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,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` (also `graphify poolside 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. diff --git a/graphify/__main__.py b/graphify/__main__.py index 0f19671d60..3c766e34b3 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -690,11 +690,10 @@ 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 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 40e768528b..22cbb14b0a 100644 --- a/graphify/install.py +++ b/graphify/install.py @@ -360,11 +360,7 @@ def _skill_registration(skill_path: str = "~/.claude/skills/graphify/SKILL.md") }, "poolside": { # Reuses Claude's lean core (skill.md) and the claude references/ - # sidecar — the Poolside skill body is byte-identical to Claude's, so - # `graphify install poolside` nukes the old `graphify install` + rsync - # workflow and writes the skill natively to the poolside skills dir. - # Skill-only: no always-on md, hook, or plugin (pool reads SKILL.md from - # the skills directory directly). + # 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, From 9fb1a9d43ea039f26b6b0747ff72a6bdaad8de17 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Sun, 16 Aug 2026 23:27:04 -0700 Subject: [PATCH 3/3] chore: bump version in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dd2e923bb2..b7ae4ff018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.9.43" +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, Poolside) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = "Apache-2.0"