feat(FN-1469): create settings-loadable example plugin

- Add fusion-plugin-settings-demo package with settings schema (string, number, boolean, enum types)
- Add lifecycle hooks (onLoad, onTaskCreated, onTaskCompleted)
- Add tools (suggest_tags, status) with settings-driven behavior
- Add comprehensive tests with 24 test cases
- Add manifest.json for plugin metadata and README with Settings installation flow
- Update PLUGIN_AUTHORING.md with new example and install instructions
- Also includes: FN-1468 plugin wiring, FN-1456 abandon flow, FN-1429 always-green tests, FN-1440 Authentication in Settings, FN-1133 plugin hot-reload support
This commit is contained in:
gsxdsm
2026-04-09 22:00:49 -07:00
parent 7b8960f3e9
commit 2aa0c0d753
9 changed files with 1028 additions and 14 deletions

View File

@@ -0,0 +1,277 @@
/**
* Settings Demo Plugin
*
* Example Fusion plugin that demonstrates:
* - Settings schema with multiple field types (string, number, boolean, enum)
* - Hooks that read configuration at runtime
* - Tools that expose settings-driven functionality
*
* This plugin suggests tags for tasks based on configurable keywords
* and provides a status endpoint for checking configuration.
*/
import { definePlugin } from "@fusion/plugin-sdk";
import type {
FusionPlugin,
PluginContext,
PluginSettingSchema,
PluginToolDefinition,
PluginToolResult,
} from "@fusion/plugin-sdk";
// ── Settings Schema ─────────────────────────────────────────────────────────────
/**
* Settings schema for the plugin.
* Users can configure these values via the dashboard Settings → Plugins UI.
*/
const settingsSchema: Record<string, PluginSettingSchema> = {
greetingMessage: {
type: "string",
label: "Greeting Message",
description: "Custom greeting message shown when the plugin loads",
defaultValue: "Hello from Settings Demo!",
},
maxTags: {
type: "number",
label: "Max Tags",
description: "Maximum number of tags to suggest per task",
defaultValue: 3,
},
enableLogging: {
type: "boolean",
label: "Enable Logging",
description: "Log plugin activity to the console",
defaultValue: true,
},
logLevel: {
type: "enum",
label: "Log Level",
description: "Minimum log level to output",
enumValues: ["debug", "info", "warn", "error"],
defaultValue: "info",
},
};
// ── Tag Keywords Configuration ─────────────────────────────────────────────────
/**
* Keyword-to-tag mappings for automatic tag suggestion.
* In a real plugin, this would be user-configurable.
*/
const TAG_KEYWORDS: Record<string, string[]> = {
bug: ["fix", "bug", "error", "crash", "broken", "issue"],
feature: ["add", "implement", "create", "new", "feature"],
refactor: ["refactor", "cleanup", "improve", "optimize", "restructure"],
docs: ["docs", "documentation", "readme", "comment"],
test: ["test", "testing", "spec", "coverage"],
security: ["security", "vulnerability", "auth", "permission"],
performance: ["performance", "speed", "optimize", "fast"],
ui: ["ui", "interface", "design", "visual", "frontend"],
backend: ["api", "backend", "server", "database"],
};
// ── Helper Functions ───────────────────────────────────────────────────────────
/**
* Extract suggested tags from task description based on keywords.
*/
function suggestTags(
description: string,
maxTags: number,
): string[] {
if (!description) return [];
const lowerDesc = description.toLowerCase();
const suggestions: { tag: string; count: number }[] = [];
for (const [tag, keywords] of Object.entries(TAG_KEYWORDS)) {
const matchCount = keywords.filter((kw) => lowerDesc.includes(kw)).length;
if (matchCount > 0) {
suggestions.push({ tag, count: matchCount });
}
}
// Sort by match count (most matches first) and take up to maxTags
return suggestions
.sort((a, b) => b.count - a.count)
.slice(0, maxTags)
.map((s) => s.tag);
}
/**
* Check if a log level should be output based on configured minimum level.
*/
function shouldLog(
configuredLevel: string,
messageLevel: string,
): boolean {
const levels = ["debug", "info", "warn", "error"];
const configuredIdx = levels.indexOf(configuredLevel);
const messageIdx = levels.indexOf(messageLevel);
return messageIdx >= configuredIdx;
}
// ── Plugin Tool ───────────────────────────────────────────────────────────────
/**
* Tool for getting tag suggestions for a task.
* Demonstrates how tools can read and use plugin settings.
*/
const suggestTagsTool: PluginToolDefinition = {
name: "settings_demo_suggest_tags",
description: "Suggest tags for a task based on its description using keyword matching. Returns up to the configured max tags.",
parameters: {
type: "object",
properties: {
taskDescription: {
type: "string",
description: "The task description to analyze for tag suggestions",
},
},
required: ["taskDescription"],
},
execute: async (
params: Record<string, unknown>,
ctx: PluginContext,
): Promise<PluginToolResult> => {
const description = params.taskDescription as string;
const maxTagsSetting = ctx.settings.maxTags as number | undefined;
// Use 3 as default only if maxTags is not explicitly set (undefined or NaN)
// Allow maxTags=0 to return no tags
const maxTags = maxTagsSetting !== undefined && !isNaN(maxTagsSetting) ? maxTagsSetting : 3;
// Log tool usage if enabled
if (ctx.settings.enableLogging) {
ctx.logger.info(`Suggesting tags for description: ${description.slice(0, 50)}...`);
}
const tags = suggestTags(description, maxTags);
const result: PluginToolResult = {
content: [
{
type: "text",
text: tags.length === 0
? "No tags could be suggested based on the task description."
: `Suggested tags: ${tags.join(", ")}`,
},
],
details: {
tags,
count: tags.length,
},
};
return result;
},
};
/**
* Tool for getting plugin configuration status.
* Demonstrates how tools expose current settings.
*/
const statusTool: PluginToolDefinition = {
name: "settings_demo_status",
description: "Get the current configuration status of the Settings Demo plugin",
parameters: {
type: "object",
properties: {},
required: [],
},
execute: async (
_params: Record<string, unknown>,
ctx: PluginContext,
): Promise<PluginToolResult> => {
// Return raw settings values for status display
const greetingMessage = ctx.settings.greetingMessage as string | undefined;
const maxTags = ctx.settings.maxTags as number | undefined;
const enableLogging = ctx.settings.enableLogging as boolean | undefined;
const logLevel = ctx.settings.logLevel as string | undefined;
// Use resolved values for display text
const greeting = greetingMessage || "Not configured";
const displayMaxTags = maxTags ?? 3;
const displayEnableLogging = enableLogging ?? true;
const displayLogLevel = logLevel || "info";
return {
content: [
{
type: "text",
text: [
"Settings Demo Plugin Status:",
`- Greeting: ${greeting}`,
`- Max Tags: ${displayMaxTags}`,
`- Logging: ${displayEnableLogging ? "enabled" : "disabled"}`,
`- Log Level: ${displayLogLevel}`,
].join("\n"),
},
],
details: {
greetingMessage,
maxTags,
enableLogging,
logLevel,
},
};
},
};
// ── Plugin Definition ───────────────────────────────────────────────────────────
const plugin: FusionPlugin = definePlugin({
manifest: {
id: "fusion-plugin-settings-demo",
name: "Settings Demo Plugin",
version: "0.1.0",
description: "Example plugin demonstrating settings schema, hooks, and tools",
settingsSchema,
},
state: "installed",
tools: [suggestTagsTool, statusTool],
hooks: {
onLoad: (ctx: PluginContext) => {
const greeting =
(ctx.settings.greetingMessage as string) || "Hello from Settings Demo!";
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
const logLevel = (ctx.settings.logLevel as string) || "info";
if (enableLogging && shouldLog(logLevel, "info")) {
ctx.logger.info(greeting);
ctx.logger.info(`Plugin configured with maxTags: ${ctx.settings.maxTags || 3}`);
}
},
onTaskCreated: async (task: { id: string; title?: string; description?: string }, ctx: PluginContext) => {
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
const logLevel = (ctx.settings.logLevel as string) || "info";
if (!enableLogging || !shouldLog(logLevel, "debug")) {
return;
}
ctx.logger.debug(`Task created: ${task.id} - ${task.title || "untitled"}`);
// Auto-suggest tags for new tasks
if (task.description) {
const maxTags = (ctx.settings.maxTags as number) || 3;
const tags = suggestTags(task.description, maxTags);
if (tags.length > 0) {
ctx.logger.debug(`Suggested tags for ${task.id}: ${tags.join(", ")}`);
}
}
},
onTaskCompleted: async (task: { id: string; title?: string }, ctx: PluginContext) => {
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
const logLevel = (ctx.settings.logLevel as string) || "info";
if (enableLogging && shouldLog(logLevel, "info")) {
ctx.logger.info(`Task completed: ${task.id} - ${task.title || "untitled"}`);
}
},
},
});
export default plugin;