> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/moeru-ai/airi/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin System

> Building plugins for Project AIRI

# Plugin System

AIRI's plugin system allows you to extend AIRI's capabilities by integrating new services, tools, and functionalities. This guide covers everything you need to know about creating, testing, and publishing AIRI plugins.

<Warning>
  The plugin SDK (`@proj-airi/plugin-sdk`) is currently a work in progress. APIs may change without warning. Check the [GitHub repository](https://github.com/moeru-ai/airi) for the latest updates.
</Warning>

## Plugin System Overview

The AIRI plugin system is built on a client-server architecture using WebSocket communication:

```mermaid theme={null}
flowchart LR
  Plugin["Plugin Instance"] <-->|WebSocket| Server["Server Runtime"]
  Server <-->|WebSocket| UI["Stage UI"]
  Server <-->|WebSocket| Plugin2["Another Plugin"]
  
  style Plugin fill:#d4f9d4
  style Plugin2 fill:#d4f9d4
  style Server fill:#f9d4d4
  style UI fill:#d4d4f9
```

### Key Concepts

<AccordionGroup>
  <Accordion title="Plugin">
    A self-contained module that extends AIRI's functionality. Plugins connect to the server runtime via WebSocket and can:

    * Receive configuration from UI
    * Send events to other modules
    * Expose tools and capabilities
    * Integrate external services
  </Accordion>

  <Accordion title="Server Runtime">
    The central communication hub (`@proj-airi/server-runtime`) that:

    * Routes messages between plugins and UI
    * Manages plugin lifecycle
    * Handles authentication
    * Maintains plugin registry
  </Accordion>

  <Accordion title="Plugin SDK">
    The SDK (`@proj-airi/plugin-sdk`) provides:

    * WebSocket client abstraction
    * Event handling utilities
    * Type-safe communication
    * State machine for lifecycle management
  </Accordion>

  <Accordion title="Plugin Protocol">
    The protocol (`@proj-airi/plugin-protocol`) defines:

    * Message formats
    * Event types
    * Metadata structure
    * Routing rules
  </Accordion>
</AccordionGroup>

## Plugin Architecture

### Communication Flow

```mermaid theme={null}
sequenceDiagram
  participant Plugin
  participant SDK
  participant ServerRuntime
  participant StageUI
  
  Plugin->>SDK: Initialize
  SDK->>ServerRuntime: Connect WebSocket
  ServerRuntime-->>SDK: Connection established
  SDK->>ServerRuntime: module:authenticate
  ServerRuntime-->>SDK: module:authenticated
  SDK->>ServerRuntime: module:announce
  ServerRuntime->>StageUI: registry:modules:sync
  StageUI->>ServerRuntime: ui:configure
  ServerRuntime->>SDK: module:configure
  SDK->>Plugin: onConfigure callback
  Plugin->>SDK: Send custom event
  SDK->>ServerRuntime: custom:event
  ServerRuntime->>StageUI: custom:event (routed)
```

### Plugin Lifecycle

1. **Initialize**: Plugin creates SDK client instance
2. **Connect**: WebSocket connection established
3. **Authenticate**: Plugin sends auth token (if required)
4. **Announce**: Plugin announces its presence and capabilities
5. **Configure**: Plugin receives configuration from UI
6. **Active**: Plugin sends/receives events
7. **Disconnect**: WebSocket connection closed

## Creating a Plugin

### Project Setup

<Steps>
  <Step title="Create Plugin Directory">
    Create a new directory in the `plugins/` folder:

    ```bash theme={null}
    mkdir plugins/airi-plugin-my-plugin
    cd plugins/airi-plugin-my-plugin
    ```
  </Step>

  <Step title="Initialize Package">
    Create a `package.json`:

    ```json theme={null}
    {
      "name": "@proj-airi/airi-plugin-my-plugin",
      "version": "0.8.5-beta.4",
      "type": "module",
      "private": true,
      "main": "./dist/index.mjs",
      "types": "./dist/index.d.mts",
      "scripts": {
        "dev": "tsx ./src/index.ts",
        "build": "tsdown",
        "typecheck": "tsc --noEmit"
      },
      "dependencies": {
        "@proj-airi/plugin-sdk": "workspace:^",
        "@proj-airi/server-sdk": "workspace:^"
      },
      "devDependencies": {
        "tsdown": "catalog:",
        "tsx": "^4.21.0",
        "typescript": "~5.9.3"
      }
    }
    ```
  </Step>

  <Step title="Create TypeScript Config">
    Create `tsconfig.json`:

    ```json theme={null}
    {
      "extends": "../../tsconfig.json",
      "compilerOptions": {
        "outDir": "./dist",
        "rootDir": "./src"
      },
      "include": ["src/**/*"]
    }
    ```
  </Step>

  <Step title="Create Build Config">
    Create `tsdown.config.ts`:

    ```typescript theme={null}
    import { defineConfig } from 'tsdown'

    export default defineConfig({
      entry: ['src/index.ts'],
      format: ['esm'],
      dts: true,
      clean: true,
    })
    ```
  </Step>
</Steps>

### Basic Plugin Implementation

Create `src/index.ts`:

```typescript theme={null}
import { Client } from '@proj-airi/server-sdk'

// Define plugin metadata
const PLUGIN_ID = 'my-plugin'
const PLUGIN_VERSION = '0.1.0'

// Create client
const client = new Client({
  name: PLUGIN_ID,
  url: process.env.SERVER_URL || 'ws://localhost:3000/ws',
  auth: process.env.AUTH_TOKEN,
})

// Handle configuration
client.on('module:configure', (event) => {
  const config = event.data.config
  console.log('Received configuration:', config)
  
  // Apply configuration
  applyConfig(config)
})

// Handle custom events
client.on('custom:trigger', async (event) => {
  console.log('Custom event received:', event.data)
  
  // Do something
  const result = await performAction(event.data)
  
  // Send response
  client.send({
    type: 'custom:response',
    data: { result },
    metadata: {
      event: {
        id: generateId(),
        parentId: event.metadata?.event.id,
      },
      source: {
        kind: 'plugin',
        plugin: {
          id: PLUGIN_ID,
          version: PLUGIN_VERSION,
        },
        id: client.instanceId,
      },
    },
  })
})

// Connect
await client.connect()

// Announce plugin
await client.announce({
  name: PLUGIN_ID,
  identity: {
    kind: 'plugin',
    plugin: {
      id: PLUGIN_ID,
      version: PLUGIN_VERSION,
    },
    id: client.instanceId,
  },
})

console.log(`Plugin ${PLUGIN_ID} started successfully`)

// Helper functions
function applyConfig(config: Record<string, unknown>) {
  // Apply configuration to your plugin
}

async function performAction(data: unknown) {
  // Your plugin logic here
  return { success: true }
}

function generateId() {
  return Math.random().toString(36).substring(7)
}
```

### Advanced Plugin Features

<Tabs>
  <Tab title="State Management">
    Use XState for complex plugin state:

    ```typescript theme={null}
    import { createMachine, interpret } from 'xstate'

    const pluginMachine = createMachine({
      id: 'plugin',
      initial: 'idle',
      states: {
        idle: {
          on: { START: 'running' }
        },
        running: {
          on: { STOP: 'idle', ERROR: 'error' }
        },
        error: {
          on: { RETRY: 'running' }
        },
      },
    })

    const service = interpret(pluginMachine)
    service.start()

    // React to state changes
    service.onTransition((state) => {
      console.log('State:', state.value)
    })
    ```
  </Tab>

  <Tab title="Event Routing">
    Control where your events are sent:

    ```typescript theme={null}
    // Send to specific modules
    client.send({
      type: 'custom:notification',
      data: { message: 'Hello!' },
      route: {
        destinations: [
          { module: 'stage-ui' },
          { module: 'another-plugin', index: 0 },
        ],
      },
    })

    // Broadcast to all
    client.send({
      type: 'custom:broadcast',
      data: { announcement: 'Important!' },
      // No route = broadcast to all authenticated peers
    })
    ```
  </Tab>

  <Tab title="Error Handling">
    Handle errors gracefully:

    ```typescript theme={null}
    client.on('error', (error) => {
      console.error('Client error:', error)
      
      // Attempt reconnection
      setTimeout(() => {
        client.connect()
      }, 5000)
    })

    client.on('close', (event) => {
      console.log('Connection closed:', event)
      
      // Cleanup and reconnect logic
    })
    ```
  </Tab>

  <Tab title="Heartbeat">
    Maintain connection with heartbeat:

    ```typescript theme={null}
    // The SDK handles heartbeat automatically
    // But you can customize it:

    const client = new Client({
      name: PLUGIN_ID,
      heartbeat: {
        interval: 30000, // 30 seconds
        timeout: 60000,  // 60 seconds
      },
    })
    ```
  </Tab>
</Tabs>

## Plugin Examples

### Example 1: Web Extension Plugin

The `airi-plugin-web-extension` shows how to create a browser extension plugin:

```typescript theme={null}
// Background script
import { Client } from '@proj-airi/server-sdk'

const client = new Client({
  name: 'web-extension',
  url: 'ws://localhost:3000/ws',
})

// Listen for page content
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete') {
    // Send page info to AIRI
    client.send({
      type: 'extension:page-loaded',
      data: {
        url: tab.url,
        title: tab.title,
      },
    })
  }
})

// Receive commands from AIRI
client.on('extension:open-tab', (event) => {
  chrome.tabs.create({ url: event.data.url })
})
```

### Example 2: Claude Code Integration

The `airi-plugin-claude-code` demonstrates CLI tool integration:

```typescript theme={null}
import { spawn } from 'child_process'
import { Client } from '@proj-airi/server-sdk'

const client = new Client({
  name: 'claude-code',
  url: process.env.SERVER_URL || 'ws://localhost:3000/ws',
})

// Handle code execution requests
client.on('claude:execute', async (event) => {
  const { command, args } = event.data
  
  const process = spawn(command, args, {
    cwd: event.data.cwd || process.cwd(),
  })
  
  // Stream output
  process.stdout.on('data', (data) => {
    client.send({
      type: 'claude:output',
      data: {
        stream: 'stdout',
        content: data.toString(),
      },
    })
  })
  
  process.stderr.on('data', (data) => {
    client.send({
      type: 'claude:output',
      data: {
        stream: 'stderr',
        content: data.toString(),
      },
    })
  })
  
  process.on('close', (code) => {
    client.send({
      type: 'claude:complete',
      data: { exitCode: code },
    })
  })
})
```

### Example 3: Home Assistant Integration

The `airi-plugin-homeassistant` is currently WIP but will demonstrate:

* Connecting to Home Assistant API
* Controlling smart home devices
* Receiving sensor data
* Automating routines

<Note>
  Check the `plugins/` directory for complete, working examples of each plugin type.
</Note>

## Plugin SDK API

### Client Class

```typescript theme={null}
class Client {
  constructor(options: ClientOptions)
  
  // Connection
  connect(): Promise<void>
  disconnect(): void
  
  // Lifecycle
  announce(data: AnnounceData): Promise<void>
  
  // Events
  send(event: WebSocketEvent): void
  on(type: string, handler: (event: WebSocketEvent) => void): void
  off(type: string, handler?: (event: WebSocketEvent) => void): void
  
  // Properties
  readonly instanceId: string
  readonly connected: boolean
}
```

### Client Options

```typescript theme={null}
interface ClientOptions {
  name: string              // Plugin name
  url: string               // WebSocket URL
  auth?: string             // Authentication token
  index?: number            // Plugin index (for multiple instances)
  heartbeat?: {
    interval?: number       // Heartbeat interval (ms)
    timeout?: number        // Heartbeat timeout (ms)
  }
}
```

### Event Structure

```typescript theme={null}
interface WebSocketEvent<T = Record<string, unknown>> {
  type: string              // Event type (e.g., 'custom:action')
  data: T                   // Event payload
  metadata?: {
    event: {
      id: string            // Unique event ID
      parentId?: string     // Parent event ID (for request/response)
    }
    source: MetadataEventSource  // Source information
  }
  route?: {
    destinations?: Destination[]  // Target modules
    bypass?: boolean        // Bypass routing (devtools only)
  }
}
```

### Standard Event Types

| Event Type                       | Direction            | Purpose                   |
| -------------------------------- | -------------------- | ------------------------- |
| `module:authenticate`            | Plugin → Server      | Authenticate with token   |
| `module:authenticated`           | Server → Plugin      | Authentication successful |
| `module:announce`                | Plugin → Server      | Announce plugin presence  |
| `module:configure`               | Server → Plugin      | Receive configuration     |
| `registry:modules:sync`          | Server → Plugin      | Module registry update    |
| `ui:configure`                   | UI → Server → Plugin | UI configuration update   |
| `transport:connection:heartbeat` | Bidirectional        | Keep connection alive     |

## Testing Plugins

### Unit Testing

```typescript theme={null}
import { describe, it, expect, vi } from 'vitest'
import { Client } from '@proj-airi/server-sdk'

describe('MyPlugin', () => {
  it('should handle configuration', async () => {
    const client = new Client({
      name: 'test-plugin',
      url: 'ws://localhost:3000/ws',
    })
    
    // Mock event handler
    const handler = vi.fn()
    client.on('module:configure', handler)
    
    // Simulate configuration event
    client.emit('module:configure', {
      type: 'module:configure',
      data: { config: { enabled: true } },
    })
    
    expect(handler).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { config: { enabled: true } },
      })
    )
  })
})
```

### Integration Testing

```bash theme={null}
# Start server runtime
pnpm -F @proj-airi/server-runtime dev

# In another terminal, start your plugin
pnpm -F @proj-airi/airi-plugin-my-plugin dev

# In another terminal, start Stage UI
pnpm dev:tamagotchi
```

Test the full flow:

1. Plugin connects and announces
2. UI sees plugin in registry
3. UI sends configuration
4. Plugin receives and applies configuration
5. Plugin sends events
6. UI receives and displays events

## Best Practices

<CardGroup cols={2}>
  <Card title="Error Handling" icon="shield">
    Always handle connection errors and implement reconnection logic. Don't let your plugin crash on network issues.
  </Card>

  <Card title="Type Safety" icon="check">
    Use TypeScript and define strict types for your event data. This prevents runtime errors and improves DX.
  </Card>

  <Card title="Event Naming" icon="tag">
    Use consistent naming: `<plugin-name>:<action>` (e.g., `weather:update`, `music:play`).
  </Card>

  <Card title="Logging" icon="file-text">
    Use structured logging with context. This helps debugging in production.
  </Card>

  <Card title="Configuration" icon="sliders">
    Make your plugin configurable. Don't hardcode values that users might want to change.
  </Card>

  <Card title="Documentation" icon="book">
    Document your plugin's events, configuration options, and requirements in a README.
  </Card>
</CardGroup>

### Code Style

<Tabs>
  <Tab title="Event Handlers">
    ```typescript theme={null}
    // ✅ Good: Specific, focused handlers
    client.on('weather:update', handleWeatherUpdate)
    client.on('weather:forecast', handleForecast)

    // ❌ Bad: Generic catch-all handler
    client.on('*', handleAllEvents)
    ```
  </Tab>

  <Tab title="Error Messages">
    ```typescript theme={null}
    // ✅ Good: Descriptive error messages
    throw new Error('Failed to fetch weather data: API key invalid')

    // ❌ Bad: Vague error messages
    throw new Error('Error')
    ```
  </Tab>

  <Tab title="Configuration">
    ```typescript theme={null}
    // ✅ Good: Validated configuration with defaults
    const config = validateConfig(event.data.config, {
      apiKey: process.env.API_KEY || '',
      refreshInterval: 60000,
    })

    // ❌ Bad: Direct usage without validation
    const apiKey = event.data.config.apiKey
    ```
  </Tab>
</Tabs>

## Publishing Plugins

### Internal Plugins

Internal plugins (in `plugins/` directory):

1. Follow monorepo conventions
2. Use `workspace:^` for internal dependencies
3. Add to workspace in root `package.json`
4. Document in main README

### External Plugins

For external/community plugins:

<Steps>
  <Step title="Create Standalone Repository">
    Create a new repository following the plugin template structure.
  </Step>

  <Step title="Publish to npm">
    ```bash theme={null}
    npm publish
    ```

    Use scoped package name: `@your-org/airi-plugin-name`
  </Step>

  <Step title="Document Installation">
    Provide clear installation and usage instructions:

    ```bash theme={null}
    npm install @your-org/airi-plugin-name
    ```
  </Step>

  <Step title="Submit to Plugin Registry">
    (Future feature) Submit your plugin to the AIRI plugin registry.
  </Step>
</Steps>

### Plugin Distribution

```json theme={null}
// package.json for distribution
{
  "name": "@your-org/airi-plugin-name",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.mjs",
  "types": "./dist/index.d.mts",
  "bin": {
    "airi-plugin-name": "./dist/cli.mjs"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "peerDependencies": {
    "@proj-airi/plugin-sdk": "^0.8.0",
    "@proj-airi/server-sdk": "^0.8.0"
  }
}
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Plugin doesn't connect">
    **Check:**

    * Server runtime is running (`pnpm -F @proj-airi/server-runtime dev`)
    * WebSocket URL is correct
    * No firewall blocking WebSocket connections
    * Authentication token matches (if auth enabled)

    **Debug:**

    ```bash theme={null}
    # Enable debug logging
    LOG_LEVEL=debug pnpm -F @proj-airi/server-runtime dev
    ```
  </Accordion>

  <Accordion title="Events not received">
    **Check:**

    * Plugin announced itself with `client.announce()`
    * Event type matches exactly (case-sensitive)
    * Event routing destinations are correct
    * Plugin is authenticated (if auth enabled)

    **Debug:**

    ```typescript theme={null}
    client.on('*', (event) => {
      console.log('Received event:', event.type, event.data)
    })
    ```
  </Accordion>

  <Accordion title="Connection drops">
    **Check:**

    * Heartbeat is enabled and working
    * Network is stable
    * Server runtime hasn't crashed

    **Fix:**

    ```typescript theme={null}
    client.on('close', async () => {
      console.log('Connection closed, reconnecting...')
      await new Promise(resolve => setTimeout(resolve, 5000))
      await client.connect()
    })
    ```
  </Accordion>

  <Accordion title="Type errors">
    **Check:**

    * Using latest `@proj-airi/plugin-sdk` version
    * TypeScript version is \~5.9.3
    * Event data matches expected types

    **Fix:**

    ```bash theme={null}
    pnpm install
    pnpm typecheck
    ```
  </Accordion>
</AccordionGroup>

## Resources

<CardGroup cols={2}>
  <Card title="Plugin SDK Repo" icon="github" href="https://github.com/moeru-ai/airi/tree/main/packages/plugin-sdk">
    Browse the plugin SDK source code
  </Card>

  <Card title="Example Plugins" icon="code" href="https://github.com/moeru-ai/airi/tree/main/plugins">
    Study working plugin implementations
  </Card>

  <Card title="Server Runtime" icon="server" href="https://github.com/moeru-ai/airi/tree/main/packages/server-runtime">
    Understand the server architecture
  </Card>

  <Card title="Join Discord" icon="discord" href="https://discord.gg/TgQ3Cu2F7A">
    Get help from the community
  </Card>
</CardGroup>

## Next Steps

<Steps>
  <Step title="Review Architecture">
    Understand the [architecture overview](/development/architecture) first.
  </Step>

  <Step title="Set Up Development">
    Follow the [contributing guide](/development/contributing) to set up your environment.
  </Step>

  <Step title="Build Your Plugin">
    Start with a simple plugin and gradually add features.
  </Step>

  <Step title="Share Your Work">
    Publish your plugin and share it with the community!
  </Step>
</Steps>
