Hooks & Events
Status: wired (#189 closed).
api.registerHook,api.on,emitHook,emitEvent,invokeToolall dispatch tocrates/perry-runtime/src/plugin.rs. Snippets below are compile-link verified againstdocs/examples/plugins/{plugin,host}_snippets.ts.
Perry plugins communicate through hooks, events, and tools.
Hook Modes
Hooks support three execution modes:
Filter Mode (default)
Each plugin receives data and returns (possibly modified) data. The output of one plugin becomes the input of the next:
function registerFilter(api: PluginApi) {
api.registerHook("transform", (data: any) => {
data.content = data.content.toUpperCase()
return data // Returned data goes to next plugin
})
}
Action Mode
Plugins receive data but return value is ignored. Used for side effects. Pass
mode = 1 to registerHookEx:
function registerAction(api: PluginApi) {
api.registerHook("onSave", (data: any) => {
console.log(`Saved: ${data.path}`)
return data
})
}
Waterfall Mode
Like filter mode, but specifically for accumulating/building up a result
through the chain. Pass mode = 2 to registerHookEx:
function registerWaterfall(api: PluginApi) {
api.registerHook("buildMenu", (items: any) => {
items.push({ label: "My Plugin Action", action: () => {} })
return items
})
}
Hook Priority
Lower priority numbers run first. Use registerHookEx for explicit priority
and mode:
function registerPriorities(api: PluginApi, validate: (d: any) => any, transform: (d: any) => any, log: (d: any) => any) {
// Lower priority numbers run first; default 10. Mode 0=filter / 1=action / 2=waterfall.
api.registerHookEx("beforeSave", validate, 10, 0) // Runs first
api.registerHookEx("beforeSave", transform, 20, 0) // Runs second
api.registerHookEx("beforeSave", log, 100, 1) // Runs last (action mode)
}
Default priority is 10 (the value registerHook passes implicitly).
Event Bus
Plugins can communicate with each other through events:
Emitting Events
function emitFromPlugin(api: PluginApi) {
api.emit("dataUpdated", { source: "my-plugin", records: 42 })
}
emitEvent("dataUpdated", { source: "host", records: 100 })
Listening for Events
function listenForEvent(api: PluginApi) {
api.on("dataUpdated", (data: any) => {
console.log(`${data.source} updated ${data.records} records`)
})
}
Tools
Plugins register callable tools (note the 3-arg shape: name, description,
handler):
function registerFormatter(api: PluginApi) {
api.registerTool("formatCode", "format source code", (args: any) => {
return `// formatted: ${args.code}`
})
}
const greeting = invokeTool("greet", { name: "Perry" })
const formatted = invokeTool("formatCode", {
code: "const x=1",
language: "typescript",
})
Configuration
Hosts can pass configuration to plugins via setPluginConfig:
initPlugins()
setPluginConfig("api_key", "test-key")
setPluginConfig("max_retries", "3")
function readConfig(api: PluginApi) {
const theme = api.getConfig("theme") // "dark"
const retries = api.getConfig("maxRetries") // "3"
return { theme, retries }
}
Introspection
Query loaded plugins and their registrations:
const plugins = listPlugins()
const hooks = listHooks()
const tools = listTools()
console.log(`loaded: ${pluginCount()} plugin(s), ${hooks.length} hook(s), ${tools.length} tool(s)`)
Next Steps
- Creating Plugins — Build a plugin
- Overview — Plugin system overview