# @paarrot/plugin-manager Framework-agnostic, event-client-agnostic plugin system. --- ## Installation ```bash 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 ```ts 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 ```ts 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 ```ts await registry.unregisterPlugin('my-plugin'); // Or tear everything down at once (e.g. on page unmount): registry.clear(); ``` --- ## Writing a Plugin ```js // 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` ```ts ctx.commands.register(command: PluginCommand): void ctx.commands.unregister(name: string): void ctx.commands.execute(name: string, args: Record): Promise ``` ### `messages` ```ts ctx.messages.onBeforeSend(interceptor: MessageInterceptor): void ctx.messages.onReceive(interceptor: MessageInterceptor): void ``` ### `settings` ```ts ctx.settings.define(schema: SettingsSchema): void ctx.settings.get(key: string): T | undefined ctx.settings.set(key: string, value: unknown): void ``` Settings are persisted to the storage adapter automatically. ### `themes` ```ts ctx.themes.register(theme: PluginTheme): void ctx.themes.unregister(themeId: string): void ``` ### `events` ```ts 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` ```ts 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` ```ts ctx.ui.registerRenderer(type: string, renderer: CustomRenderer): void ctx.ui.unregisterRenderer(type: string): void ``` ### `notify` ```ts 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` ```ts const api = ctx.require('other-plugin-id'); ``` Returns `exports` from another loaded plugin. ### Logging ```ts 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 ```ts registry.processBeforeSend(msg: MessageContext): Promise registry.processReceive(msg: MessageContext): Promise 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: ```ts 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: ```ts 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.