plugin development

plugin structure:

  minimal plugin:
    plugins/my_plugin/
      __init__.py               (plugin entry point)
      plugin.py                 (main plugin class)
      config.json               (plugin configuration schema)

  plugin.py structure:
    from kollabor.plugins.base import BasePlugin
    from kollabor.events.models import EventType, Hook, HookPriority

    class MyPlugin(BasePlugin):
        def __init__(self, event_bus, config, renderer, command_registry):
            super().__init__(
                name="my_plugin",
                version="1.0.0",
                event_bus=event_bus,
                config=config,
                renderer=renderer
            )
            self.command_registry = command_registry

        async def initialize(self):
            """called when plugin loaded"""
            self.register_hooks()
            self.register_commands()

        def register_hooks(self):
            """register event hooks"""
            self.event_bus.register(
                EventType.POST_API_RESPONSE,
                Hook(
                    func=self.on_response,
                    priority=HookPriority.NORMAL,
                    plugin_name=self.name
                )
            )

        async def on_response(self, context):
            """hook handler"""
            # modify response if needed
            return context

        def register_commands(self):
            """register slash commands"""
            from kollabor.events.models import CommandDefinition, CommandCategory

            cmd = CommandDefinition(
                name="mycommand",
                description="my custom command",
                category=CommandCategory.CUSTOM,
                handler=self.handle_mycommand,
                aliases=["mc", "mycmd"]
            )
            self.command_registry.register_command(cmd)

        async def handle_mycommand(self, args: str):
            """command handler"""
            return "command executed"

        def get_status_line(self, area: str) -> Optional[str]:
            """contribute to status line"""
            if area == "C":
                return "my-plugin: active"
            return None

        async def shutdown(self):
            """cleanup when plugin unloaded"""
            pass

plugin discovery and loading:

  discovery process:
    [1] plugin_discovery scans plugins/ directory
        → finds all python modules
        → loads plugin metadata
    [2] plugin_factory instantiates plugins
        → injects dependencies: event_bus, config, renderer, command_registry
    [3] plugin.initialize() called
        → plugin registers hooks, commands
        → plugin merges config
    [4] plugin ready to receive events

  config.json schema:
    {
      "name": "my_plugin",
      "version": "1.0.0",
      "description": "my custom plugin",
      "author": "your name",
      "config_schema": {
        "enabled": {
          "type": "boolean",
          "default": true,
          "description": "enable plugin"
        },
        "custom_setting": {
          "type": "string",
          "default": "default_value",
          "description": "custom setting"
        }
      }
    }

  config merging:
    - plugin config merged into global config
    - accessible via: config.get("my_plugin.custom_setting")
    - plugins can read their own config at initialization

hook registration patterns:

  priority selection:
    [900] CRITICAL              (security, validation)
        → use for: permission checks, input validation
        → example: permissions plugin checks tool safety

    [700] HIGH                   (core functionality)
        → use for: state changes, core features
        → example: llm service processes responses

    [500] NORMAL                 (standard plugins)
        → use for: most plugin behavior
        → example: formatting plugin transforms output

    [300] LOW                    (monitoring, logging)
        → use for: analytics, monitoring
        → example: logging plugin records events

  hook context pattern:
    async def my_hook(self, context):
        """modify and return context"""

        # read from context
        user_input = context.get("user_input")
        response = context.get("response")

        # modify context
        context["modified_field"] = "new_value"

        # return modified context
        return context

  event types for hooks:
    pre_user_input              (before input processed)
        → context: {"user_input": str}

    post_user_input             (after input processed)
        → context: {"user_input": str, "response": str}

    pre_api_request             (before llm call)
        → context: {"request_data": dict}

    post_api_response           (after llm response)
        → context: {"response": str, "metadata": dict}

    tool_execution              (tool execution events)
        → context: {"tool_name": str, "args": dict, "result": any}

    permission_request          (permission requests)
        → context: {"tool_name": str, "risk_level": str}

status line contributions:

  status areas:
    area A: left side of status line
    area B: middle section
    area C: right side

  get_status_line() pattern:
    def get_status_line(self, area: str) -> Optional[str]:
        """return string for status area or None"""
        if area == "A":
            return "left-status"
        elif area == "B":
            return "middle-status"
        elif area == "C":
            return "right-status"
        return None

  status line formatting:
    - keep status strings short (<30 chars recommended)
    - use plain text (no emojis unless explicitly requested)
    - update dynamically by returning different values
    - return None to not contribute to that area

