Files
plugin-manager/USAGE.md
2026-04-18 20:13:12 +10:00

5.9 KiB

@paarrot/plugin-manager

Framework-agnostic, event-client-agnostic plugin system.


Installation

npm install git+http://synbox.ruv.wtf:8418/litruv/plugin-manager.git

Core Concepts

Class / Function Purpose
PluginRegistry Central store — manages plugins, commands, settings, themes, etc.
createPluginContext Factory that wires a PluginContext to a registry instance.
PluginContext The object passed to every plugin's onLoad.

Quick Start

1. Create the registry

import { PluginRegistry } from '@paarrot/plugin-manager';

export const registry = new PluginRegistry({
  // Use localStorage in the browser, or MemoryStorage for Node/tests
  storage: localStorage,

  // Called when a plugin registers a theme — inject the CSS however you want
  onThemeRegistered: (themeId, className, css) => {
    const el = document.createElement('style');
    el.id = `theme-${themeId}`;
    el.textContent = css;
    document.head.appendChild(el);
  },

  onThemeUnregistered: (themeId) => {
    document.getElementById(`theme-${themeId}`)?.remove();
  },
});

2. Load a plugin

import { createPluginContext } from '@paarrot/plugin-manager';

// Any object with on()/off() works as the event client —
// Node EventEmitter, matrix-js-sdk MatrixClient, socket.io, etc.
const context = createPluginContext(
  {
    pluginId: 'my-plugin',
    eventClient: myEmitter,
    onNotify: (opts) => console.log(`[${opts.title}]`, opts.body),
  },
  registry
);

registry.registerPlugin('my-plugin', plugin, context);
await plugin.onLoad(context);

3. Unload / cleanup

await registry.unregisterPlugin('my-plugin');

// Or tear everything down at once (e.g. on page unmount):
registry.clear();

Writing a Plugin

// my-plugin/index.js
module.exports = {
  name: 'My Plugin',
  version: '1.0.0',

  onLoad(ctx) {
    // Register a slash command
    ctx.commands.register({
      name: 'hello',
      description: 'Say hello',
      run: (args, ctx) => {
        ctx.log('Hello!');
        return 'Hello, world!';
      },
    });

    // Define persistent settings
    ctx.settings.define({
      enabled: { type: 'boolean', label: 'Enable feature', default: true },
      prefix:  { type: 'string',  label: 'Prefix',         default: '!'  },
    });

    // Listen to events from the event client
    ctx.events.on('Room.timeline', (event) => {
      ctx.log('Received event:', event.getType());
    });

    // Intercept outgoing messages
    ctx.messages.onBeforeSend((msg) => {
      if (msg.content.startsWith('brb')) {
        msg.content = msg.content.replace('brb', 'be right back');
      }
    });

    // Send a notification on load
    ctx.notify({ title: 'My Plugin', body: 'Loaded!', type: 'success' });
  },

  onUnload() {
    // cleanup if needed
  },

  // Other plugins can call ctx.require('my-plugin').greet()
  exports: {
    greet: (name) => `Hello, ${name}!`,
  },
};

PluginContext API Reference

commands

ctx.commands.register(command: PluginCommand): void
ctx.commands.unregister(name: string): void
ctx.commands.execute(name: string, args: Record<string, unknown>): Promise<string | void>

messages

ctx.messages.onBeforeSend(interceptor: MessageInterceptor): void
ctx.messages.onReceive(interceptor: MessageInterceptor): void

settings

ctx.settings.define(schema: SettingsSchema): void
ctx.settings.get<T>(key: string): T | undefined
ctx.settings.set(key: string, value: unknown): void

Settings are persisted to the storage adapter automatically.

themes

ctx.themes.register(theme: PluginTheme): void
ctx.themes.unregister(themeId: string): void

events

ctx.events.on(eventType: string, handler: (...args: any[]) => void): void
ctx.events.off(eventType: string, handler: (...args: any[]) => void): void

Wires to whatever eventClient you pass into createPluginContext. All handlers are cleaned up automatically on unregisterPlugin.

timers

ctx.timers.setInterval(callback, ms): number
ctx.timers.setTimeout(callback, ms): number
ctx.timers.clearInterval(id): void
ctx.timers.clearTimeout(id): void

All timers are cleared automatically on unregisterPlugin.

ui

ctx.ui.registerRenderer(type: string, renderer: CustomRenderer): void
ctx.ui.unregisterRenderer(type: string): void

notify

ctx.notify('Simple message')
ctx.notify({ title: 'Title', body: 'Body', type: 'success' | 'error' | 'warning' | 'info' })

Delegates to the onNotify callback you provide to createPluginContext.

require

const api = ctx.require('other-plugin-id');

Returns exports from another loaded plugin.

Logging

ctx.log(...args)
ctx.warn(...args)
ctx.error(...args)

Forwarded to console and stored in the registry log buffer (accessible via registry.getLogs(pluginId)).


Registry Methods

registry.processBeforeSend(msg: MessageContext): Promise<MessageContext>
registry.processReceive(msg: MessageContext): Promise<MessageContext>
registry.getCommands(): Array<{ name, pluginId, command }>
registry.getRenderer(type: string): CustomRenderer | undefined
registry.getPluginThemes(): Array<{ id, name, kind, className }>
registry.getLogs(pluginId?: string): PluginLogEntry[]
registry.clearLogs(pluginId?: string): void

Storage Adapter

The default is MemoryStorage (in-memory, not persisted). Pass any object implementing:

interface IPluginStorage {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
}

localStorage works directly in browsers.


Event Client Adapter

Pass any object implementing:

interface IPluginEventClient {
  on(event: string, handler: (...args: any[]) => void): void;
  off(event: string, handler: (...args: any[]) => void): void;
}

Compatible with: Node.js EventEmitter, matrix-js-sdk MatrixClient, socket.io sockets, and similar.