diff --git a/README.md b/README.md index 81685cf72..155aa7f70 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ for example `graphify claude install --project` or `graphify codex install --pro | Agent Skills (cross-framework) | `graphify install --platform agents` (alias `--platform skills`) | | Kiro IDE/CLI | `graphify kiro install` | | Pi coding agent | `graphify install --platform pi` | +| Oh My Pi (OMP) | `graphify omp install` | | Cursor | `graphify cursor install` | | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | @@ -291,12 +292,13 @@ 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` | +| Oh My Pi (OMP) | `graphify omp install` | | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | This writes a small config file that tells your assistant to consult the knowledge graph for codebase questions, preferring scoped queries like `graphify query ""` over reading the full report or grepping raw files. -- **Hook platforms** (Claude Code, Gemini CLI): a hook fires automatically before search-style tool calls (and, on Claude Code, before reading source files one by one via the Read/Glob tools) and nudges your assistant toward the graph path. +- **Hook platforms** (Claude Code, Gemini CLI, Oh My Pi): a hook fires automatically before search-style tool calls (and, on Claude Code, before reading source files one by one via the Read/Glob tools) and nudges your assistant toward the graph path. - **Instruction-file platforms** (Codex, OpenCode, Cursor, etc.): persistent instruction files (`AGENTS.md`, `.cursor/rules/`, etc.) provide the same query-first guidance. `GRAPH_REPORT.md` is still available for broad architecture review. @@ -688,6 +690,8 @@ graphify kiro install # .kiro/skills/ + .kiro/steering/graphify.md graphify kiro uninstall graphify pi install # skill file (Pi coding agent) graphify pi uninstall +graphify omp install # skill file + OMP extension (Oh My Pi) +graphify omp uninstall graphify devin install # skill file + .windsurf/rules/graphify.md (Devin CLI) graphify devin uninstall graphify antigravity install # .agents/rules + .agents/workflows (Google Antigravity) diff --git a/graphify/install.py b/graphify/install.py index b34290eec..674937bbc 100644 --- a/graphify/install.py +++ b/graphify/install.py @@ -392,6 +392,12 @@ def _skill_registration(skill_path: str = "~/.claude/skills/graphify/SKILL.md") "claude_md": False, "skill_refs": "pi", }, + "omp": { + "skill_file": "skill-agents.md", + "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + "skill_refs": "agents", + }, "codebuddy": { # Reuses claude's split bundle (shares skill.md). "skill_file": "skill.md", @@ -608,6 +614,10 @@ def install(platform: str = "claude", *, project: bool = False, project_dir: Pat if platform == "opencode": _install_opencode_plugin(project_dir if project else Path(".")) + if platform == "omp": + _omp_install(project_dir, project=project) + return + # Refresh version stamps in all other previously-installed skill dirs so # stale-version warnings don't fire for platforms not explicitly re-installed. if project: @@ -1285,6 +1295,130 @@ def _uninstall_opencode_plugin(project_dir: Path) -> None: config.pop("plugin") config_file.write_text(json.dumps(config, indent=2), encoding="utf-8") print(f" {_OPENCODE_CONFIG_PATH} -> plugin deregistered") + +# OMP (Oh My Pi) extension — loaded by the OMP extension runner as a native +# extension module. It listens to tool_result events and prepends a graphify +# nudge to grep/search/read/glob results when a knowledge graph exists. +_OMP_EXTENSION_TS = r"""import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +const GRAPH_PATH = "graphify-out/graph.json"; + +const SEARCH_NUDGE = + 'graphify: graphify-out/graph.json exists. For focused questions, run graphify query "" before grepping raw files. Only grep after graphify has oriented you, or to modify/debug specific lines.'; +const READ_NUDGE = + 'graphify: graphify-out/graph.json exists. For focused questions, run graphify query/explain/path before reading source files. Only read raw files after graphify has oriented you, or to modify/debug specific lines.'; + +const SOURCE_EXTS = new Set([ + ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", + ".py", ".go", ".rs", ".java", ".kt", ".scala", + ".c", ".cpp", ".h", ".hpp", ".cs", ".rb", + ".swift", ".php", ".sh", ".bash", ".zsh", + ".md", ".json", ".yaml", ".yml", ".toml", +]); + +function isUnderGraphifyOut(path: string): boolean { + return path.toLowerCase().includes("graphify-out/"); +} + +function isSourceFile(path: string): boolean { + const lower = path.toLowerCase(); + if (isUnderGraphifyOut(lower)) return false; + const dot = lower.lastIndexOf("."); + if (dot === -1) return false; + return SOURCE_EXTS.has(lower.slice(dot)); +} + +export default function graphifyOmp(pi: ExtensionAPI): void { + pi.setLabel("graphify guard"); + + pi.on("tool_result", async (event, ctx) => { + const graphPath = resolve(ctx.cwd, GRAPH_PATH); + if (!existsSync(graphPath)) return; + + let nudge = ""; + if (event.toolName === "bash") { + const cmd = String(event.input?.command ?? ""); + if (/\b(grep|ripgrep|rg |find |fd |ack |ag )\b/.test(cmd)) { + nudge = SEARCH_NUDGE; + } + } else if (event.toolName === "read" || event.toolName === "glob") { + const input = (event.input ?? {}) as any; + const path = String(input.path ?? input.file_path ?? input.pattern ?? ""); + if (isSourceFile(path)) { + nudge = READ_NUDGE; + } + } + + if (!nudge) return; + + const content = [{ type: "text", text: nudge }, ...(event.content ?? [])]; + return { content }; + }); +} +""" + +_OMP_EXTENSION_PATH = Path(".omp") / "agent" / "extensions" / "graphify.ts" +_OMP_PROJECT_EXTENSION_PATH = Path(".omp") / "extensions" / "graphify.ts" + + +def _install_omp_extension(project_dir: Path | None = None, *, project: bool = False) -> None: + """Write graphify OMP extension to the native extension directory. + + OMP auto-discovers extensions from ~/.omp/agent/extensions (user scope) and + /.omp/extensions (project scope), so no settings registration is needed. + """ + if project: + ext_path = (project_dir or Path(".")) / _OMP_PROJECT_EXTENSION_PATH + else: + ext_path = Path.home() / _OMP_EXTENSION_PATH + ext_path.parent.mkdir(parents=True, exist_ok=True) + ext_path.write_text(_OMP_EXTENSION_TS, encoding="utf-8") + print(f" {ext_path} -> OMP extension written") + + +def _uninstall_omp_extension(project_dir: Path | None = None, *, project: bool = False) -> None: + """Remove graphify OMP extension from the native extension directory.""" + if project: + ext_path = (project_dir or Path(".")) / _OMP_PROJECT_EXTENSION_PATH + else: + ext_path = Path.home() / _OMP_EXTENSION_PATH + if ext_path.exists(): + ext_path.unlink() + print(f" {ext_path} -> removed") + + +def _omp_install(project_dir: Path | None = None, *, project: bool = False) -> None: + """Install graphify skill and OMP extension.""" + skill_dst = _copy_skill_file("omp", project=project, project_dir=project_dir) + _install_omp_extension(project_dir, project=project) + if project: + _agents_install(project_dir or Path("."), "omp") + _print_project_git_add_hint( + [ + _project_scope_root(skill_dst, project_dir or Path(".")), + (project_dir or Path(".")) / ".omp", + (project_dir or Path(".")) / "AGENTS.md", + ] + ) + else: + _refresh_all_version_stamps() + print() + print("Oh My Pi (OMP) will now check the knowledge graph before answering") + print("codebase questions and rebuild it after code changes.") + + +def _omp_uninstall(project_dir: Path | None = None, *, project: bool = False) -> None: + """Remove graphify skill and OMP extension.""" + removed = _remove_skill_file("omp", project=project, project_dir=project_dir) + _uninstall_omp_extension(project_dir, project=project) + if project: + _agents_uninstall(project_dir or Path("."), "omp") + if not removed: + print("nothing to remove") + + def _resolve_graphify_exe() -> str: """Return the absolute path to the graphify executable. @@ -1373,11 +1507,12 @@ def _agents_install(project_dir: Path, platform: str) -> None: _install_kilo_plugin(project_dir or Path(".")) print() + display_name = "Oh My Pi (OMP)" if platform == "omp" else platform.capitalize() print( - f"{platform.capitalize()} will now check the knowledge graph before answering" + f"{display_name} will now check the knowledge graph before answering" ) print("codebase questions and rebuild it after code changes.") - if platform not in ("codex", "opencode", "kilo"): + if platform not in ("codex", "opencode", "kilo", "omp"): print() print("Note: unlike Claude Code, there is no PreToolUse hook equivalent for") print( @@ -1497,6 +1632,8 @@ def _project_uninstall(platform_name: str, project_dir: Path | None = None) -> N removed = _remove_skill_file(platform_name, project=True, project_dir=project_dir) if not removed: print("nothing to remove") + elif platform_name == "omp": + _omp_uninstall(project_dir, project=True) elif platform_name == "codebuddy": codebuddy_uninstall(project_dir) else: @@ -1679,6 +1816,9 @@ def uninstall_all(project_dir: Path | None = None, purge: bool = False) -> None: _remove_skill_file("agents") _uninstall_opencode_plugin(pd) _uninstall_codex_hook(pd) + _uninstall_omp_extension(pd, project=True) + _uninstall_omp_extension(pd, project=False) + _remove_skill_file("omp") # Git hook try: @@ -1872,6 +2012,7 @@ def codebuddy_uninstall(project_dir: Path | None = None, *, project: bool = Fals "kilo", "kiro", "opencode", + "omp", "pi", "skills", "trae", @@ -2068,6 +2209,21 @@ def dispatch_install_cli(cmd: str) -> bool: else: print("Usage: graphify devin [install|uninstall]", file=sys.stderr) sys.exit(1) + elif cmd == "omp": + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + if subcmd == "install": + if "--project" in sys.argv[3:]: + _omp_install(Path("."), project=True) + else: + _omp_install(project=False) + elif subcmd == "uninstall": + if "--project" in sys.argv[3:]: + _omp_uninstall(Path("."), project=True) + else: + _omp_uninstall(project=False) + else: + print("Usage: graphify omp [install|uninstall]", file=sys.stderr) + sys.exit(1) elif cmd == "pi": subcmd = sys.argv[2] if len(sys.argv) > 2 else "" if subcmd == "install": diff --git a/pyproject.toml b/pyproject.toml index 2e5340d39..cd348eeb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,10 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" version = "0.9.17" -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, Oh My Pi (OMP), Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" } -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", "omp", "oh-my-pi", "devin", "knowledge-graph", "rag", "graphrag", "obsidian", "community-detection", "tree-sitter", "leiden", "llm"] requires-python = ">=3.10" dependencies = [ "networkx>=3.4", diff --git a/uv.lock b/uv.lock index 088ebbbdc..09653a59d 100644 --- a/uv.lock +++ b/uv.lock @@ -1090,7 +1090,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.6" +version = "0.9.17" source = { editable = "." } dependencies = [ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },