KKairos/Docs

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 zod

2. 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

FieldTypeDescription
namestringUnique name, e.g. kairos-plugin-instagram
versionstringsemver, e.g. 1.0.0
displayNamestringHuman-readable name shown in the UI
descriptionstringShort description (max 140 chars)
authorstringAuthor name or org
handlesInputTypesreadonly InputType[]Which input types this plugin handles
canHandle(input)booleanFine-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 string
  • ctx.completeStructured(prompt, schema)Structured LLM call, validated by a Zod schema
  • ctx.getConfig() / setConfig()Per-user, per-plugin config (persisted in DB)
  • ctx.getMemory() / setMemory() / updateMemory()Plugin memory — persists between invocations
  • ctx.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 task

Input types

TypeWhen usedcontent
textUser pastes plain text into the scratchpadThe raw text
urlUser pastes or shares a URLThe full URL
shareMobile share sheet (URL + optional title/text)JSON: { url, title?, text? }
voiceVoice memo from the mobile appTranscript or data: URI
fileFile 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.