slash command registration:

  command definition:
    from kollabor.events.models import CommandDefinition, CommandCategory, SubcommandInfo

    cmd = CommandDefinition(
        name="mycommand",                # command name
        description="does something",    # description
        category=CommandCategory.CUSTOM, # category
        aliases=["mc", "mycmd"],         # optional aliases
        handler=self.handle_command,     # handler function
        subcommands=[                    # optional subcommands
            SubcommandInfo("action1", "", "do action 1"),
            SubcommandInfo("action2", "<arg>", "do action 2 with arg")
        ]
    )

  command handler signature:
    async def handle_command(self, args: str) -> str:
        """
        args: everything after command name
        returns: result message to display
        """
        # parse args if needed
        # execute command logic
        # return result message
        return "command executed"

  command categories:
    CUSTOM                          (custom plugins)
    SYSTEM                          (system commands)
    CONFIG                          (configuration commands)
    DEBUG                           (debugging commands)
    DEVELOPMENT                     (development commands)

plugin configuration:

  reading plugin config:
    def initialize(self):
        # read plugin config
        enabled = self.config.get("my_plugin.enabled", true)
        custom_value = self.config.get("my_plugin.custom_setting", "default")

        # use config to control behavior
        if enabled:
            self.register_hooks()

  writing to config:
    # plugin can modify its config
    self.config.set("my_plugin.custom_setting", "new_value")

  default values:
    # provide defaults in config.json schema
    # or use default values in get() calls
    value = self.config.get("my_plugin.setting", default_value)

example plugin 1: simple logging plugin

  plugins/logger_plugin/plugin.py:
    from kollabor.plugins.base import BasePlugin
    from kollabor.events.models import EventType, Hook, HookPriority
    import logging

    logger = logging.getLogger(__name__)

    class LoggerPlugin(BasePlugin):
        def initialize(self):
            """register hooks"""
            self.event_bus.register(
                EventType.POST_USER_INPUT,
                Hook(
                    func=self.log_input,
                    priority=HookPriority.LOW,
                    plugin_name=self.name
                )
            )

        async def log_input(self, context):
            """log user input"""
            user_input = context.get("user_input", "")
            logger.info(f"User input: {user_input[:50]}...")
            return context

example plugin 2: response enhancer plugin

  plugins/enhancer_plugin/plugin.py:
    from kollabor.plugins.base import BasePlugin
    from kollabor.events.models import EventType, Hook, HookPriority

    class EnhancerPlugin(BasePlugin):
        def initialize(self):
            """register hooks"""
            self.event_bus.register(
                EventType.POST_API_RESPONSE,
                Hook(
                    func=self.enhance_response,
                    priority=HookPriority.NORMAL,
                    plugin_name=self.name
                )
            )

        async def enhance_response(self, context):
            """enhance llm response"""
            response = context.get("response", "")

            # add formatting
            if "<code>" in response:
                response = response.replace("<code>", "```")
                response = response.replace("</code>", "```")
                context["response"] = response

            return context

example plugin 3: status widget plugin

  plugins/status_widget_plugin/plugin.py:
    from kollabor.plugins.base import BasePlugin
    from kollabor.events.models import EventType, Hook, HookPriority
    from datetime import datetime
    import psutil

    class StatusWidgetPlugin(BasePlugin):
        def initialize(self):
            """register status line hook"""
            self.event_bus.register(
                EventType.STATUS_UPDATE,
                Hook(
                    func=self.update_status,
                    priority=HookPriority.NORMAL,
                    plugin_name=self.name
                )
            )

        async def update_status(self, context):
            """update status line"""
            # status will be queried via get_status_line()
            return context

        def get_status_line(self, area: str) -> str:
            """contribute to status line"""
            if area == "C":
                # show current time
                return datetime.now().strftime("%H:%M:%S")
            return None

plugin best practices:

  [1] keep plugins focused
      each plugin should do one thing well
      avoid monolithic plugins that do everything

  [2] use appropriate hook priorities
      don't use CRITICAL unless necessary
      most plugins should use NORMAL priority

  [3] handle errors gracefully
      wrap hook code in try-except
      log errors but don't crash the application

  [4] be efficient
      hooks are called frequently
      avoid blocking operations
      use async for i/o operations

  [5] respect user preferences
      read config to control behavior
      provide sensible defaults
      document configuration options

  [6] clean up on shutdown
      implement shutdown() method
      release resources, close connections

  [7] document your plugin
      add README.md with usage examples
      document config options
      explain hook behavior

