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
Createtsconfig.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
Createtsdown.config.ts:
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true
})
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
Createsrc/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
Createmanifest.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
Createtest/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
