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:
277
plugins/examples/fusion-plugin-settings-demo/src/index.ts
Normal file
277
plugins/examples/fusion-plugin-settings-demo/src/index.ts
Normal 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;
|
||||
Reference in New Issue
Block a user