Skip to content

Commit 090aacb

Browse files
committed
feat: add Kilo Code support with installation and command integration
- Introduced Kilo Code as a supported platform in the README and installation scripts. - Added Kilo-specific skill and command files for knowledge graph integration. - Implemented installation and uninstallation logic for Kilo Code, including plugin registration. - Enhanced tests to cover Kilo Code installation, command existence, and plugin registration. - Updated project metadata to reflect Kilo Code integration.
1 parent 64e4c64 commit 090aacb

6 files changed

Lines changed: 840 additions & 9 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</a>
2121
</p>
2222

23-
**An AI coding assistant skill.** Type `/graphify` in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, or Google Antigravity - it reads your files, builds a knowledge graph, and gives you back structure you didn't know was there. Understand a codebase faster. Find the "why" behind architectural decisions.
23+
**An AI coding assistant skill.** Type `/graphify` in Claude Code, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, or Google Antigravity - it reads your files, builds a knowledge graph, and gives you back structure you didn't know was there. Understand a codebase faster. Find the "why" behind architectural decisions.
2424

2525
Fully multimodal. Drop in code, PDFs, markdown, screenshots, diagrams, whiteboard photos, images in other languages, or video and audio files - graphify extracts concepts and relationships from all of it and connects them into one graph. Videos are transcribed with Whisper using a domain-aware prompt derived from your corpus. 25 languages supported via tree-sitter AST (Python, JS, TS, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Lua, Zig, PowerShell, Elixir, Objective-C, Julia, Verilog, SystemVerilog, Vue, Svelte, Dart).
2626

@@ -60,7 +60,7 @@ Every relationship is tagged `EXTRACTED` (found directly in source), `INFERRED`
6060

6161
## Install
6262

