Codumentor logo Codumentor

Plugin Distribution

Distribute Codumentor plugins as .tgz archives that users install with a single CLI command.


Quick Start

Installing a Plugin

codumentor plugin install my_plugin-1.0.0.tgz

The plugin auto-loads on the next Codumentor startup. No YAML editing required.

Listing Installed Plugins

codumentor plugin list

Removing a Plugin

codumentor plugin remove my_plugin

Package Format

A plugin .tgz archive contains:

plugin.yaml              # Required: package manifest
backend/                 # Optional: Python source code
  <package_name>/        # Uniquely-named Python package
    __init__.py
    plugin.py
    tui_widgets.py       # Optional: TUI widget classes
    ...
ui/                      # Optional: pre-built UI bundle
  manifest.json          # UI extension manifest
  index.js               # Pre-built ES module

plugin.yaml

The manifest declares the plugin's identity, entry points, and dependencies:

name: my_plugin
version: 1.0.0
description: A brief description of what the plugin does
author: Your Name

has_backend: true
has_ui: true

# Backend entry point (dotted Python module path inside backend/)
entry_module: my_plugin.plugin
entry_class: MyPlugin

# Plugin registration priority (lower = registers earlier, default 50)
priority: 50

# Optional: declare load-order dependencies on other plugins by name
# (matched against each plugin's meta["name"]). The loader registers
# every plugin in this list before yours. See "Plugin Load Order" below.
dependencies:
  - workspace_isolation

# Python packages to pip-install during installation
python_dependencies:
  - requests>=2.28

# Default constructor arguments (users can override in codumentor.yaml)
default_args:
  some_option: true
  threshold: 100

Field reference:

FieldRequiredDescription
nameYesUnique identifier ([a-z][a-z0-9_]{1,63})
versionYesSemver string (e.g., 1.0.0)
descriptionNoHuman-readable description
authorNoPlugin author
has_backendYes*Whether the plugin has Python backend code
has_uiYes*Whether the plugin has a UI component
entry_moduleIf backendDotted module path inside backend/
entry_classIf backendPython class name to instantiate
priorityNoPlugin registration priority (default: 50)
dependenciesNoNames of other plugins that must register first
python_dependenciesNoList of pip requirements
default_argsNoDefault constructor keyword arguments

*At least one of has_backend or has_ui must be true.


Plugin Load Order

Built-in and external plugins are registered in a single ordered pass at
startup. Two manifest fields control where your plugin lands:

Use dependencies when your plugin reads state another plugin publishes
(e.g. branch_switching reads the execution strategy that
workspace_isolation injects, so branch_switching declares
dependencies: ["workspace_isolation"]). Use priority when you only
need rough ordering relative to plugins you don't know about by name.

Failure modes

Overriding from codumentor.yaml

Both fields can be overridden per-deployment by adding a matching entry
to the user's plugins: list. This is useful when an operator needs to
patch ordering without editing plugin code:

plugins:
  - module: my_plugin.plugin
    class: MyPlugin
    dependencies: ["workspace_isolation", "branch_switching"]

The config-level value fully replaces the manifest's dependencies
list, mirroring how priority overrides work today.


Creating a Plugin Package

1. Set Up the Directory

my_plugin/
  plugin.yaml
  backend/
    my_plugin/
      __init__.py
      plugin.py
      tui_widgets.py     # Optional: TUI widget classes
  ui/
    manifest.json
    src/
      index.ts
    package.json

Important: The Python package inside backend/ must have a unique name (conventionally the same as the plugin name). This prevents import collisions with other plugins.

2. Build the UI (if applicable)

cd my_plugin/ui
npm install && npm run build

The build must produce index.js in the ui/ directory. The .tgz ships the pre-built bundle — users do not need Node.js to install plugins.

3. Pack

codumentor plugin pack my_plugin/

This creates my_plugin-1.0.0.tgz in the current directory.

Options:

4. Distribute

Share the .tgz file. Users install with:

codumentor plugin install my_plugin-1.0.0.tgz

Installation Directory

Installed plugins live in ~/.codumentor/plugins/:

~/.codumentor/plugins/
  my_plugin/
    plugin.yaml
    backend/
      my_plugin/
        __init__.py
        plugin.py
    ui/
      manifest.json
      index.js

Auto-Discovery and Activation

