Skip to main content

Prerequisites

Before creating a plugin, ensure you have:
  • Node.js 18+ installed
  • Basic understanding of TypeScript
  • Familiarity with async/await patterns
  • Understanding of event-driven architecture

Project Setup

1. Initialize Project

mkdir my-airi-plugin
cd my-airi-plugin
npm init -y

2. Install Dependencies

npm install @proj-airi/plugin-sdk @moeru/eventa
npm install -D typescript tsdown @types/node

3. Configure TypeScript

Create tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}

4. Configure Build

Create tsdown.config.ts:
import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm'],
  dts: true,
  clean: true
})
Update package.json:
{
  "name": "my-airi-plugin",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.mjs",
  "types": "./dist/index.d.mts",
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",
      "default": "./dist/index.mjs"
    }
  },
  "scripts": {
    "build": "tsdown",
    "dev": "tsdown --watch"
  }
}

Creating Your First Plugin

Basic Plugin Structure

Create src/index.ts:
import { definePlugin } from '@proj-airi/plugin-sdk'
import type { ContextInit } from '@proj-airi/plugin-sdk'

export default definePlugin('my-plugin', '1.0.0', () => ({
  async init(context: ContextInit) {
    console.log('Plugin initializing...')
    
    // Initialize your plugin here
    // Return false to abort initialization
    return true
  },
  
  async setupModules(context: ContextInit) {
    console.log('Setting up modules...')
    
    // Register capabilities and event handlers here
  }
}))

Plugin Manifest

Create manifest.json:
{
  "apiVersion": "v1",
  "kind": "manifest.plugin.airi.moeru.ai",
  "name": "my-plugin",
  "entrypoints": {
    "default": "./dist/index.mjs"
  }
}

Implementing Core Features

1. Configuration Handling

Plugins can receive configuration from users:
import { definePlugin } from '@proj-airi/plugin-sdk'

interface MyPluginConfig {
  apiKey: string
  endpoint: string
  timeout?: number
}

let config: MyPluginConfig | null = null

export default definePlugin('my-plugin', '1.0.0', () => ({
  async init({ channels }) {
    // Listen for configuration
    channels.host.on('module:configure', async (event) => {
      const newConfig = event.payload.config as MyPluginConfig
      
      // Validate configuration
      if (!newConfig.apiKey || !newConfig.endpoint) {
        console.error('Invalid configuration')
        return
      }
      
      config = newConfig
      console.log('Configuration applied:', config)
    })
    
    return true
  },
  
  async setupModules({ channels, apis }) {
    if (!config) {
      console.warn('Plugin not configured')
      return
    }
    
    // Use configuration
    console.log('Using endpoint:', config.endpoint)
  }
}))

2. Announcing Capabilities

Plugins announce capabilities they provide:
export default definePlugin('weather-plugin', '1.0.0', () => ({
  async init() {
    return true
  },
  
  async setupModules({ apis, channels }) {
    // Announce weather capability
    await apis.protocol.capabilities.markReady('tool:weather', {
      version: '1.0.0',
      features: ['current', 'forecast']
    })
    
    console.log('Weather capability ready')
  }
}))

3. Waiting for Dependencies

Plugins can depend on capabilities from other plugins:
export default definePlugin('weather-ui', '1.0.0', () => ({
  async init() {
    return true
  },
  
  async setupModules({ apis, channels }) {
    try {
      // Wait for weather capability (15 second timeout)
      const capability = await apis.protocol.capabilities.wait(
        'tool:weather',
        15000
      )
      
      console.log('Weather capability available:', capability)
      
      // Now safe to use weather feature
    } catch (error) {
      console.error('Weather capability not available:', error)
      // Handle missing dependency
    }
  }
}))

4. Custom Event Handlers

Define and handle custom events:
import { defineEvent } from '@moeru/eventa'

// Define custom events
const weatherQuery = defineEvent<{
  location: string
  units?: 'metric' | 'imperial'
}>('weather:query')

const weatherResponse = defineEvent<{
  location: string
  temperature: number
  condition: string
}>('weather:response')

export default definePlugin('weather-plugin', '1.0.0', () => ({
  async init() {
    return true
  },
  
  async setupModules({ channels }) {
    // Handle weather queries
    channels.host.on(weatherQuery, async (event) => {
      const { location, units = 'metric' } = event.payload
      
      // Fetch weather data
      const data = await fetchWeather(location, units)
      
      // Send response
      channels.host.emit(weatherResponse, {
        location,
        temperature: data.temp,
        condition: data.condition
      })
    })
  }
}))

async function fetchWeather(location: string, units: string) {
  // Implement weather API call
  return {
    temp: 72,
    condition: 'sunny'
  }
}

5. Provider Registration

