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:
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier ([a-z][a-z0-9_]{1,63}) |
version | Yes | Semver string (e.g., 1.0.0) |
description | No | Human-readable description |
author | No | Plugin author |
has_backend | Yes* | Whether the plugin has Python backend code |
has_ui | Yes* | Whether the plugin has a UI component |
entry_module | If backend | Dotted module path inside backend/ |
entry_class | If backend | Python class name to instantiate |
priority | No | Plugin registration priority (default: 50) |
dependencies | No | Names of other plugins that must register first |
python_dependencies | No | List of pip requirements |
default_args | No | Default 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:
dependencies— names of plugins that must register before yours. Matched against each loaded plugin'smeta["name"]. Topological order always wins: a plugin in this list registers earlier than yours, no matter whatpriorityeither side declares.priority— integer used as a tie-breaker within a topological layer (lower number = registers earlier). Plugins that don't declaredependenciesbehave exactly as they did before this field existed.
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
- A
dependenciesentry naming a plugin that isn't loaded aborts startup withPlugin '<name>' declares dependency '<missing>', which is not among the loaded plugins.Either ship the missing plugin or drop the entry. - A cycle in the dependency graph aborts startup with
Plugin dependency cycle detected: A -> B -> A. Break the cycle by removing one of the edges (often one direction was unintended).
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:
--output path/to/output.tgz— custom output path
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:
- Scans
~/.codumentor/plugins/*/plugin.yaml - Adds each plugin's
backend/directory to the Python import path - Loads and registers the plugin alongside built-in plugins
No
plugins:config entry is required. Discovery runs regardless of
whethercodumentor.yamlhas aplugins:section — an empty or absent list
does not disable external plugins. (Before this was fixed, the loader
gated on a truthyplugins: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:
ui/manifest.json— declares custom elements and placementsui/index.js— pre-built ES module that registers web components
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
- Create a widget module in your plugin's
backend/directory:
backend/
my_plugin/
__init__.py
plugin.py
tui_widgets.py # TUI widget classes
- 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
- Declare the
tuisection in yourui_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
| Class | Import | Use Case |
|---|---|---|
PluginCard | codumentor.tui.widgets.plugin_cards | Collapsible transcript cards with header/details (recommended) |
BaseTUIPluginWidget | codumentor.plugins.tui_sdk | Fully 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?
- Check logs:
codumentor --log-level DEBUG - Verify plugin.yaml is valid:
codumentor plugin listshould show it - Check for name collisions with built-in plugins
Python import errors?
- Ensure the package name inside
backend/is unique - Install missing dependencies:
pip install <package>
UI not showing?
- Verify
ui/index.jsexists (must be pre-built) - Check the browser console for custom element registration errors
- Ensure
ui/manifest.jsondeclares the correct custom elements