A minimal reference plugin for roam-code showing the substrate
for roam-plugin-* packages (framework analyzers — nextjs,
laravel, prisma, django, …).
A roam plugin is any Python package that exposes a
register(ctx: RoamPluginContext) -> None callable via the
roam.plugins Python entry-point group. At startup, roam:
- Walks installed packages' metadata for the
roam.pluginsgroup. - Imports each entry-point target.
- Calls
register(ctx)to wire the plugin's hooks into roam.
Discovery is wrapped in try/except end-to-end — a broken plugin is
recorded as a discovery error (visible via roam plugins doctor)
but never crashes roam itself.
roam-plugin-example/
├── pyproject.toml # entry-point declaration
├── README.md
└── roam_plugin_example/
└── __init__.py # exposes register(ctx)
The critical line in pyproject.toml:
[project.entry-points."roam.plugins"]
example = "roam_plugin_example:register"The critical line in roam_plugin_example/__init__.py:
from roam.plugins import FrameworkProfile
def register(ctx):
ctx.declare(name="example", version="0.1.0", description="…")
ctx.register_framework_profile(FrameworkProfile(
name="example",
detect_fn=my_detector, # Callable[[pathlib.Path], Optional[str]] (W56)
file_patterns=("example.config.*",),
recommended_commands=("describe", "health"),
conventions={"controller": "examples/controllers/*"},
))
ctx.register_detector("my-task", "my-way", my_detector_fn)
ctx.register_language_extractor("qml", MyExtractor, extensions=[".qml"])
ctx.register_bridge(MyBridge())register_framework_profile (W123) is the preferred surface for new
plugins; it wires the detector AND the profile in one call. The
legacy register_framework_detector(detect_fn) still works for
detector-only plugins (see roam_plugin_rails/).
Commands a plugin registers via ctx.register_command() do not
roll into roam's core-command headline count. The auto-count
scripts (dev/build_readme_counts.py, roam.surface_counts) are
AST-only and scope to commands shipped in the roam-code wheel.
Surface plugin-registered commands at runtime via:
roam plugins list # which plugins loaded
roam plugins info <name> # what each plugin contributes
roam --help-all # full command roster including plugin commands| Method | Purpose |
|---|---|
declare(name, version, ...) |
Plugin metadata (optional but recommended). |
register_command(...) |
Add a roam <name> CLI subcommand. |
register_detector(...) |
Add an algorithm-catalog detector. |
register_language_extractor |
Add a symbol/reference extractor for a language. |
register_framework_detector |
Detect which framework a project uses. detect_fn signature: Callable[[pathlib.Path], Optional[str]] (W56 — type-hint Path, not str). |
register_framework_profile |
Bundle a detector with file_patterns + recommended_commands + conventions in one call (W123). Prefer this over the bare detector API for new plugins. |
register_bridge(...) |
Add a cross-language reference bridge. |
See src/roam/plugins/registry.py for the typed signatures and
CLAUDE.md "Writing a roam plugin" for the canonical contract +
discovery semantics.
This package isn't published. To dogfood it against the host repo:
# Install in editable mode against the host roam-code venv:
pip install -e dev/example-plugin/
# Then ask roam what it sees:
roam plugins list # should list "example"
roam plugins info example
roam plugins doctor # no errorsFor one-shot development without installing the package, use the
ROAM_PLUGIN_MODULES channel — this matches the canonical form in
CLAUDE.md ("Writing a roam plugin" section):
PYTHONPATH=dev/example-plugin \
ROAM_PLUGIN_MODULES=roam_plugin_example \
roam plugins listdev/example-plugin/ ships three sibling packages so plugin authors
have a copy-fork template for every hook on RoamPluginContext:
| Package | Demonstrates | How to load |
|---|---|---|
roam_plugin_example/ |
declare, register_framework_detector, register_framework_profile, register_detector |
entry point (example) |
roam_plugin_example_extras/ |
register_command, register_bridge, register_language_extractor |
env channel (W1292) |
roam_plugin_rails/ |
Dogfood validation of register_framework_detector against the real Rails detection rule (W28.2) |
env channel |
Together the three packages cover all 7 hooks the typed
RoamPluginContext exposes. A real roam-plugin-nextjs /
roam-plugin-laravel package will pick the subset it needs.
roam_plugin_example/__init__.py registers:
- A no-op framework detector that returns
None(always defers). - A tiny detector that returns one synthetic finding when invoked.
- A
FrameworkProfilebundling the detector withfile_patterns,recommended_commands, andconventions. - A
declare()call so the plugin appears with name + version inroam plugins list.
roam_plugin_example_extras/ registers:
- A Click subcommand
roam example-greet --name <name>viaregister_command. Lives incli.pyso roam's lazy-import path (module_path+attr_name) works exactly as it does for core'sLazyGroupcommands. - A synthetic
ExampleBridge(LanguageBridge)viaregister_bridgethat maps.example->.example_targetfiles. Shows thename/source_extensions/target_extensions/detect/resolvesurface a production bridge fills in. - An
ExampleExtractor(LanguageExtractor)viaregister_language_extractorkeyed on the.exampleextension. Demonstrates thelanguage_name/file_extensions/extract_symbols/extract_referenceslifecycle plus the_make_symbol(...)helper on the base class.
Load it via the env channel — it's not declared as an entry point so consumers (and tests) opt in explicitly:
PYTHONPATH=dev/example-plugin \
ROAM_PLUGIN_MODULES=roam_plugin_example_extras \
roam plugins list
# Then exercise the new command:
PYTHONPATH=dev/example-plugin \
ROAM_PLUGIN_MODULES=roam_plugin_example_extras \
roam example-greet --name agent
# -> VERDICT: greeted agent