Developing Plugins
This guide walks you through creating a KinBot plugin from scratch.
Quick Start
Section titled “Quick Start”# Create a plugin directorymkdir plugins/my-plugincd plugins/my-pluginCreate two files: a manifest and an entry point.
plugin.json
Section titled “plugin.json”{ "name": "my-plugin", "version": "1.0.0", "description": "My awesome KinBot plugin", "author": "Your Name", "main": "index.ts", "kinbot": ">=0.10.0", "permissions": [], "config": {}}index.ts
Section titled “index.ts”import { tool } from 'ai'import { z } from 'zod'
export default function(ctx) { ctx.log.info('My plugin loaded!')
return { tools: { hello: { availability: ['main', 'sub-kin'], create: () => tool({ description: 'Say hello', parameters: z.object({ name: z.string().describe('Name to greet'), }), execute: async ({ name }) => { return { result: `Hello, ${name}!` } }, }), }, },
async activate() { ctx.log.info('Plugin activated') },
async deactivate() { ctx.log.info('Plugin deactivated') }, }}That’s it! Restart KinBot (or click Reload Plugins) and your plugin appears in Settings → Plugins.
Plugin Structure
Section titled “Plugin Structure”plugins/my-plugin/├── plugin.json # Manifest (required)├── index.ts # Entry point (required)├── README.md # Documentation (optional)└── assets/ # Static assets, icons (optional) └── icon.pngPlugins live in the plugins/ directory at the KinBot root (sibling to src/).
Manifest Reference
Section titled “Manifest Reference”The plugin.json manifest declares metadata, permissions, and configuration:
{ "name": "weather", "version": "1.0.0", "description": "Get current weather and forecasts for any location", "author": "KinBot Community", "homepage": "https://github.com/user/kinbot-plugin-weather", "license": "MIT", "kinbot": ">=0.10.0", "main": "index.ts", "icon": "assets/icon.png", "permissions": [ "http:api.openweathermap.org" ], "dependencies": {}, "config": { "apiKey": { "type": "string", "label": "OpenWeatherMap API Key", "description": "Get one at https://openweathermap.org/api", "required": true, "secret": true }, "units": { "type": "select", "label": "Temperature Units", "options": ["metric", "imperial"], "default": "metric" } }}Manifest Fields
Section titled “Manifest Fields”| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Unique identifier ([a-z0-9-]+) |
version | string | ✅ | Semver version |
description | string | ✅ | One-line description |
main | string | ✅ | Entry point file (relative to plugin dir) |
author | string | Author name or org | |
homepage | string | URL to repo or docs | |
license | string | SPDX license identifier | |
kinbot | string | Semver range of compatible KinBot versions | |
icon | string | Path to icon (PNG/SVG, 128x128 recommended) | |
permissions | string[] | Declared permissions (see Security) | |
dependencies | object | Plugin dependencies (see Dependencies) | |
config | object | Configuration schema (see Configuration) |
Entry Point Contract
Section titled “Entry Point Contract”The plugin’s main file must default-export a function that receives a PluginContext and returns a PluginExports object:
import type { PluginContext, PluginExports } from 'kinbot/plugin'
export default function(ctx: PluginContext): PluginExports { return { tools: { /* ... */ }, providers: { /* ... */ }, channels: { /* ... */ }, hooks: { /* ... */ }, activate: async () => { /* called on enable */ }, deactivate: async () => { /* called on disable */ }, }}All exports are optional. Include only what your plugin provides.
Registering Tools
Section titled “Registering Tools”Tools are AI-callable functions that Kins use in conversations. They use the Vercel AI SDK tool format:
import { tool } from 'ai'import { z } from 'zod'
export default function(ctx) { return { tools: { get_weather: { // Where this tool is available availability: ['main', 'sub-kin'],
// Optional: if true, tool is disabled by default (user must opt in) defaultDisabled: false,
// Factory: receives execution context, returns an AI SDK tool create: (execCtx) => tool({ description: 'Get current weather for a location', parameters: z.object({ location: z.string().describe('City name or "lat,lon"'), }), execute: async ({ location }) => { const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&units=${ctx.config.units}&appid=${ctx.config.apiKey}` const res = await ctx.http.fetch(url) const data = await res.json() return { location: data.name, temperature: data.main.temp, description: data.weather[0].description, } }, }), }, }, }}Tool names are automatically namespaced: get_weather in plugin weather becomes plugin_weather_get_weather in the AI’s tool list.
Availability options:
'main'— Available in the main Kin agent'sub-kin'— Available in sub-Kin tasks
Registering Hooks
Section titled “Registering Hooks”Hooks let you intercept KinBot’s behavior at key lifecycle points:
export default function(ctx) { return { hooks: { afterToolCall: async (hookCtx) => { ctx.log.info({ tool: hookCtx.toolName, kinId: hookCtx.kinId }, 'Tool called') },
beforeChat: async (hookCtx) => { ctx.log.info({ kinId: hookCtx.kinId }, 'Chat starting') return hookCtx }, }, }}Available hooks:
| Hook | Fires when |
|---|---|
beforeChat | Before processing a chat message |
afterChat | After generating a response |
beforeToolCall | Before executing a tool |
afterToolCall | After a tool returns |
beforeCompacting | Before conversation compaction |
afterCompacting | After conversation compaction |
onTaskSpawn | When a sub-Kin task is spawned |
onCronTrigger | When a cron job fires |
Plugins can also register custom hooks for inter-plugin communication.
Registering Providers
Section titled “Registering Providers”Plugins can add custom AI providers:
export default function(ctx) { return { providers: { my_llm: { displayName: 'My Custom LLM', capabilities: ['llm'], definition: { type: 'my-llm', async testConnection(config) { return config.apiKey ? { valid: true } : { valid: false, error: 'API key required' } }, async listModels(config) { return [ { id: 'model-small', name: 'Small Model', capability: 'llm' }, ] }, }, }, }, }}Registering Channels
Section titled “Registering Channels”Plugins can add new messaging channels by implementing KinBot’s ChannelAdapter interface. See the built-in adapters in src/server/channels/ for reference.
Configuration
Section titled “Configuration”Define config fields in plugin.json to surface settings in the UI:
Config Field Types
Section titled “Config Field Types”| Type | UI Widget | Extra Properties |
|---|---|---|
string | Text input | placeholder, pattern (regex) |
number | Number input | min, max, step |
boolean | Toggle switch | |
select | Dropdown | options: string[] |
text | Textarea | rows, placeholder |
Common Properties
Section titled “Common Properties”All types support: label, description, required, default, secret.
Secrets Handling
Section titled “Secrets Handling”Fields with secret: true are:
- Stored encrypted in the KinBot database (same mechanism as provider API keys)
- Never exposed in API responses (replaced with
"••••••••") - Available to the plugin at runtime via
ctx.config - Shown as password fields in the UI
Dependencies
Section titled “Dependencies”Plugins can declare dependencies on other plugins using the dependencies field in plugin.json. Values are semver ranges.
{ "dependencies": { "core-utils": ">=1.0.0", "data-provider": "^2.0.0" }}How It Works
Section titled “How It Works”- On activation, KinBot checks that all declared dependencies are installed, enabled, and version-compatible.
- If any dependency is missing, disabled, or the wrong version, the plugin will not activate and shows an error.
- Disabling or uninstalling a plugin that other enabled plugins depend on is blocked with a clear error message.
- Dependencies are shown in the plugin settings UI, along with a list of dependents (plugins that require this one).
Dependency Order
Section titled “Dependency Order”KinBot automatically resolves plugin activation order using topological sorting. Dependencies are always activated before the plugins that need them, regardless of filesystem order. Circular dependencies are detected and reported as errors.
Security
Section titled “Security”Permission Model
Section titled “Permission Model”Plugins declare required permissions in plugin.json:
| Permission | Grants |
|---|---|
http:<host_pattern> | HTTP access to matching hosts (supports * wildcard) |
memory:read | Read Kin memories |
memory:write | Write/update Kin memories |
notify | Send notifications to users |
storage | Use plugin key-value storage (always granted) |
hooks:<hook_name> | Register a specific hook |
{ "permissions": [ "http:api.openweathermap.org", "http:*.twilio.com", "memory:read", "notify" ]}User Consent
Section titled “User Consent”When a plugin is first enabled, the UI shows a confirmation dialog listing all declared permissions. If a plugin update adds new permissions, re-approval is required.
What Plugins Cannot Do
Section titled “What Plugins Cannot Do”- Direct filesystem access — No
fsmodule. UsePluginStorageinstead. - Direct database access — No raw SQL. Use provided APIs.
- Modify other plugins — No access to other plugin internals.
- Access process/env — No
process.env. Secrets come viactx.config. - Spawn processes — No
child_process. Usectx.httpfor external APIs. - Import core internals — Only the
kinbot/pluginSDK types are public API.
Publishing
Section titled “Publishing”To the Plugin Registry
Section titled “To the Plugin Registry”- Host your plugin on a public GitHub repository
- Include a
README.mdwith documentation - Fork MarlBurroW/kinbot-plugins
- Add your entry to
registry.json - Open a Pull Request
npm Packages
Section titled “npm Packages”Convention: packages named kinbot-plugin-* on npm.
Examples
Section titled “Examples”Weather Tool Plugin
Section titled “Weather Tool Plugin”A complete plugin with configuration, HTTP calls, and two tools:
import { tool } from 'ai'import { z } from 'zod'
export default function(ctx) { const { apiKey, units = 'metric' } = ctx.config
return { tools: { get_weather: { availability: ['main', 'sub-kin'], create: () => tool({ description: 'Get current weather for a location', parameters: z.object({ location: z.string().describe('City name or "lat,lon"'), }), execute: async ({ location }) => { const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&units=${units}&appid=${apiKey}` const res = await ctx.http.fetch(url) const data = await res.json() return { location: data.name, temperature: data.main.temp, feels_like: data.main.feels_like, humidity: data.main.humidity, description: data.weather[0].description, wind_speed: data.wind.speed, } }, }), },
get_forecast: { availability: ['main'], create: () => tool({ description: 'Get 5-day weather forecast', parameters: z.object({ location: z.string().describe('City name'), days: z.number().min(1).max(5).optional().describe('Number of days (default: 3)'), }), execute: async ({ location, days = 3 }) => { const url = `https://api.openweathermap.org/data/2.5/forecast?q=${encodeURIComponent(location)}&units=${units}&cnt=${days * 8}&appid=${apiKey}` const res = await ctx.http.fetch(url) const data = await res.json() return { location: data.city.name, forecasts: data.list .filter((_: any, i: number) => i % 8 === 0) .map((entry: any) => ({ date: entry.dt_txt, temp: entry.main.temp, description: entry.weather[0].description, })), } }, }), }, }, }}Audit Log Hook Plugin
Section titled “Audit Log Hook Plugin”A plugin that logs all tool calls using hooks and storage:
export default function(ctx) { const { logLevel } = ctx.config
return { hooks: { afterToolCall: async (hookCtx) => { const entry = { timestamp: new Date().toISOString(), kinId: hookCtx.kinId, tool: hookCtx.toolName, hasError: !!hookCtx.toolResult?.error, }
if (logLevel === 'errors-only' && !entry.hasError) return
const logKey = `log:${new Date().toISOString().split('T')[0]}` const existing = await ctx.storage.get<any[]>(logKey) ?? [] existing.push(entry) await ctx.storage.set(logKey, existing) }, },
async activate() { ctx.log.info('Audit log plugin activated') }, }}Export Validation
Section titled “Export Validation”KinBot validates the object returned by your plugin’s init function before registering any tools, hooks, providers, or channels. This catches common mistakes early:
Errors (fatal — plugin won’t activate):
- Returning
null,undefined, or a non-object tools,hooks,providers, orchannelsnot being plain objectsactivateordeactivatenot being functions
Warnings (logged, plugin still activates):
- Tool missing
availabilityarray orcreatefunction - Unknown availability values (must be
'main'or'sub-kin') - Unknown hook names (e.g.
onFooinstead ofafterChat) - Hook handler that isn’t a function
- Provider missing
definition,displayName, orcapabilities - Channel missing
platform - Unknown top-level export keys
If your plugin fails to activate, check the logs for validation messages — they’ll tell you exactly what’s wrong.
Best Practices
Section titled “Best Practices”- Keep plugins focused — one plugin, one purpose
- Use
ctx.log— structured logging makes debugging easier - Handle errors gracefully — return error objects from tools instead of throwing
- Declare all HTTP domains — don’t try to bypass the permission system
- Use
ctx.storagefor state — don’t write to the filesystem - Mark secrets as
secret: true— they’re encrypted at rest - Provide good defaults — minimize required configuration
- Include a README — help users understand what your plugin does