diff --git a/packages/kilo-docs/lib/nav/automate.ts b/packages/kilo-docs/lib/nav/automate.ts index ce3ddf622b..3bbab572fc 100644 --- a/packages/kilo-docs/lib/nav/automate.ts +++ b/packages/kilo-docs/lib/nav/automate.ts @@ -33,6 +33,11 @@ export const AutomateNav: NavSection[] = [ href: "/automate/extending/shell-integration", children: "Shell Integration", }, + { + href: "/automate/extending/plugins", + children: "Plugins", + platform: "new", + }, { href: "/automate/extending/auto-launch", children: "Auto-launch Configuration", diff --git a/packages/kilo-docs/pages/automate/extending/plugins.md b/packages/kilo-docs/pages/automate/extending/plugins.md new file mode 100644 index 0000000000..5ae61bc47e --- /dev/null +++ b/packages/kilo-docs/pages/automate/extending/plugins.md @@ -0,0 +1,487 @@ +--- +title: "Plugins" +description: "Extend the Kilo CLI with custom hooks, tools, auth providers, and more" +platform: new +--- + +# Plugins + +Plugins extend the Kilo CLI by hooking into events, adding custom tools, registering auth or model providers, and customizing runtime behavior. They are TypeScript or JavaScript modules loaded at startup. + +{% callout type="note" %} +Plugins run in the Kilo CLI. They are available everywhere the CLI runs — directly, through `kilo serve`, and inside the VS Code extension (which spawns `kilo serve` under the hood). +{% /callout %} + +## What plugins can do + +- **Add custom tools** the model can call (like `read`, `write`, `bash`). +- **Intercept tool calls** to mutate arguments, rewrite output, or block dangerous operations. +- **Subscribe to events** — sessions, messages, permissions, LSP diagnostics, file changes, etc. +- **Register auth providers** — OAuth or API-key flows for model providers. +- **Register model providers** — dynamic model catalogs. +- **Mutate chat parameters or headers** sent to the LLM. +- **Customize compaction** — inject or replace the prompt used when a session is compacted. +- **Inject shell environment variables** for commands executed by the agent or user. + +--- + +## Use a plugin + +There are three ways to load plugins. + +### From a config file + +Add an array of plugin specifiers to your config file: + +```json +{ + "$schema": "https://app.kilo.ai/config.json", + "plugin": [ + "@your-org/your-plugin", + "your-plugin@1.2.3", + ["your-plugin", { "apiKey": "{env:MY_API_KEY}" }], + "./plugins/local.ts", + "file:///abs/path/plugin.ts" + ] +} +``` + +Each entry can be: + +| Form | Loaded from | +| -------------------------------------- | ---------------------------------------------------------------- | +| `"package-name"` | Latest version from npm | +| `"package-name@1.2.3"` | Pinned version from npm | +| `["package-name", { options }]` | npm package with options passed to the plugin function | +| `"./path/plugin.ts"` / `"file:///..."` | Local file (relative to the config file or absolute `file:` URL) | + +Config files live in the same locations as the rest of your CLI configuration — see the [CLI configuration reference](/docs/code-with-ai/platforms/cli#configuration). + +### From a plugin directory + +Drop TypeScript or JavaScript files into a `plugin/` or `plugins/` folder inside any config directory: + +- Global: `~/.config/kilo/plugin/` +- Project: `.kilo/plugin/`, `.kilocode/plugin/`, or `.opencode/plugin/` + +Every `.ts` or `.js` file in those directories is auto-registered at startup — no need to list them in the config file. + +```text +my-project/ +├── kilo.json +└── .kilo/ + └── plugin/ + ├── env-guard.ts + └── notifications.ts +``` + +### From the `kilo plugin` command + +Install an npm plugin and patch your config in one step: + +```bash +# Install into the current project's config +kilo plugin my-plugin + +# Install into your global config +kilo plugin my-plugin --global + +# Replace an existing entry +kilo plugin my-plugin --force +``` + +The command resolves the package, reads its `package.json` for `exports["./server"]` / `exports["./tui"]`, and writes the entry into your `kilo.json` / `opencode.json` while preserving comments. + +### How plugins are installed + +- **npm plugins** are installed automatically at startup using Bun. Packages and their dependencies are cached in Kilo's XDG cache directory (`~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows). +- **Local plugins** are loaded directly from the plugin directory. If your plugin imports external packages, add a `package.json` to your config directory (see [Dependencies](#dependencies)) — Kilo runs `bun install` on startup so imports resolve. + +### Load order + +Plugins from all sources run on every session. They load in this order: + +1. Internal built-ins (Kilo Gateway auth, Codex auth, Copilot auth, Cloudflare, etc.) +2. Global config plugin array (`~/.config/kilo/kilo.json`) +3. Global plugin directory (`~/.config/kilo/plugin/`) +4. Project config plugin array (`kilo.json` / `opencode.json`) +5. Project plugin directory (`.kilo/plugin/` and friends) + +Duplicates (same package, same version) are deduplicated. Hooks from multiple plugins run sequentially in load order. + +### Disabling external plugins + +Set the `KILO_PURE=1` environment variable to skip all external plugins — only built-in plugins will load. Useful for reproducible CI runs or debugging. + +--- + +## Create a plugin + +A plugin is a module that exports a function returning a set of [hooks](#hooks-reference). + +### Basic structure + +Create a file in your plugin directory: + +```ts +// .kilo/plugin/hello.ts +import type { Plugin } from "@kilocode/plugin" + +const hello: Plugin = async ({ project, client, $, directory, worktree }) => { + console.log("hello plugin loaded") + + return { + // hook implementations go here + } +} + +export default { id: "hello", server: hello } +``` + +The plugin function receives a context object: + +| Field | Description | +| ------------------------ | ----------------------------------------------------------------- | +| `project` | Current project metadata. | +| `directory` | Current working directory for this session. | +| `worktree` | Git worktree root for this session. | +| `client` | A Kilo SDK client (`@kilocode/sdk`) for calling the local server. | +| `$` | [Bun's shell API](https://bun.com/docs/runtime/shell). | +| `serverUrl` | URL of the local Kilo server. | +| `experimental_workspace` | Register workspace adaptors (used by Agent Manager). | + +The function returns a `Hooks` object. Any second argument is the options object passed via config (e.g. the `{ apiKey: "..." }` from `["my-plugin", { apiKey: "..." }]`). + +### Module shape + +Plugins must default-export a module descriptor. `id` is required for local-file plugins and inferred from `package.json#name` for npm plugins. + +```ts +import type { Plugin } from "@kilocode/plugin" + +const server: Plugin = async (ctx) => ({ + /* hooks */ +}) + +export default { + id: "my-plugin", + server, +} +``` + +An npm plugin can also expose a TUI entry point (`tui`) for [TUI plugins](#tui-plugins), but `server` and `tui` are separate modules. + +### TypeScript support + +Install the plugin package locally and import its types: + +```bash +bun add -d @kilocode/plugin +``` + +```ts +import type { Plugin } from "@kilocode/plugin" +import { tool } from "@kilocode/plugin/tool" +``` + +Kilo automatically creates a `package.json` in config directories that contain a `plugin/` folder and installs `@kilocode/plugin` so types resolve out of the box. + +### Engine compatibility + +Declare a CLI version range to prevent a plugin from loading against an incompatible build: + +```json +{ + "name": "my-plugin", + "engines": { "opencode": "^7.0.0" } +} +``` + +If the running CLI does not satisfy the range, the plugin is skipped and a warning is surfaced. + +### Dependencies + +Local plugins and custom tools can use external npm packages. Add a `package.json` to your config directory: + +```json +// .kilo/package.json +{ + "dependencies": { + "shescape": "^2.1.0" + } +} +``` + +Kilo runs `bun install` at startup so your plugins can import the packages: + +```ts +// .kilo/plugin/escape-bash.ts +import { escape } from "shescape" +import type { Plugin } from "@kilocode/plugin" + +const EscapeBash: Plugin = async () => ({ + "tool.execute.before": async (input, output) => { + if (input.tool === "bash") { + output.args.command = escape(output.args.command) + } + }, +}) + +export default { id: "escape-bash", server: EscapeBash } +``` + +--- + +## Hooks reference + +Every hook is optional. Return only the ones you care about. + +### Lifecycle + +| Hook | Description | +| -------- | --------------------------------------------------------------------------------- | +| `config` | Receives the fully-resolved config at startup. Read-only — useful for inspection. | +| `event` | Called for **every** event on the internal bus (see [Events](#events)). | + +### Tools + +| Hook | Description | +| --------------------- | ----------------------------------------------------------------------------------------------- | +| `tool` | Map of tool name → [tool definition](#custom-tools). Added tools are callable by the model. | +| `tool.execute.before` | Fires before a tool runs; you can mutate `output.args`. | +| `tool.execute.after` | Fires after a tool returns; you can rewrite `output.title`, `output.output`, `output.metadata`. | +| `tool.definition` | Mutate a tool's `description` and `parameters` before they are sent to the model. | + +### Chat + +| Hook | Description | +| ------------------------ | ---------------------------------------------------------------------------- | +| `chat.message` | Fires when a new user message arrives. Inspect or modify `parts`. | +| `chat.params` | Mutate `temperature`, `topP`, `topK`, `maxOutputTokens`, provider `options`. | +| `chat.headers` | Add or replace HTTP headers on the LLM API call. | +| `permission.ask` | Auto-allow or auto-deny permission prompts. | +| `command.execute.before` | Intercept slash command execution; mutate the resulting `parts`. | +| `shell.env` | Inject environment variables into every shell command Kilo runs. | + +### Providers & auth + +| Hook | Description | +| ---------- | ------------------------------------------------------------------------------------ | +| `auth` | Register an auth method (OAuth or API key) for a provider, with interactive prompts. | +| `provider` | Dynamically supply a model catalog for a provider (useful for BYO-model gateways). | + +### Experimental + +These hooks live behind the `experimental.` prefix and may change between releases. + +| Hook | Description | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `experimental.chat.messages.transform` | Rewrite the full message history before it is sent to the model. | +| `experimental.chat.system.transform` | Modify the system prompt array. | +| `experimental.session.compacting` | Inject extra context (`output.context`) or replace the compaction prompt entirely (`output.prompt`). | +| `experimental.compaction.autocontinue` | Disable the synthetic "continue" turn that follows compaction. | +| `experimental.text.complete` | Post-process final text parts (e.g. append signatures, redact secrets). | + +### Events + +The `event` hook fires for every event on Kilo's internal bus. Common event types include: + +- **Session**: `session.created`, `session.updated`, `session.idle`, `session.error`, `session.deleted`, `session.compacted`, `session.diff`, `session.status` +- **Message**: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed` +- **Tool**: `tool.execute.before`, `tool.execute.after` +- **Permission**: `permission.asked`, `permission.replied` +- **File**: `file.edited`, `file.watcher.updated` +- **Shell**: `shell.env` +- **Command**: `command.executed` +- **LSP**: `lsp.updated`, `lsp.client.diagnostics` +- **Todo**: `todo.updated` +- **Server**: `server.connected` +- **Installation**: `installation.updated` + +```ts +const server: Plugin = async () => ({ + event: async ({ event }) => { + if (event.type === "session.idle") { + // session finished responding + } + }, +}) +``` + +--- + +## Custom tools + +Plugins can register tools the model can call alongside the built-in ones. Use the `tool()` helper for type-safety: + +```ts +// .kilo/plugin/database.ts +import type { Plugin } from "@kilocode/plugin" +import { tool } from "@kilocode/plugin/tool" + +const DatabasePlugin: Plugin = async () => ({ + tool: { + query: tool({ + description: "Run a read-only SQL query against the project database", + args: { + sql: tool.schema.string().describe("SQL query to execute"), + }, + async execute(args, context) { + const { directory, worktree } = context + // your query logic here + return `ran: ${args.sql}` + }, + }), + }, +}) + +export default { id: "database", server: DatabasePlugin } +``` + +`args` uses a [Zod](https://zod.dev) schema via `tool.schema`. The tool's `execute` function receives: + +- `args` — validated against your schema +- `context` — `{ sessionID, messageID, agent, directory, worktree, abort, metadata, ask }` + +### Name precedence + +If a custom tool uses the same name as a built-in tool, **the custom tool wins**. Prefer unique names unless you intentionally want to override a built-in (for example, to wrap `bash` with extra validation). + +### Alternative: standalone tool files + +For tools that don't need the full plugin context, drop them in a `tool/` or `tools/` folder inside any config directory — for example `.kilo/tool/database.ts` or `~/.config/kilo/tool/database.ts`. The filename becomes the tool name, and each file exports a `tool()` definition directly. The layout is identical to the [OpenCode custom tools guide](https://opencode.ai/docs/custom-tools); substitute `.kilo/` (or `.kilocode/` / `.opencode/`) for `.opencode/`. + +--- + +## Examples + +### Send a notification when a session finishes + +```ts +// .kilo/plugin/notify.ts +import type { Plugin } from "@kilocode/plugin" + +const Notify: Plugin = async ({ $ }) => ({ + event: async ({ event }) => { + if (event.type === "session.idle") { + await $`osascript -e 'display notification "Session complete!" with title "Kilo"'` + } + }, +}) + +export default { id: "notify", server: Notify } +``` + +{% callout type="tip" %} +The VS Code extension already emits system notifications when a session finishes or errors — this plugin is for the raw CLI / TUI. +{% /callout %} + +### Block reads of `.env` files + +```ts +// .kilo/plugin/env-guard.ts +import type { Plugin } from "@kilocode/plugin" + +const EnvGuard: Plugin = async () => ({ + "tool.execute.before": async (input, output) => { + if (input.tool === "read" && String(output.args.filePath).includes(".env")) { + throw new Error("reading .env files is blocked") + } + }, +}) + +export default { id: "env-guard", server: EnvGuard } +``` + +### Inject environment variables into every shell command + +```ts +// .kilo/plugin/inject-env.ts +import type { Plugin } from "@kilocode/plugin" + +const InjectEnv: Plugin = async () => ({ + "shell.env": async (input, output) => { + output.env.MY_API_KEY = "secret" + output.env.PROJECT_ROOT = input.cwd + }, +}) + +export default { id: "inject-env", server: InjectEnv } +``` + +### Structured logging + +Prefer `client.app.log()` over `console.log` so entries land in Kilo's log pipeline: + +```ts +import type { Plugin } from "@kilocode/plugin" + +const Logger: Plugin = async ({ client }) => { + await client.app.log({ + body: { + service: "my-plugin", + level: "info", + message: "plugin initialized", + extra: { version: "1.0.0" }, + }, + }) + return {} +} + +export default { id: "logger", server: Logger } +``` + +Levels: `debug`, `info`, `warn`, `error`. + +### Inject context during session compaction + +```ts +// .kilo/plugin/compaction.ts +import type { Plugin } from "@kilocode/plugin" + +const Compaction: Plugin = async () => ({ + "experimental.session.compacting": async (input, output) => { + output.context.push( + "## Persist across compaction\n- current task status\n- files being actively edited\n- key decisions", + ) + }, +}) + +export default { id: "compaction", server: Compaction } +``` + +Set `output.prompt` to replace the default compaction prompt entirely — when present, `output.context` is ignored. + +--- + +## TUI plugins + +Plugins can also target the Kilo TUI itself — registering slash commands, routes, sidebar slots, dialogs, and keybinds. TUI plugins are SolidJS modules exported from `"./tui"` in your plugin package. + +TUI plugins live in a separate module namespace (`@kilocode/plugin/tui`) and have their own API surface (`TuiPluginApi`). Because the TUI API is larger and still evolving, this guide doesn't cover it exhaustively — use the types in `@kilocode/plugin/tui` as the reference, and look at the built-in TUI plugins under `packages/opencode/src/cli/cmd/tui/feature-plugins/` for working examples. + +--- + +## Troubleshooting + +- **Plugin failed to load** — check the CLI logs with `kilo --print-logs --log-level DEBUG`. Load failures are also surfaced as session errors in the TUI and VS Code extension. +- **Plugin loaded but hooks never fire** — make sure the default export includes `server`: + + ```ts + export default { id: "my-plugin", server } + ``` + + Named function exports are also accepted for backwards compatibility but should be considered legacy. + +- **Local plugin can't find an npm import** — add a `package.json` in the config directory so `bun install` picks up the dependency (see [Dependencies](#dependencies)). +- **Plugin loads in dev but not in CI** — verify `KILO_PURE` is not set, and that npm-installed plugins are cached (Kilo's XDG cache directory — `~/.cache/kilo/` on Linux, `~/Library/Caches/kilo/` on macOS, `%LOCALAPPDATA%\kilo\` on Windows). Run with `--log-level DEBUG` to see install output. +- **Reset the plugin cache** — delete the `node_modules/` under Kilo's cache directory (or the `node_modules` cache under your config directory) and restart Kilo. + +--- + +## Reference + +- Types: [`@kilocode/plugin`](https://github.com/Kilo-Org/kilocode/tree/main/packages/plugin) — `Plugin`, `Hooks`, `PluginInput`, `ToolDefinition`, `AuthHook`, `ProviderHook`. +- Example plugin: [`packages/plugin/src/example.ts`](https://github.com/Kilo-Org/kilocode/blob/main/packages/plugin/src/example.ts) +- CLI command: [`kilo plugin`](/docs/code-with-ai/platforms/cli-reference#kilo-plugin) +- Upstream docs (behavior is identical to OpenCode): [opencode.ai/docs/plugins](https://opencode.ai/docs/plugins) and [opencode.ai/docs/custom-tools](https://opencode.ai/docs/custom-tools)