Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hooks & Events

Status: wired (#189 closed). api.registerHook, api.on, emitHook, emitEvent, invokeTool all dispatch to crates/perry-runtime/src/plugin.rs. Snippets below are compile-link verified against docs/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