Files
deepagents/libs/code/deepagents_code/plugins/adapters/skills.py
T
Johannes du Plessis 0187d14b83 feat(code): add plugin marketplace support (#4554)
`deepagents-code` now supports experimental plugin marketplaces with
namespaced skills and MCP servers. Enable with
`DEEPAGENTS_CODE_EXPERIMENTAL=1`.

Docs: https://github.com/langchain-ai/docs/pull/4905

---

`deepagents-code` can now manage experimental plugin marketplaces and
load installed plugins that contribute namespaced skills and MCP
servers. The new plugin manager establishes the `tui/modals`,
`tui/screens`, and `tui/widgets` UI convention, with colocated styling
and focused state/content modules.

- Supports local, GitHub, Git, and marketplace JSON sources.
- Adds install, enable, disable, uninstall, and safe marketplace removal
through `/plugins` and `dcode plugin`.
- Reloads plugin skills and plugin MCP configuration with `/reload` when
experimental mode is enabled.
- Namespaces plugin skills as `{plugin_id}:{skill_name}` through a
code-local `PluginSkillsMiddleware` adapter, so no SDK changes are
required.

Made by [Open
SWE](https://openswe.vercel.app/agents/019f5dea-0f1f-73fe-9803-32d1b13fbd4c)

---------

Signed-off-by: Johannes du Plessis <johannes@langchain.dev>
Co-authored-by: Alexander Olsen <aolsenjazz@gmail.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-07-15 09:11:28 -04:00

122 lines
3.7 KiB
Python

"""Adapter from discovered plugins to `SkillsMiddleware` sources."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import TYPE_CHECKING, TypeAlias
from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy
if TYPE_CHECKING:
from deepagents_code.plugins.models import PluginInstance
logger = logging.getLogger(__name__)
SkillPath: TypeAlias = str
SkillLabel: TypeAlias = str
SkillNamespace: TypeAlias = str
DirectorySkillSource: TypeAlias = tuple[SkillPath, SkillLabel]
PluginSkillSource: TypeAlias = tuple[SkillPath, SkillLabel, SkillNamespace]
CodeSkillSource: TypeAlias = DirectorySkillSource | PluginSkillSource
def namespaced_skill_name(
namespace: SkillNamespace,
name: str,
subfolders: tuple[str, ...] = (),
) -> str:
"""Qualify a skill name under its plugin namespace.
Nested skill directories contribute intermediate `:`-joined segments
between the plugin namespace and the skill name, matching the plugin skill
naming convention (e.g. `plugin:sub:review`).
Args:
namespace: Plugin namespace (its `plugin_id`).
name: Skill name from the skill's frontmatter.
subfolders: Directory names between the plugin skills root and the
skill directory, in path order.
Returns:
The qualified skill name.
"""
return ":".join((namespace, *subfolders, name)).lower()
def plugin_skill_sources(
plugins: tuple[PluginInstance, ...],
) -> list[PluginSkillSource]:
"""Return skill source tuples for plugin skills.
Args:
plugins: Plugin instances.
Returns:
Source tuples containing path, label, and plugin namespace.
"""
sources: list[PluginSkillSource] = []
for plugin in plugins:
for path in plugin.inventory.skills:
source_path = path.parent if path.name == "SKILL.md" else path
try:
if not source_path.exists():
continue
except OSError:
logger.warning("Could not inspect plugin skill path %s", source_path)
continue
sources.append(
(
str(source_path),
f"Plugin: {plugin.plugin_id}",
plugin.plugin_id,
)
)
return sources
def plugin_skill_roots(plugins: tuple[PluginInstance, ...]) -> list[Path]:
"""Return plugin skill roots for skill-content containment checks.
Args:
plugins: Discovered plugin instances.
Returns:
Skill root directories.
"""
roots: list[Path] = []
for plugin in plugins:
roots.extend(
path.parent if path.name == "SKILL.md" else path
for path in plugin.inventory.skills
)
return roots
def discover_plugin_skill_sources_and_roots() -> tuple[
tuple[tuple[Path, str], ...], tuple[Path, ...]
]:
"""Discover plugin skill sources and containment roots.
Returns:
Plugin skill sources and roots, or empty tuples when plugins are disabled
or discovery fails.
"""
plugin_sources: tuple[tuple[Path, str], ...] = ()
plugin_roots: tuple[Path, ...] = ()
try:
if is_env_truthy(EXPERIMENTAL):
from deepagents_code.plugins import discover_plugins
plugins = discover_plugins().plugins
plugin_sources = tuple(
(Path(path), namespace)
for path, _label, namespace in plugin_skill_sources(plugins)
)
plugin_roots = tuple(plugin_skill_roots(plugins))
except (OSError, RuntimeError):
logger.warning("Could not discover plugin skills", exc_info=True)
return (), ()
return plugin_sources, plugin_roots