# Plugin Authoring Guide A comprehensive guide to creating Fusion plugins that extend the task board with custom tools, routes, and lifecycle hooks. ## Table of Contents 1. [Getting Started](#1-getting-started) 2. [Plugin Manifest Reference](#2-plugin-manifest-reference) 3. [Plugin Settings Schema](#3-plugin-settings-schema) 4. [Available Hooks and Signatures](#4-available-hooks-and-signatures) 5. [Registering Tools](#5-registering-tools) 6. [Registering Routes](#6-registering-routes) 7. [Plugin Context API Reference](#7-plugin-context-api-reference) 8. [Plugin Lifecycle States](#8-plugin-lifecycle-states) 9. [Testing Plugins](#9-testing-plugins) 10. [Publishing Plugins](#10-publishing-plugins) 11. [Example Plugins](#11-example-plugins) --- ## 1. Getting Started ### What Are Fusion Plugins? Fusion plugins extend the task board with custom functionality: - **Lifecycle Hooks**: React to task creation, movement, completion, and errors - **AI Agent Tools**: Add custom tools that AI agents can use during task execution - **Custom API Routes**: Create dashboard API endpoints for frontend integration - **Settings**: Accept user configuration via typed settings schemas ### Prerequisites - Node.js 18+ - TypeScript familiarity - A Fusion project with the plugin system installed ### Quick Start Create a new plugin using the scaffold command: ```bash fn plugin create my-first-plugin cd my-first-plugin pnpm install pnpm test ``` ### Plugin Project Structure ``` my-plugin/ ├── package.json # Plugin metadata + "fusion-plugin" keyword ├── tsconfig.json # TypeScript configuration ├── vitest.config.ts # Test configuration ├── src/ │ ├── index.ts # Plugin entry point (exports default FusionPlugin) │ └── __tests__/ │ └── index.test.ts # Plugin tests └── README.md # Plugin documentation ``` --- ## 2. Plugin Manifest Reference The manifest defines your plugin's metadata and capabilities: ```typescript import type { PluginManifest } from "@fusion/plugin-sdk"; const manifest: PluginManifest = { id: "my-custom-plugin", // Unique identifier (kebab-case) name: "My Custom Plugin", // Human-readable name version: "1.0.0", // Semver version description: "Does something useful", author: "Your Name", homepage: "https://github.com/you/plugin", fusionVersion: ">=1.0.0", // Optional: minimum Fusion version dependencies: [], // Optional: plugin IDs this depends on settingsSchema: { /* ... */ }, // Optional: configuration schema }; ``` ### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `id` | string | Yes | Unique identifier (kebab-case, validated) | | `name` | string | Yes | Human-readable display name | | `version` | string | Yes | Semver version (e.g., "1.0.0") | | `description` | string | No | Short description | | `author` | string | No | Author name or organization | | `homepage` | string | No | URL to documentation or repository | | `fusionVersion` | string | No | Minimum Fusion version required | | `dependencies` | string[] | No | IDs of plugins this depends on | | `settingsSchema` | Record | No | Configuration schema | --- ## 3. Plugin Settings Schema Settings allow users to configure your plugin through the dashboard: ```typescript import type { PluginSettingSchema } from "@fusion/plugin-sdk"; const settingsSchema: Record = { webhookUrl: { type: "string", label: "Webhook URL", description: "URL to send notifications to", required: true, }, maxRetries: { type: "number", label: "Max Retries", description: "Maximum number of retry attempts", defaultValue: 3, }, enabled: { type: "boolean", label: "Enable Feature", description: "Toggle the feature on/off", defaultValue: true, }, severity: { type: "enum", label: "Log Severity", description: "Minimum severity level to log", enumValues: ["debug", "info", "warn", "error"], defaultValue: "info", }, }; ``` ### Setting Types | Type | Description | Extra Fields | |------|-------------|--------------| | `"string"` | Text input | — | | `"number"` | Numeric input | — | | `"boolean"` | Toggle switch | — | | `"enum"` | Dropdown select | `enumValues: string[]` | ### Accessing Settings Settings are available in hooks via `ctx.settings`: ```typescript hooks: { onLoad: (ctx) => { const webhookUrl = ctx.settings.webhookUrl as string; if (!webhookUrl) { ctx.logger.warn("No webhook URL configured"); } }, }, ``` --- ## 4. Available Hooks and Signatures Hooks let your plugin react to events in the Fusion system: ```typescript import type { FusionPlugin, PluginContext } from "@fusion/plugin-sdk"; const plugin: FusionPlugin = { manifest: { /* ... */ }, state: "installed", hooks: { onLoad: async (ctx) => { ctx.logger.info("Plugin loaded!"); }, onTaskCreated: async (task, ctx) => { ctx.logger.info(`New task: ${task.title}`); }, // ... other hooks }, }; ``` ### Hook Reference | Hook | Signature | When It Fires | |------|-----------|---------------| | `onLoad` | `(ctx: PluginContext) => Promise \| void` | Plugin first loaded and started | | `onUnload` | `() => Promise \| void` | Plugin stopped/shutdown | | `onTaskCreated` | `(task: Task, ctx: PluginContext) => Promise \| void` | New task created | | `onTaskMoved` | `(task: Task, fromColumn: string, toColumn: string, ctx: PluginContext) => Promise \| void` | Task moved between columns | | `onTaskCompleted` | `(task: Task, ctx: PluginContext) => Promise \| void` | Task reached "done" | | `onError` | `(error: Error, ctx: PluginContext) => Promise \| void` | Error occurred in plugin execution | ### Hook Behavior - **Timeout**: 5 seconds per invocation (logged and skipped if exceeded) - **Error Isolation**: Hook failures never block the host system - **Optional**: Only define the hooks you need ### Example: Notification on Task Completion ```typescript hooks: { onTaskCompleted: async (task, ctx) => { const webhookUrl = ctx.settings.webhookUrl as string; if (!webhookUrl) return; await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `✅ Task completed: ${task.title || task.id}`, }), }); }, }, ``` --- ## 5. Registering Tools Tools extend AI agents with custom capabilities: ```typescript import type { FusionPlugin, PluginToolDefinition, PluginToolResult } from "@fusion/plugin-sdk"; const myTool: PluginToolDefinition = { name: "my_custom_tool", description: "Does something useful with input text", parameters: { type: "object", properties: { input: { type: "string", description: "The text to process", }, }, required: ["input"], }, execute: async (params, ctx) => { const input = params.input as string; // Do something useful... const result = input.toUpperCase(); return { content: [{ type: "text", text: result }], }; }, }; const plugin: FusionPlugin = { manifest: { /* ... */ }, state: "installed", tools: [myTool], }; ``` ### Tool Naming - Use a unique name prefixed with your plugin ID (e.g., `my-plugin_action`) - Avoid conflicts with built-in tools ### Tool Result Format ```typescript interface PluginToolResult { content: Array<{ type: "text"; text: string }>; isError?: boolean; details?: Record; } ``` --- ## 6. Registering Routes Routes create custom API endpoints in the dashboard: ```typescript import type { FusionPlugin, PluginRouteDefinition } from "@fusion/plugin-sdk"; const routes: PluginRouteDefinition[] = [ { method: "GET", path: "/status", description: "Get plugin status", handler: async (req, ctx) => { return { status: "ok", uptime: process.uptime() }; }, }, { method: "POST", path: "/action", description: "Perform an action", handler: async (req, ctx) => { // Access request body const body = req as { action?: string }; ctx.logger.info(`Action: ${body.action}`); return { success: true }; }, }, ]; const plugin: FusionPlugin = { manifest: { /* ... */ }, state: "installed", routes, }; ``` ### Route Mounting Routes are mounted at `/api/plugins/{pluginId}/{path}`: - Plugin ID: `fusion-plugin-notification` - Route path: `/status` - Full URL: `/api/plugins/fusion-plugin-notification/status` ### Supported Methods - `GET` - `POST` - `PUT` - `DELETE` --- ## 7. Plugin Context API Reference The context object is passed to hooks, tools, and route handlers: ```typescript interface PluginContext { pluginId: string; taskStore: TaskStore; settings: Record; logger: PluginLogger; emitEvent: (event: string, data: unknown) => void; } ``` ### Properties | Property | Type | Description | |----------|------|-------------| | `pluginId` | `string` | Your plugin's unique ID | | `taskStore` | `TaskStore` | Access to task data (read-only) | | `settings` | `Record` | User configuration (merged with defaults) | | `logger` | `PluginLogger` | Structured logging | | `emitEvent` | `(event, data) => void` | Emit custom events | ### Logger Methods ```typescript interface PluginLogger { info(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; debug(message: string, ...args: unknown[]): void; } ``` ### Example: Using the Context ```typescript hooks: { onLoad: (ctx) => { ctx.logger.info("Plugin starting..."); // Access settings const apiKey = ctx.settings.apiKey as string; // Emit custom event ctx.emitEvent("my-plugin:ready", { timestamp: Date.now() }); }, }, ``` --- ## 8. Plugin Lifecycle States Plugins transition through these states: ``` ┌────────────┐ │ installed │ (registered, not loaded) └─────┬──────┘ │ enable ▼ ┌────────────┐ │ started │ ←─────┐ (loaded, hooks active) └─────┬──────┘ │ │ │ load │ stop │ ▼ │ ┌────────────┐ │ │ stopped │ ──────┘ └────────────┘ Any state can transition to: ┌────────────┐ │ error │ (load failure or runtime error) └────────────┘ ``` ### State Descriptions | State | Description | |-------|-------------| | `installed` | Plugin registered but not yet loaded | | `started` | Plugin loaded and hooks active | | `stopped` | Plugin shut down gracefully | | `error` | Plugin failed during load or execution | --- ## 9. Testing Plugins Use Vitest for unit testing your plugins: ### Test Structure ```typescript // src/__tests__/index.test.ts import { describe, it, expect, vi, beforeEach } from "vitest"; import plugin from "../index.js"; describe("my plugin", () => { beforeEach(() => { vi.clearAllMocks(); }); it("should export a valid plugin", () => { expect(plugin.manifest.id).toBe("my-plugin"); expect(plugin.manifest.name).toBeDefined(); }); it("should call onLoad hook", async () => { const mockCtx = { pluginId: "my-plugin", settings: {}, logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, emitEvent: vi.fn(), taskStore: {}, }; await plugin.hooks.onLoad?.(mockCtx as any); expect(mockCtx.logger.info).toHaveBeenCalled(); }); }); ``` ### Testing Tools ```typescript it("should return correct result from tool", async () => { const tool = plugin.tools![0]; const mockCtx = { /* ... */ }; const result = await tool.execute({ input: "hello" }, mockCtx as any); expect(result.content[0].text).toBe("HELLO"); }); ``` ### Testing Routes ```typescript it("should return status from GET /status", async () => { const route = plugin.routes!.find(r => r.path === "/status"); const req = { params: {}, method: "GET", url: "/status" }; const ctx = { /* ... */ }; const result = await route.handler(req as any, ctx as any); expect(result).toHaveProperty("status"); }); ``` ### Running Tests ```bash pnpm test ``` --- ## 10. Publishing Plugins ### Package Requirements ```json { "name": "fusion-plugin-my-plugin", "version": "1.0.0", "keywords": ["fusion-plugin"], "exports": { ".": { "types": "./src/index.ts", "import": "./dist/index.js" } }, "peerDependencies": { "@fusion/core": "workspace:*" } } ``` ### Publishing Steps 1. Update `package.json`: - Set `name` to `fusion-plugin-*` or `@scope/fusion-plugin-*` - Add `"keywords": ["fusion-plugin"]` - Set `"private": false` 2. Build the plugin: ```bash pnpm build ``` 3. Publish to npm: ```bash npm publish --access public ``` ### Installation Users can install your plugin via CLI: ```bash fn plugin install fusion-plugin-my-plugin # or fn plugin install @scope/fusion-plugin-my-plugin ``` Or by copying to the plugins directory: ```bash cp -r fusion-plugin-my-plugin ~/.fusion/plugins/ ``` --- ## 11. Example Plugins Explore these reference implementations: ### [Notification Plugin](../../plugins/examples/fusion-plugin-notification/) Sends webhook notifications on task lifecycle events (Slack, Discord, generic HTTP). - Demonstrates: `onLoad`, `onTaskCompleted`, `onTaskMoved`, `onError` hooks - Features: Settings schema, webhook formatting, event filtering ### [Auto-Label Plugin](../../plugins/examples/fusion-plugin-auto-label/) Automatically labels tasks based on description content using keyword matching. - Demonstrates: `onTaskCreated` hook, AI agent tools - Features: Text classification, event emission, tool registration ### [CI Status Plugin](../../plugins/examples/fusion-plugin-ci-status/) Polls CI status for branches and provides custom API endpoints. - Demonstrates: Custom routes, periodic background work, route handlers - Features: `onLoad`/`onUnload` lifecycle, `setInterval` polling, REST API --- ## Quick Reference ### Minimal Plugin ```typescript import { definePlugin } from "@fusion/plugin-sdk"; export default definePlugin({ manifest: { id: "my-plugin", name: "My Plugin", version: "1.0.0", }, state: "installed", hooks: { onLoad: (ctx) => { ctx.logger.info("Hello from my plugin!"); }, }, }); ``` ### Full Plugin Example ```typescript import { definePlugin } from "@fusion/plugin-sdk"; import type { FusionPlugin, PluginContext } from "@fusion/plugin-sdk"; export default definePlugin({ manifest: { id: "my-full-plugin", name: "My Full Plugin", version: "1.0.0", description: "A complete example with hooks, tools, and routes", settingsSchema: { apiKey: { type: "string", label: "API Key", required: true, }, }, }, state: "installed", tools: [ { name: "my_tool", description: "Does something useful", parameters: { type: "object", properties: { input: { type: "string" }, }, required: ["input"], }, execute: async (params, ctx) => { const result = process(params.input as string); return { content: [{ type: "text", text: result }] }; }, }, ], routes: [ { method: "GET", path: "/status", handler: async () => ({ status: "ok" }), }, ], hooks: { onLoad: (ctx) => ctx.logger.info("Loaded!"), onTaskCreated: (task, ctx) => { ctx.logger.info(`Task created: ${task.id}`); }, onUnload: () => { // Cleanup }, }, } satisfies FusionPlugin); ``` --- For more information, see the [Plugin SDK Reference](../packages/plugin-sdk/src/index.ts).