Add OpenCode tool.execute.before plugin via graphify opencode install (#71)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-04-09 08:55:07 +01:00
parent 2a550a3fe6
commit e5760e6b03
2 changed files with 132 additions and 3 deletions
+88 -3
View File
@@ -160,6 +160,87 @@ Rules:
_AGENTS_MD_MARKER = "## graphify"
# OpenCode tool.execute.before plugin — fires before every tool call.
# Injects a graph reminder into bash command output when graph.json exists.
_OPENCODE_PLUGIN_JS = """\
// graphify OpenCode plugin
// Injects a knowledge graph reminder before bash tool calls when the graph exists.
import { existsSync } from "fs";
import { join } from "path";
export const GraphifyPlugin = async ({ directory }) => {
let reminded = false;
return {
"tool.execute.before": async (input, output) => {
if (reminded) return;
if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
if (input.tool === "bash") {
output.args.command =
'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." && ' +
output.args.command;
reminded = true;
}
},
};
};
"""
_OPENCODE_PLUGIN_PATH = Path(".opencode") / "plugins" / "graphify.js"
_OPENCODE_CONFIG_PATH = Path("opencode.json")
def _install_opencode_plugin(project_dir: Path) -> None:
"""Write graphify.js plugin and register it in opencode.json."""
plugin_file = project_dir / _OPENCODE_PLUGIN_PATH
plugin_file.parent.mkdir(parents=True, exist_ok=True)
plugin_file.write_text(_OPENCODE_PLUGIN_JS, encoding="utf-8")
print(f" {_OPENCODE_PLUGIN_PATH} -> tool.execute.before hook written")
config_file = project_dir / _OPENCODE_CONFIG_PATH
if config_file.exists():
try:
config = json.loads(config_file.read_text(encoding="utf-8"))
except json.JSONDecodeError:
config = {}
else:
config = {}
plugins = config.setdefault("plugin", [])
entry = str(_OPENCODE_PLUGIN_PATH)
if entry not in plugins:
plugins.append(entry)
config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
print(f" {_OPENCODE_CONFIG_PATH} -> plugin registered")
else:
print(f" {_OPENCODE_CONFIG_PATH} -> plugin already registered (no change)")
def _uninstall_opencode_plugin(project_dir: Path) -> None:
"""Remove graphify.js plugin and deregister from opencode.json."""
plugin_file = project_dir / _OPENCODE_PLUGIN_PATH
if plugin_file.exists():
plugin_file.unlink()
print(f" {_OPENCODE_PLUGIN_PATH} -> removed")
config_file = project_dir / _OPENCODE_CONFIG_PATH
if not config_file.exists():
return
try:
config = json.loads(config_file.read_text(encoding="utf-8"))
except json.JSONDecodeError:
return
plugins = config.get("plugin", [])
entry = str(_OPENCODE_PLUGIN_PATH)
if entry in plugins:
plugins.remove(entry)
if not plugins:
config.pop("plugin")
config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
print(f" {_OPENCODE_CONFIG_PATH} -> plugin deregistered")
_CODEX_HOOK = {
"hooks": {
"PreToolUse": [
@@ -238,11 +319,13 @@ def _agents_install(project_dir: Path, platform: str) -> None:
if platform == "codex":
_install_codex_hook(project_dir or Path("."))
elif platform == "opencode":
_install_opencode_plugin(project_dir or Path("."))
print()
print(f"{platform.capitalize()} will now check the knowledge graph before answering")
print("codebase questions and rebuild it after code changes.")
if platform != "codex":
if platform not in ("codex", "opencode"):
print()
print("Note: unlike Claude Code, there is no PreToolUse hook equivalent for")
print(f"{platform.capitalize()} — the AGENTS.md rules are the always-on mechanism.")
@@ -274,6 +357,8 @@ def _agents_uninstall(project_dir: Path) -> None:
target.unlink()
print(f"AGENTS.md was empty after removal - deleted {target.resolve()}")
_uninstall_opencode_plugin(project_dir or Path("."))
def claude_install(project_dir: Path | None = None) -> None:
"""Write the graphify section to the local CLAUDE.md."""
@@ -402,8 +487,8 @@ def main() -> None:
print(" claude uninstall remove graphify section from CLAUDE.md + PreToolUse hook")
print(" codex install write graphify section to AGENTS.md (Codex)")
print(" codex uninstall remove graphify section from AGENTS.md")
print(" opencode install write graphify section to AGENTS.md (OpenCode)")
print(" opencode uninstall remove graphify section from AGENTS.md")
print(" opencode install write graphify section to AGENTS.md + tool.execute.before plugin (OpenCode)")
print(" opencode uninstall remove graphify section from AGENTS.md + plugin")
print(" claw install write graphify section to AGENTS.md (OpenClaw)")
print(" claw uninstall remove graphify section from AGENTS.md")
print(" droid install write graphify section to AGENTS.md (Factory Droid)")
+44
View File
@@ -181,3 +181,47 @@ def test_agents_uninstall_no_op_when_not_installed(tmp_path, capsys):
_agents_uninstall(tmp_path)
out = capsys.readouterr().out
assert "nothing to do" in out
# --- OpenCode plugin tests ---
def test_opencode_agents_install_writes_plugin(tmp_path):
"""opencode install writes .opencode/plugins/graphify.js."""
_agents_install(tmp_path, "opencode")
plugin = tmp_path / ".opencode" / "plugins" / "graphify.js"
assert plugin.exists()
assert "tool.execute.before" in plugin.read_text()
def test_opencode_agents_install_registers_plugin_in_config(tmp_path):
"""opencode install registers the plugin in opencode.json."""
_agents_install(tmp_path, "opencode")
config_file = tmp_path / "opencode.json"
assert config_file.exists()
import json as _json
config = _json.loads(config_file.read_text())
assert any("graphify.js" in p for p in config.get("plugin", []))
def test_opencode_agents_install_merges_existing_config(tmp_path):
"""opencode install preserves existing opencode.json keys."""
import json as _json
config_file = tmp_path / "opencode.json"
config_file.write_text(_json.dumps({"model": "claude-opus-4-5", "plugin": []}))
_agents_install(tmp_path, "opencode")
config = _json.loads(config_file.read_text())
assert config["model"] == "claude-opus-4-5"
assert any("graphify.js" in p for p in config["plugin"])
def test_opencode_agents_uninstall_removes_plugin(tmp_path):
"""opencode uninstall removes the plugin file and deregisters from opencode.json."""
import json as _json
_agents_install(tmp_path, "opencode")
_agents_uninstall(tmp_path)
plugin = tmp_path / ".opencode" / "plugins" / "graphify.js"
assert not plugin.exists()
config_file = tmp_path / "opencode.json"
if config_file.exists():
config = _json.loads(config_file.read_text())
assert not any("graphify.js" in p for p in config.get("plugin", []))