63-
**Requires:** Python 3.10+ and one of: [Claude Code](https://claude.ai/code), [Codex](https://openai.com/codex), [OpenCode](https://opencode.ai), [Cursor](https://cursor.com), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli), [VS Code Copilot Chat](https://code.visualstudio.com/docs/copilot/overview), [Aider](https://aider.chat), [OpenClaw](https://openclaw.ai), [Factory Droid](https://factory.ai), [Trae](https://trae.ai), [Kiro](https://kiro.dev), Hermes, or [Google Antigravity](https://antigravity.google)
63+
**Requires:** Python 3.10+ and one of: [Claude Code](https://claude.ai/code), [Codex](https://openai.com/codex), [OpenCode](https://opencode.ai), [Kilo Code](https://kilocode.ai), [Cursor](https://cursor.com), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli), [VS Code Copilot Chat](https://code.visualstudio.com/docs/copilot/overview), [Aider](https://aider.chat), [OpenClaw](https://openclaw.ai), [Factory Droid](https://factory.ai), [Trae](https://trae.ai), [Kiro](https://kiro.dev), Hermes, or [Google Antigravity](https://antigravity.google)
6464

6565
```bash
6666
# Recommended — works on Mac and Linux with no PATH setup needed
@@ -83,6 +83,7 @@ pip install graphifyy && graphify install
8383
| Claude Code (Windows) | `graphify install` (auto-detected) or `graphify install --platform windows` |
8484
| Codex | `graphify install --platform codex` |
8585
| OpenCode | `graphify install --platform opencode` |
86+
| Kilo Code | `graphify install --platform kilo` |
8687
| GitHub Copilot CLI | `graphify install --platform copilot` |
8788
| VS Code Copilot Chat | `graphify vscode install` |
8889
| Aider | `graphify install --platform aider` |
@@ -115,6 +116,7 @@ After building a graph, run this once in your project:
115116
| Claude Code | `graphify claude install` |
116117
| Codex | `graphify codex install` |
117118
| OpenCode | `graphify opencode install` |
119+
| Kilo Code | `graphify kilo install` |
118120
| GitHub Copilot CLI | `graphify copilot install` |
119121
| VS Code Copilot Chat | `graphify vscode install` |
120122
| Aider | `graphify aider install` |
@@ -134,6 +136,8 @@ After building a graph, run this once in your project:
134136

135137
**OpenCode** writes to `AGENTS.md` and also installs a **`tool.execute.before` plugin** (`.opencode/plugins/graphify.js` + `opencode.json` registration) that fires before bash tool calls and injects the graph reminder into tool output when the graph exists.
136138

139+
**Kilo Code** installs the Graphify skill to `~/.config/kilo/skills/graphify/SKILL.md` and a native `/graphify` command to `~/.config/kilo/command/graphify.md`. `graphify kilo install` also writes `AGENTS.md` plus a native **`tool.execute.before` plugin** (`.kilo/plugins/graphify.js` + `.kilo/kilo.json` or `.kilo/kilo.jsonc` registration) so Kilo gets the same always-on graph reminder behavior through native `.kilo` config.
140+
137141
**Cursor** writes `.cursor/rules/graphify.mdc` with `alwaysApply: true` — Cursor includes it in every conversation automatically, no hook needed.
138142

139143
**Gemini CLI** copies the skill to `~/.gemini/skills/graphify/SKILL.md`, writes a `GEMINI.md` section, and installs a `BeforeTool` hook in `.gemini/settings.json` that fires before file-read tool calls — same always-on mechanism as Claude Code.
@@ -294,6 +298,8 @@ graphify claude install # CLAUDE.md + PreToolUse hook (Claude Code)
294298
graphify claude uninstall
295299
graphify codex install # AGENTS.md + PreToolUse hook in .codex/hooks.json (Codex)
296300
graphify opencode install # AGENTS.md + tool.execute.before plugin (OpenCode)
301+
graphify kilo install # native Kilo skill + /graphify command + AGENTS.md + .kilo plugin
302+
graphify kilo uninstall
297303
graphify cursor install # .cursor/rules/graphify.mdc (Cursor)
298304
graphify cursor uninstall
299305
graphify gemini install # GEMINI.md + BeforeTool hook (Gemini CLI)

graphify/__main__.py

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ def _refresh_all_version_stamps() -> None:
7575
"skill_dst": Path(".config") / "opencode" / "skills" / "graphify" / "SKILL.md",
7676
"claude_md": False,
7777
},
78+
"kilo": {
79+
"skill_file": "skill-kilo.md",
80+
"skill_dst": Path(".config") / "kilo" / "skills" / "graphify" / "SKILL.md",
81+
"claude_md": False,
82+
},
7883
"aider": {
7984
"skill_file": "skill-aider.md",
8085
"skill_dst": Path(".aider") / "graphify" / "SKILL.md",
@@ -154,6 +159,20 @@ def install(platform: str = "claude") -> None:
154159
(skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
155160
print(f" skill installed -> {skill_dst}")
156161

162+
if platform == "kilo":
163+
# Kilo Code also supports a native /graphify command file.
164+
command_src = Path(__file__).parent / "command-kilo.md"
165+
if not command_src.exists():
166+
print(
167+
f"error: command-kilo.md not found in package - reinstall graphify",
168+
file=sys.stderr,
169+
)
170+
sys.exit(1)
171+
command_dst = Path.home() / ".config" / "kilo" / "command" / "graphify.md"
172+
command_dst.parent.mkdir(parents=True, exist_ok=True)
173+
shutil.copy(command_src, command_dst)
174+
print(f" command installed -> {command_dst}")
175+
157176
if cfg["claude_md"]:
158177
# Register in ~/.claude/CLAUDE.md (Claude Code only)
159178
claude_md = Path.home() / ".claude" / "CLAUDE.md"
@@ -615,6 +634,161 @@ def _cursor_uninstall(project_dir: Path) -> None:
615634
print(f"graphify Cursor rule removed from {rule_path.resolve()}")
616635

617636

637+
_KILO_PLUGIN_JS = """\
638+
// graphify Kilo plugin
639+
// Injects a knowledge graph reminder before bash tool calls when the graph exists.
640+
import { existsSync } from "fs";
641+
import { join } from "path";
642+
643+
export const GraphifyPlugin = async ({ directory }) => {
644+
let reminded = false;
645+
646+
return {
647+
"tool.execute.before": async (input, output) => {
648+
if (reminded) return;
649+
if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
650+
651+
if (input.tool === "bash") {
652+
output.args.command =
653+
'echo "[graphify] Knowledge graph available. Read graphify-out/GRAPH_REPORT.md for god nodes and architecture context before searching files." && ' +
654+
output.args.command;
655+
reminded = true;
656+
}
657+
},
658+
};
659+
};
660+
"""
661+
662+
_KILO_PLUGIN_PATH = Path(".kilo") / "plugins" / "graphify.js"
663+
_KILO_CONFIG_JSON_PATH = Path(".kilo") / "kilo.json"
664+
_KILO_CONFIG_JSONC_PATH = Path(".kilo") / "kilo.jsonc"
665+
666+
667+
def _strip_json_comments(raw: str) -> str:
668+
"""Remove JSONC-style comments while leaving string content intact."""
669+
result: list[str] = []
670+
in_string = False
671+
escaped = False
672+
line_comment = False
673+
block_comment = False
674+
i = 0
675+
676+
while i < len(raw):
677+
ch = raw[i]
678+
nxt = raw[i + 1] if i + 1 < len(raw) else ""
679+
680+
if line_comment:
681+
if ch == "\n":
682+
line_comment = False
683+
result.append(ch)
684+
i += 1
685+
continue
686+
687+
if block_comment:
688+
if ch == "*" and nxt == "/":
689+
block_comment = False
690+
i += 2
691+
else:
692+
i += 1
693+
continue
694+
695+
if in_string:
696+
result.append(ch)
697+
if escaped:
698+
escaped = False
699+
elif ch == "\\":
700+
escaped = True
701+
elif ch == '"':
702+
in_string = False
703+
i += 1
704+
continue
705+
706+
if ch == "/" and nxt == "/":
707+
line_comment = True
708+
i += 2
709+
continue
710+
if ch == "/" and nxt == "*":
711+
block_comment = True
712+
i += 2
713+
continue
714+
715+
result.append(ch)
716+
if ch == '"':
717+
in_string = True
718+
i += 1
719+
720+
return re.sub(r",(\s*[}\]])", r"\1", "".join(result))
721+
722+
723+
def _load_json_like(config_file: Path) -> dict:
724+
if not config_file.exists():
725+
return {}
726+
try:
727+
raw = config_file.read_text(encoding="utf-8")
728+
if config_file.suffix == ".jsonc":
729+
raw = _strip_json_comments(raw)
730+
loaded = json.loads(raw)
731+
except (OSError, json.JSONDecodeError):
732+
return {}
733+
return loaded if isinstance(loaded, dict) else {}
734+
735+
736+
def _kilo_config_path(project_dir: Path) -> Path:
737+
kilo_dir = (project_dir or Path(".")) / ".kilo"
738+
jsonc_path = kilo_dir / _KILO_CONFIG_JSONC_PATH.name
739+
if jsonc_path.exists():
740+
return jsonc_path
741+
return kilo_dir / _KILO_CONFIG_JSON_PATH.name
742+
743+
744+
def _install_kilo_plugin(project_dir: Path) -> None:
745+
"""Write graphify.js plugin and register it in .kilo/kilo.json(.c)."""
746+
plugin_file = project_dir / _KILO_PLUGIN_PATH
747+
plugin_file.parent.mkdir(parents=True, exist_ok=True)
748+
plugin_file.write_text(_KILO_PLUGIN_JS, encoding="utf-8")
749+
print(f" {_KILO_PLUGIN_PATH} -> tool.execute.before hook written")
750+
751+
config_file = _kilo_config_path(project_dir)
752+
config_file.parent.mkdir(parents=True, exist_ok=True)
753+
config = _load_json_like(config_file)
754+
plugins = config.get("plugin")
755+
if not isinstance(plugins, list):
756+
plugins = []
757+
config["plugin"] = plugins
758+
entry = plugin_file.resolve().as_uri()
759+
if entry not in plugins:
760+
plugins.append(entry)
761+
config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
762+
print(f" {config_file.relative_to(project_dir)} -> plugin registered")
763+
else:
764+
print(
765+
f" {config_file.relative_to(project_dir)} -> plugin already registered (no change)"
766+
)
767+
768+
769+
def _uninstall_kilo_plugin(project_dir: Path) -> None:
770+
"""Remove graphify.js plugin and deregister it from .kilo/kilo.json(.c)."""
771+
plugin_file = project_dir / _KILO_PLUGIN_PATH
772+
if plugin_file.exists():
773+
plugin_file.unlink()
774+
print(f" {_KILO_PLUGIN_PATH} -> removed")
775+
776+
config_file = _kilo_config_path(project_dir)
777+
if not config_file.exists():
778+
return
779+
config = _load_json_like(config_file)
780+
plugins = config.get("plugin", [])
781+
if not isinstance(plugins, list):
782+
plugins = []
783+
entry = plugin_file.resolve().as_uri()
784+
if entry in plugins:
785+
config["plugin"] = [plugin for plugin in plugins if plugin != entry]
786+
if not config["plugin"]:
787+
config.pop("plugin")
788+
config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
789+
print(f" {config_file.relative_to(project_dir)} -> plugin deregistered")
790+
791+
618792
# OpenCode tool.execute.before plugin — fires before every tool call.
619793
# Injects a graph reminder into bash command output when graph.json exists.
620794
_OPENCODE_PLUGIN_JS = """\
@@ -776,7 +950,7 @@ def _agents_install(project_dir: Path, platform: str) -> None:
776950
print()
777951
print(f"{platform.capitalize()} will now check the knowledge graph before answering")
778952
print("codebase questions and rebuild it after code changes.")
779-
if platform not in ("codex", "opencode"):
953+
if platform not in ("codex", "opencode", "kilo"):
780954
print()
781955
print("Note: unlike Claude Code, there is no PreToolUse hook equivalent for")
782956
print(f"{platform.capitalize()} — the AGENTS.md rules are the always-on mechanism.")
@@ -788,11 +962,19 @@ def _agents_uninstall(project_dir: Path, platform: str = "") -> None:
788962

789963
if not target.exists():
790964
print("No AGENTS.md found in current directory - nothing to do")
965+
if platform == "opencode":
966+
_uninstall_opencode_plugin(project_dir or Path("."))
967+
elif platform == "kilo":
968+
_uninstall_kilo_plugin(project_dir or Path("."))
791969
return
792970

793971
content = target.read_text(encoding="utf-8")
794972
if _AGENTS_MD_MARKER not in content:
795973
print("graphify section not found in AGENTS.md - nothing to do")
974+
if platform == "opencode":
975+
_uninstall_opencode_plugin(project_dir or Path("."))
976+
elif platform == "kilo":
977+
_uninstall_kilo_plugin(project_dir or Path("."))
796978
return
797979

798980
cleaned = re.sub(
@@ -810,6 +992,52 @@ def _agents_uninstall(project_dir: Path, platform: str = "") -> None:
810992

811993
if platform == "opencode":
812994
_uninstall_opencode_plugin(project_dir or Path("."))
995+
elif platform == "kilo":
996+
_uninstall_kilo_plugin(project_dir or Path("."))
997+
998+
999+
def _kilo_uninstall_global() -> list[str]:
1000+
removed = []
1001+
command_dst = Path.home() / ".config" / "kilo" / "command" / "graphify.md"
1002+
if command_dst.exists():
1003+
command_dst.unlink()
1004+
removed.append(f"command removed: {command_dst}")
1005+
try:
1006+
command_dst.parent.rmdir()
1007+
except OSError:
1008+
pass
1009+
1010+
skill_dst = Path.home() / _PLATFORM_CONFIG["kilo"]["skill_dst"]
1011+
if skill_dst.exists():
1012+
skill_dst.unlink()
1013+
removed.append(f"skill removed: {skill_dst}")
1014+
version_file = skill_dst.parent / ".graphify_version"
1015+
if version_file.exists():
1016+
version_file.unlink()
1017+
for d in (
1018+
skill_dst.parent,
1019+
skill_dst.parent.parent,
1020+
skill_dst.parent.parent.parent,
1021+
):
1022+
try:
1023+
d.rmdir()
1024+
except OSError:
1025+
break
1026+
1027+
return removed
1028+
1029+
1030+
def _kilo_install(project_dir: Path) -> None:
1031+
"""Install native Kilo skill + command globally and always-on project wiring locally."""
1032+
install(platform="kilo")
1033+
_agents_install(project_dir or Path("."), "kilo")
1034+
1035+
1036+
def _kilo_uninstall(project_dir: Path) -> None:
1037+
"""Remove Kilo always-on project wiring and global skill/command files."""
1038+
_agents_uninstall(project_dir or Path("."), platform="kilo")
1039+
removed = _kilo_uninstall_global()
1040+
print("; ".join(removed) if removed else "nothing to remove")
8131041

8141042

8151043
def claude_install(project_dir: Path | None = None) -> None:
@@ -954,6 +1182,12 @@ def main() -> None:
9541182
print(" codex uninstall remove graphify section from AGENTS.md")
9551183
print(" opencode install write graphify section to AGENTS.md + tool.execute.before plugin (OpenCode)")
9561184
print(" opencode uninstall remove graphify section from AGENTS.md + plugin")
1185+
print(
1186+
" kilo install install native Kilo skill + command + AGENTS.md + .kilo plugin"
1187+
)
1188+
print(
1189+
" kilo uninstall remove native Kilo skill + command + AGENTS.md + .kilo plugin"
1190+
)
9571191
print(" aider install write graphify section to AGENTS.md (Aider)")
9581192
print(" aider uninstall remove graphify section from AGENTS.md")
9591193
print(" copilot install copy graphify skill to ~/.copilot/skills (GitHub Copilot CLI)")
@@ -1052,6 +1286,15 @@ def main() -> None:
10521286
else:
10531287
print("Usage: graphify copilot [install|uninstall]", file=sys.stderr)
10541288
sys.exit(1)
1289+
elif cmd == "kilo":
1290+
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1291+
if subcmd == "install":
1292+
_kilo_install(Path("."))
1293+
elif subcmd == "uninstall":
1294+
_kilo_uninstall(Path("."))
1295+
else:
1296+
print("Usage: graphify kilo [install|uninstall]", file=sys.stderr)
1297+
sys.exit(1)
10551298
elif cmd == "kiro":
10561299
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
10571300
if subcmd == "install":

graphify/command-kilo.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
description: Build or query a graphify knowledge graph
3+
---
4+
5+
Invoke the `graphify` skill immediately.
6+
7+
Pass the full `/graphify` argument string through unchanged.
8+
If no arguments were supplied, treat the target path as `.`.
9+
10+
Examples:
11+
- `/graphify`
12+
- `/graphify src --update`
13+
- `/graphify query "what connects auth to billing?"`
14+
15+
Do not answer from raw files before handing off to the `graphify` skill.

0 commit comments

Comments
 (0)