Plugins can register providers for resources:
export default definePlugin('openai-plugin', '1.0.0', () => ({
  async init({ channels }) {
    channels.host.on('module:configure', async (event) => {
      const { apiKey } = event.payload.config
      // Store API key securely
    })
    return true
  },
  
  async setupModules({ apis, channels }) {
    // Get provider list
    const providers = await apis.client.resources.providers.list()
    console.log('Available providers:', providers)
    
    // Announce LLM provider capability
    await apis.protocol.capabilities.markReady('llm:provider:openai', {
      models: ['gpt-4', 'gpt-3.5-turbo'],
      features: ['chat', 'completion', 'streaming']
    })
  }
}))

Advanced Patterns

State Management

import { definePlugin } from '@proj-airi/plugin-sdk'

class PluginState {
  private connected = false
  private cache = new Map<string, any>()
  
  setConnected(value: boolean) {
    this.connected = value
  }
  
  isConnected() {
    return this.connected
  }
  
  setCache(key: string, value: any) {
    this.cache.set(key, value)
  }
  
  getCache(key: string) {
    return this.cache.get(key)
  }
  
  clear() {
    this.cache.clear()
  }
}

export default definePlugin('stateful-plugin', '1.0.0', () => {
  const state = new PluginState()
  
  return {
    async init({ channels }) {
      state.setConnected(true)
      return true
    },
    
    async setupModules({ channels }) {
      channels.host.on('plugin:cache:set', async (event) => {
        const { key, value } = event.payload
        state.setCache(key, value)
      })
      
      channels.host.on('plugin:cache:get', async (event) => {
        const { key } = event.payload
        return state.getCache(key)
      })
    }
  }
})

Error Handling

export default definePlugin('resilient-plugin', '1.0.0', () => ({
  async init({ channels, apis }) {
    try {
      // Attempt initialization
      await initializeResources()
      return true
    } catch (error) {
      console.error('Initialization failed:', error)
      
      // Emit status for observability
      channels.host.emit('module:status', {
        phase: 'failed',
        reason: error instanceof Error ? error.message : 'Unknown error'
      })
      
      // Abort initialization
      return false
    }
  },
  
  async setupModules({ channels }) {
    channels.host.on('custom:action', async (event) => {
      try {
        await performAction(event.payload)
      } catch (error) {
        // Handle action error gracefully
        console.error('Action failed:', error)
        channels.host.emit('custom:action:error', {
          error: error instanceof Error ? error.message : 'Unknown error'
        })
      }
    })
  }
}))

async function initializeResources() {
  // Resource initialization
}

async function performAction(payload: any) {
  // Action implementation
}

Cleanup and Lifecycle

export default definePlugin('managed-plugin', '1.0.0', () => {
  const resources: Array<() => Promise<void>> = []
  
  return {
    async init({ channels }) {
      // Track cleanup functions
      const cleanup = async () => {
        console.log('Cleaning up resources')
      }
      resources.push(cleanup)
      
      // Listen for stop event
      channels.host.on('module:stop', async () => {
        for (const clean of resources) {
          await clean()
        }
      })
      
      return true
    },
    
    async setupModules({ channels }) {
      const interval = setInterval(() => {
        // Periodic task
      }, 5000)
      
      resources.push(async () => {
        clearInterval(interval)
      })
    }
  }
})

Testing Your Plugin

Create test/plugin.test.ts:
import { PluginHost } from '@proj-airi/plugin-sdk/plugin-host'
import type { ManifestV1 } from '@proj-airi/plugin-sdk/plugin-host'
import { describe, it, expect } from 'vitest'

const manifest: ManifestV1 = {
  apiVersion: 'v1',
  kind: 'manifest.plugin.airi.moeru.ai',
  name: 'my-plugin',
  entrypoints: {
    default: './dist/index.mjs'
  }
}

describe('My Plugin', () => {
  it('should initialize successfully', async () => {
    const host = new PluginHost({
      runtime: 'node',
      transport: { kind: 'in-memory' }
    })
    
    const session = await host.start(manifest, {
      cwd: process.cwd()
    })
    
    expect(session.phase).toBe('ready')
  })
  
  it('should handle configuration', async () => {
    const host = new PluginHost()
    const session = await host.load(manifest)
    await host.init(session.id, { requireConfiguration: true })
    
    expect(session.phase).toBe('configuration-needed')
    
    await host.applyConfiguration(session.id, {
      configId: 'test',
      revision: 1,
      schemaVersion: 1,
      full: { apiKey: 'test-key' }
    })
    
    expect(session.phase).toBe('configured')
  })
})

Build and Package

# Build the plugin
npm run build

# Test the plugin
npm test

# Package for distribution
npm pack

Next Steps

Plugin API Reference

Explore the complete Plugin API reference

Plugin SDK Overview

Learn more about plugin architecture

Build docs developers (and LLMs) love