External plugins are automatically loaded on Codumentor startup. The loader:

  1. Scans ~/.codumentor/plugins/*/plugin.yaml
  2. Adds each plugin's backend/ directory to the Python import path
  3. Loads and registers the plugin alongside built-in plugins

No plugins: config entry is required. Discovery runs regardless of
whether codumentor.yaml has a plugins: section — an empty or absent list
does not disable external plugins. (Before this was fixed, the loader
gated on a truthy plugins: list, so installing a plugin with no built-in
entries configured silently loaded nothing.) Add a config entry only to
override settings (below) or disable a plugin.

Disabling a Plugin

Add the plugin name to disabled_plugins in codumentor.yaml:

disabled_plugins:
  - my_plugin

Overriding Plugin Settings

To customize an external plugin's args or priority, add a matching entry to your plugins: list in codumentor.yaml. The config entry takes precedence over the plugin's default_args:

plugins:
  # ... built-in plugins ...
  - module: my_plugin.plugin
    class: MyPlugin
    priority: 10
    args:
      some_option: false
      threshold: 200

Backend Plugin Requirements

External backend plugins follow the same interface as built-in plugins:

class MyPlugin:
    def __init__(self, config=None, **kwargs):
        self.meta = {
            "name": "my_plugin",
            "version": "1.0.0",
            "description": "My plugin description",
            "priority": 50,
        }

    async def register(self, bus):
        bus.on("onInputReceived", self.on_input, priority=10)

    async def on_input(self, agent, ctx, payload):
        return None  # or {"type": "continue", "patch": {...}}

    def capabilities(self):
        return {}

    def ui_manifest(self):
        return None  # or dict with UI extension config

    def api_router(self):
        return None  # or FastAPI APIRouter

See Backend Hooks for the full hook reference.


UI Plugin Requirements

External UI plugins follow the same conventions as built-in ones:

The UI bundle is served at /ui/extensions/plugins/<plugin_id>/index.js and loaded by the frontend automatically.

See UI Components for the SDK and component reference.


TUI Widget Support

External plugins can provide custom terminal UI widgets that render in the TUI (Textual-based interface). This is optional — without it, plugin events render using a generic fallback card.

Adding TUI Widgets

  1. Create a widget module in your plugin's backend/ directory:
backend/
  my_plugin/
    __init__.py
    plugin.py
    tui_widgets.py       # TUI widget classes
  1. Implement a widget class that subclasses PluginCard:
# my_plugin/tui_widgets.py
from codumentor.plugins.tui_sdk import PluginCard
from rich.text import Text


class MyPluginCard(PluginCard):
    """Custom TUI card for my plugin events."""

    def _build_header_text(self) -> Text:
        text = Text()
        text.append(" \u25b8 ", style="dim")
        text.append("My Plugin", style="bold cyan")
        summary = self.plugin_data.get("summary", "")
        if summary:
            text.append(f"  {summary}", style="dim")
        return text

    def _build_details_text(self) -> Text:
        text = Text()
        for key, val in self.plugin_data.items():
            if key.startswith("_"):
                continue
            text.append(f"  {key}: ", style="bold")
            text.append(f"{val}\n")
        return text
  1. Declare the tui section in your ui_manifest():
def ui_manifest(self):
    return {
        "plugin_id": "my_plugin",
        # ... other fields ...
        "tui": {
            "sdk_version": "1.0",
            "widget_module": "my_plugin.tui_widgets",
            "widget_classes": ["MyPluginCard"],
            "placements": ["transcript-card"],
        },
    }

The widget_module uses standard Python dotted import paths. Since backend/ is automatically added to sys.path during plugin loading, modules are importable directly by package name.

Base Classes

ClassImportUse Case
PluginCardcodumentor.tui.widgets.plugin_cardsCollapsible transcript cards with header/details (recommended)
BaseTUIPluginWidgetcodumentor.plugins.tui_sdkFully custom Textual widgets with reactive event data

How It Works

On startup, the TUIWidgetRegistry reads each plugin's ui_manifest(), imports the widget_module, and maps event types to widget classes. When a plugin event arrives, the TUI looks up the registered widget class instead of using the generic fallback.

See Backend Hooks — TUI Widget Section for the full manifest field reference.


Troubleshooting

Plugin not loading?

Python import errors?

UI not showing?