Building plugins
A Kairos plugin is a TypeScript module that takes a scratchpad input and returns candidate tasks. Plugins are the only way to add new input handlers — the core stays small by design.
1. Install the SDK
pnpm add @kairos/plugin-sdk zod2. Implement your plugin
import { definePlugin, createParseResult } from '@kairos/plugin-sdk';
import { z } from 'zod';
const TasksSchema = z.object({
tasks: z.array(z.object({
title: z.string(),
tags: z.array(z.string()).default([]),
priority: z.number().int().min(1).max(4).default(3),
})),
});
export default definePlugin({
name: 'my-plugin',
version: '1.0.0',
displayName: 'My Plugin',
description: 'Extracts tasks from my custom source.',
author: 'Your Name',
handlesInputTypes: ['text'] as const,
canHandle(input) {
return input.inputType === 'text';
},
async parse(input, ctx) {
const { tasks } = await ctx.completeStructured(
`Extract tasks from: ${input.content}`,
TasksSchema,
);
return createParseResult({
pluginName: 'my-plugin',
pluginVersion: '1.0.0',
tasks,
});
},
});3. Test it
Use createMockContext from @kairos/plugin-sdk/testing to stub LLM calls:
import { describe, it, expect } from 'vitest';
import { createMockContext } from '@kairos/plugin-sdk/testing';
import plugin from '../src/index.js';
describe('my-plugin', () => {
it('extracts tasks from text', async () => {
const ctx = createMockContext({
completeStructuredResponse: {
tasks: [{ title: 'Write tests', tags: ['dev'], priority: 2 }],
},
});
const input = {
id: 'test-1', userId: 'u1',
inputType: 'text' as const,
content: 'We need to write tests for the new feature.',
payload: {}, createdAt: new Date(),
};
const result = await plugin.parse(input, ctx);
expect(result.tasks).toHaveLength(1);
expect(result.tasks[0].title).toBe('Write tests');
});
});Plugin contract
| Field | Type | Description |
|---|---|---|
| name | string | Unique name, e.g. kairos-plugin-instagram |
| version | string | semver, e.g. 1.0.0 |
| displayName | string | Human-readable name shown in the UI |
| description | string | Short description (max 140 chars) |
| author | string | Author name or org |
| handlesInputTypes | readonly InputType[] | Which input types this plugin handles |
| canHandle(input) | boolean | Fine-grained check (e.g. URL domain match) |
| parse(input, ctx) | Promise<ParseResult> | Main extraction method |
| onInstall?(ctx) | Promise<void> | Optional hook called once on install |
| onUninstall?(ctx) | Promise<void> | Optional hook called once on uninstall |
PluginContext
Every plugin receives a PluginContext that provides:
ctx.complete(prompt)Call the user's configured LLM, returns a stringctx.completeStructured(prompt, schema)Structured LLM call, validated by a Zod schemactx.getConfig() / setConfig()Per-user, per-plugin config (persisted in DB)ctx.getMemory() / setMemory() / updateMemory()Plugin memory — persists between invocationsctx.getRulesets()User-defined rules evaluated inside parse()ctx.log(level, message)Structured logging
Rulesets
Rulesets are user-defined JSON rules stored per plugin that let users tweak extraction without code. Evaluate them inside parse(), after the LLM call.
// User-defined ruleset example (stored in DB per user per plugin)
{
"if": { "contains": "health" },
"then": { "tag": "health", "durationMins": 30 }
}
// Apply in your plugin:
const rulesets = await ctx.getRulesets();
// evaluate them against each extracted taskInput types
| Type | When used | content |
|---|---|---|
| text | User pastes plain text into the scratchpad | The raw text |
| url | User pastes or shares a URL | The full URL |
| share | Mobile share sheet (URL + optional title/text) | JSON: { url, title?, text? } |
| voice | Voice memo from the mobile app | Transcript or data: URI |
| file | File upload (PDF, image, etc.) | data: URI or file path |
Example plugins
See examples/plugins/ in the Kairos repo for full reference implementations: Instagram, Twitter/X, Readwise, and Voice Memo.