c2roo 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
c2roo/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
c2roo/cli.py ADDED
@@ -0,0 +1,277 @@
1
+ import shutil
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from c2roo.converter.agent_converter import convert_agent
8
+ from c2roo.converter.command_converter import convert_command
9
+ from c2roo.converter.hook_converter import convert_hooks
10
+ from c2roo.converter.mcp_converter import convert_mcp
11
+ from c2roo.converter.skill_converter import convert_skill
12
+ from c2roo.parser.plugin_parser import parse_plugin
13
+ from c2roo.report import ConversionReport
14
+ from c2roo.sources.git_source import check_git_available, clone_repo, is_git_url
15
+ from c2roo.sources.local_source import resolve_local
16
+ from c2roo.sources.marketplace import MarketplaceRegistry
17
+ from c2roo.writer.roo_writer import RooWriter
18
+
19
+
20
+ def _resolve_output_root(target_global: bool) -> Path:
21
+ if target_global:
22
+ return Path.home() / ".roo"
23
+ return Path.cwd() / ".roo"
24
+
25
+
26
+ def _run_conversion(plugin_path: Path, output_root: Path, force: bool, dry_run: bool) -> None:
27
+ """Shared parse -> convert -> write pipeline."""
28
+ plugin = parse_plugin(plugin_path)
29
+ writer = RooWriter(output_root=output_root, force=force, dry_run=dry_run)
30
+ report = ConversionReport(plugin_name=plugin.metadata.name)
31
+
32
+ for skill_ir in plugin.skills:
33
+ converted_skill = convert_skill(skill_ir)
34
+ source_dir = plugin_path / "skills" / skill_ir.name
35
+ writer.write_skill(converted_skill, source_dir)
36
+ report.add_skill(converted_skill.name, dropped=converted_skill.dropped_fields)
37
+
38
+ for cmd_ir in plugin.commands:
39
+ converted_cmd = convert_command(cmd_ir)
40
+ writer.write_command(converted_cmd)
41
+ report.add_command(converted_cmd.name, dropped=converted_cmd.dropped_fields)
42
+
43
+ for agent_ir in plugin.agents:
44
+ converted_agent = convert_agent(agent_ir)
45
+ writer.write_agent(converted_agent)
46
+ report.add_agent(agent_ir.name)
47
+
48
+ if plugin.hooks:
49
+ hooks_content = convert_hooks(plugin.hooks)
50
+ writer.write_hooks(hooks_content)
51
+ report.add_hooks(count=len(plugin.hooks))
52
+
53
+ if plugin.mcp_servers:
54
+ mcp_data = convert_mcp(plugin.mcp_servers, output_root)
55
+ writer.write_mcp(mcp_data)
56
+ report.add_mcp(count=len(plugin.mcp_servers))
57
+
58
+ report.print(str(output_root))
59
+
60
+
61
+ @click.group()
62
+ @click.version_option()
63
+ def main() -> None:
64
+ """Convert Claude Code plugins to Roo Code format."""
65
+
66
+
67
+ # --- marketplace subcommands ---
68
+
69
+
70
+ @main.group()
71
+ def marketplace() -> None:
72
+ """Browse and manage plugin marketplace sources."""
73
+
74
+
75
+ @marketplace.command("browse")
76
+ @click.option("--source", default=None, help="Filter to a specific marketplace source.")
77
+ def marketplace_browse(source: str | None) -> None:
78
+ """List plugins from registered marketplaces."""
79
+ from rich.console import Console
80
+ from rich.table import Table
81
+
82
+ registry = MarketplaceRegistry()
83
+ sources = registry.list_sources()
84
+ if source:
85
+ sources = [s for s in sources if s["name"] == source]
86
+
87
+ console = Console()
88
+ table = Table(title="Available Plugins")
89
+ table.add_column("Name", style="cyan")
90
+ table.add_column("Description")
91
+ table.add_column("Source", style="green")
92
+
93
+ for src in sources:
94
+ try:
95
+ plugins = registry.fetch_marketplace_json(src)
96
+ for plugin in plugins:
97
+ table.add_row(
98
+ str(plugin.get("name", "?")),
99
+ str(plugin.get("description", "")),
100
+ src["name"],
101
+ )
102
+ except Exception as e:
103
+ console.print(f"[yellow]Warning: could not fetch {src['name']}: {e}[/yellow]")
104
+
105
+ console.print(table)
106
+
107
+
108
+ @marketplace.command("add")
109
+ @click.argument("repo")
110
+ @click.option("--name", required=True, help="Short name for this marketplace.")
111
+ @click.option("--description", default="", help="Description of this marketplace.")
112
+ def marketplace_add(repo: str, name: str, description: str) -> None:
113
+ """Register a new marketplace source (owner/repo format)."""
114
+ registry = MarketplaceRegistry()
115
+ registry.add_source(name, repo, description)
116
+ click.echo(f"Added marketplace '{name}' ({repo})")
117
+
118
+
119
+ @marketplace.command("list")
120
+ def marketplace_list() -> None:
121
+ """Show registered marketplace sources."""
122
+ from rich.console import Console
123
+ from rich.table import Table
124
+
125
+ registry = MarketplaceRegistry()
126
+ sources = registry.list_sources()
127
+
128
+ console = Console()
129
+ table = Table(title="Registered Marketplaces")
130
+ table.add_column("Name", style="cyan")
131
+ table.add_column("Repo", style="green")
132
+ table.add_column("Description")
133
+
134
+ for src in sources:
135
+ table.add_row(src["name"], src.get("repo", ""), src.get("description", ""))
136
+
137
+ console.print(table)
138
+
139
+
140
+ @marketplace.command("remove")
141
+ @click.argument("name")
142
+ def marketplace_remove(name: str) -> None:
143
+ """Remove a marketplace source."""
144
+ registry = MarketplaceRegistry()
145
+ registry.remove_source(name)
146
+ click.echo(f"Removed marketplace '{name}'")
147
+
148
+
149
+ # --- convert command ---
150
+
151
+
152
+ @main.command()
153
+ @click.argument("source")
154
+ @click.option(
155
+ "--global", "target_global", is_flag=True, default=False, help="Install to ~/.roo/ (global)."
156
+ )
157
+ @click.option(
158
+ "--project", "target_project", is_flag=True, default=False, help="Install to .roo/ (project)."
159
+ )
160
+ @click.option("--dry-run", is_flag=True, default=False, help="Show what would be converted.")
161
+ @click.option("--force", is_flag=True, default=False, help="Overwrite existing files.")
162
+ def convert(
163
+ source: str,
164
+ target_global: bool,
165
+ target_project: bool,
166
+ dry_run: bool,
167
+ force: bool,
168
+ ) -> None:
169
+ """Convert a Claude Code plugin from a local path or git URL."""
170
+ if not target_global and not target_project:
171
+ raise click.UsageError("Must specify --global or --project.")
172
+ if target_global and target_project:
173
+ raise click.UsageError("Cannot specify both --global and --project.")
174
+
175
+ temp_dir = None
176
+ if is_git_url(source):
177
+ check_git_available()
178
+ temp_dir = tempfile.mkdtemp(prefix="c2roo-")
179
+ plugin_path = clone_repo(source, dest=Path(temp_dir) / "plugin")
180
+ else:
181
+ plugin_path = resolve_local(source)
182
+
183
+ output_root = _resolve_output_root(target_global)
184
+
185
+ if dry_run:
186
+ click.echo("[DRY RUN] No files will be written.\n")
187
+
188
+ try:
189
+ _run_conversion(plugin_path, output_root, force, dry_run)
190
+ finally:
191
+ if temp_dir:
192
+ shutil.rmtree(temp_dir, ignore_errors=True)
193
+
194
+
195
+ # --- install command ---
196
+
197
+
198
+ @main.command()
199
+ @click.argument("plugin_name")
200
+ @click.option(
201
+ "--global", "target_global", is_flag=True, default=False, help="Install to ~/.roo/ (global)."
202
+ )
203
+ @click.option(
204
+ "--project", "target_project", is_flag=True, default=False, help="Install to .roo/ (project)."
205
+ )
206
+ @click.option("--source", default=None, help="Which marketplace to search.")
207
+ @click.option("--dry-run", is_flag=True, default=False, help="Show what would be converted.")
208
+ @click.option("--force", is_flag=True, default=False, help="Overwrite existing files.")
209
+ def install(
210
+ plugin_name: str,
211
+ target_global: bool,
212
+ target_project: bool,
213
+ source: str | None,
214
+ dry_run: bool,
215
+ force: bool,
216
+ ) -> None:
217
+ """Install a plugin from a marketplace, converting to Roo format."""
218
+ if not target_global and not target_project:
219
+ raise click.UsageError("Must specify --global or --project.")
220
+ if target_global and target_project:
221
+ raise click.UsageError("Cannot specify both --global and --project.")
222
+
223
+ check_git_available()
224
+
225
+ registry = MarketplaceRegistry()
226
+ result = registry.search_plugin(plugin_name, source_filter=source)
227
+
228
+ if result is None:
229
+ raise click.ClickException(
230
+ f"Plugin '{plugin_name}' not found in any registered marketplace."
231
+ )
232
+
233
+ plugin_entry, mkt_source = result
234
+ click.echo(f"Found '{plugin_name}' in marketplace '{mkt_source['name']}'")
235
+
236
+ output_root = _resolve_output_root(target_global)
237
+
238
+ if dry_run:
239
+ click.echo("[DRY RUN] No files will be written.\n")
240
+
241
+ temp_dir = tempfile.mkdtemp(prefix="c2roo-")
242
+ try:
243
+ plugin_source = plugin_entry.get("source")
244
+
245
+ if isinstance(plugin_source, str):
246
+ # Relative path — clone the marketplace repo first
247
+ mkt_repo_url = f"https://github.com/{mkt_source['repo']}.git"
248
+ mkt_dir = Path(temp_dir) / "marketplace"
249
+ clone_repo(mkt_repo_url, dest=mkt_dir)
250
+ plugin_path = mkt_dir / plugin_source
251
+ elif isinstance(plugin_source, dict):
252
+ source_type = plugin_source.get("source")
253
+ if source_type == "url":
254
+ url = plugin_source["url"]
255
+ sha = plugin_source.get("sha")
256
+ plugin_path = clone_repo(url, dest=Path(temp_dir) / "plugin", sha=sha)
257
+ elif source_type == "git-subdir":
258
+ repo_url = plugin_source["url"]
259
+ if not repo_url.startswith("http"):
260
+ repo_url = f"https://github.com/{repo_url}.git"
261
+ sha = plugin_source.get("sha")
262
+ subdir = plugin_source.get("path")
263
+ plugin_path = clone_repo(
264
+ repo_url, dest=Path(temp_dir) / "plugin", sha=sha, subdir=subdir
265
+ )
266
+ else:
267
+ raise click.ClickException(f"Unknown source type: {source_type}")
268
+ else:
269
+ raise click.ClickException(f"Invalid source format for plugin '{plugin_name}'")
270
+
271
+ if not (plugin_path / ".claude-plugin" / "plugin.json").exists():
272
+ raise click.ClickException("Downloaded plugin has no .claude-plugin/plugin.json")
273
+
274
+ _run_conversion(plugin_path, output_root, force, dry_run)
275
+
276
+ finally:
277
+ shutil.rmtree(temp_dir, ignore_errors=True)
File without changes
@@ -0,0 +1,94 @@
1
+ from dataclasses import dataclass
2
+
3
+ from c2roo.models.agent import Agent
4
+
5
+ # Mapping from Claude Code tool names to Roo tool groups
6
+ TOOL_TO_GROUP = {
7
+ "Read": "read",
8
+ "Grep": "read",
9
+ "Glob": "read",
10
+ "LS": "read",
11
+ "NotebookRead": "read",
12
+ "Write": "edit",
13
+ "Edit": "edit",
14
+ "MultiEdit": "edit",
15
+ "NotebookEdit": "edit",
16
+ "Bash": "command",
17
+ "BashOutput": "command",
18
+ "KillShell": "command",
19
+ }
20
+
21
+ MCP_TOOLS = {"WebFetch", "WebSearch", "TodoWrite"}
22
+
23
+
24
+ def _humanize_name(slug: str) -> str:
25
+ """Convert kebab-case slug to Title Case name."""
26
+ return " ".join(word.capitalize() for word in slug.split("-"))
27
+
28
+
29
+ def _extract_role_definition(body: str, max_length: int = 500) -> str:
30
+ """Extract the first paragraph of the body as a role definition."""
31
+ paragraphs = body.split("\n\n")
32
+ first = paragraphs[0].strip() if paragraphs else body.strip()
33
+ if len(first) > max_length:
34
+ first = first[:max_length].rsplit(" ", 1)[0] + "..."
35
+ return first
36
+
37
+
38
+ def _map_tools_to_groups(tools: list[str]) -> list[str]:
39
+ """Map Claude Code tool names to Roo tool groups."""
40
+ groups = set()
41
+ for tool in tools:
42
+ if tool in TOOL_TO_GROUP:
43
+ groups.add(TOOL_TO_GROUP[tool])
44
+ elif tool in MCP_TOOLS:
45
+ groups.add("mcp")
46
+ return sorted(groups)
47
+
48
+
49
+ @dataclass
50
+ class ConvertedAgent:
51
+ mode: dict[str, object]
52
+ rules_content: str
53
+ slug: str
54
+
55
+
56
+ def convert_agent(agent: Agent) -> ConvertedAgent:
57
+ """Convert an Agent IR to a Roo custom mode + rules file."""
58
+ slug = agent.name
59
+ groups = _map_tools_to_groups(agent.tools)
60
+
61
+ mode: dict[str, object] = {
62
+ "slug": slug,
63
+ "name": _humanize_name(slug),
64
+ "description": agent.description,
65
+ "roleDefinition": _extract_role_definition(agent.body),
66
+ "groups": groups,
67
+ }
68
+
69
+ lines = [
70
+ f"# {_humanize_name(slug)}",
71
+ "",
72
+ f"> Converted from Claude Code agent: `{agent.name}`",
73
+ "",
74
+ agent.body,
75
+ ]
76
+
77
+ footer_parts = []
78
+ if agent.model:
79
+ footer_parts.append(f"Originally configured for model: {agent.model}")
80
+ if agent.tools:
81
+ footer_parts.append(f"Original tool list: {', '.join(agent.tools)}")
82
+
83
+ if footer_parts:
84
+ lines.append("")
85
+ lines.append("---")
86
+ lines.append("")
87
+ for part in footer_parts:
88
+ lines.append(f"*{part}*")
89
+
90
+ return ConvertedAgent(
91
+ mode=mode,
92
+ rules_content="\n".join(lines),
93
+ slug=slug,
94
+ )
@@ -0,0 +1,32 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from c2roo.models.command import Command
4
+
5
+
6
+ @dataclass
7
+ class ConvertedCommand:
8
+ frontmatter: dict[str, object]
9
+ body: str
10
+ name: str
11
+ dropped_fields: list[str] = field(default_factory=list)
12
+
13
+
14
+ def convert_command(cmd: Command) -> ConvertedCommand:
15
+ """Convert a Command IR to Roo slash command format."""
16
+ frontmatter: dict[str, object] = {}
17
+
18
+ if cmd.description:
19
+ frontmatter["description"] = cmd.description
20
+ if cmd.argument_hint:
21
+ frontmatter["argument-hint"] = cmd.argument_hint
22
+
23
+ dropped = []
24
+ if cmd.allowed_tools:
25
+ dropped.append("allowed-tools")
26
+
27
+ return ConvertedCommand(
28
+ frontmatter=frontmatter,
29
+ body=cmd.body,
30
+ name=cmd.name,
31
+ dropped_fields=sorted(dropped),
32
+ )
@@ -0,0 +1,43 @@
1
+ from c2roo.models.hook import Hook
2
+
3
+
4
+ def convert_hooks(hooks: list[Hook]) -> str:
5
+ """Convert Hook IRs to a Roo guidance rules markdown document."""
6
+ if not hooks:
7
+ return ""
8
+
9
+ lines = [
10
+ "# Converted from Claude Code hooks",
11
+ "",
12
+ "> These hooks were originally enforced automatically by Claude Code.",
13
+ "> Roo Code does not support hooks. The guidance below describes the",
14
+ "> original behavior — consider running these manually or setting up",
15
+ "> an equivalent workflow.",
16
+ "",
17
+ ]
18
+
19
+ # Group by event type
20
+ by_event: dict[str, list[Hook]] = {}
21
+ for hook in hooks:
22
+ by_event.setdefault(hook.event, []).append(hook)
23
+
24
+ for event, event_hooks in by_event.items():
25
+ lines.append(f"## {event}")
26
+ lines.append("")
27
+
28
+ for hook in event_hooks:
29
+ if hook.matcher:
30
+ lines.append(f"**Trigger:** when tools matching `{hook.matcher}` are used")
31
+ lines.append("")
32
+
33
+ lines.append("**Original command:**")
34
+ lines.append("```")
35
+ lines.append(hook.command)
36
+ lines.append("```")
37
+
38
+ if hook.timeout:
39
+ lines.append(f"*Timeout: {hook.timeout}s*")
40
+
41
+ lines.append("")
42
+
43
+ return "\n".join(lines).rstrip() + "\n"
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+
3
+ from c2roo.models.mcp import McpServer
4
+
5
+
6
+ def _resolve_plugin_root(value: str, install_path: Path) -> str:
7
+ """Replace ${CLAUDE_PLUGIN_ROOT} with the actual install path."""
8
+ return value.replace("${CLAUDE_PLUGIN_ROOT}", str(install_path))
9
+
10
+
11
+ def convert_mcp(servers: dict[str, McpServer], install_path: Path) -> dict[str, dict[str, object]]:
12
+ """Convert McpServer IRs to a Roo mcp.json-compatible dict."""
13
+ if not servers:
14
+ return {}
15
+
16
+ result: dict[str, dict[str, object]] = {}
17
+ for name, server in servers.items():
18
+ entry: dict[str, object] = {}
19
+
20
+ if server.command:
21
+ entry["command"] = _resolve_plugin_root(server.command, install_path)
22
+ if server.args:
23
+ entry["args"] = [_resolve_plugin_root(a, install_path) for a in server.args]
24
+ if server.env:
25
+ entry["env"] = {k: _resolve_plugin_root(v, install_path) for k, v in server.env.items()}
26
+ if server.url:
27
+ entry["url"] = server.url
28
+ if server.headers:
29
+ entry["headers"] = server.headers
30
+ if server.disabled:
31
+ entry["disabled"] = True
32
+ if server.always_allow:
33
+ entry["alwaysAllow"] = server.always_allow
34
+ if server.timeout:
35
+ entry["timeout"] = server.timeout
36
+
37
+ result[name] = entry
38
+
39
+ return result
@@ -0,0 +1,48 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from c2roo.models.skill import Skill
4
+
5
+ CLAUDE_SPECIFIC_FIELDS = ["disable-model-invocation", "user-invocable", "context", "agent"]
6
+
7
+
8
+ @dataclass
9
+ class ConvertedSkill:
10
+ frontmatter: dict[str, object]
11
+ body: str
12
+ name: str
13
+ dropped_fields: list[str] = field(default_factory=list)
14
+
15
+
16
+ def convert_skill(skill: Skill) -> ConvertedSkill:
17
+ """Convert a Skill IR to Roo skill format (cleaned frontmatter)."""
18
+ frontmatter: dict[str, object] = {
19
+ "name": skill.name,
20
+ "description": skill.description,
21
+ }
22
+
23
+ if skill.license:
24
+ frontmatter["license"] = skill.license
25
+ if skill.compatibility:
26
+ frontmatter["compatibility"] = skill.compatibility
27
+ if skill.metadata:
28
+ frontmatter["metadata"] = skill.metadata
29
+ if skill.allowed_tools:
30
+ frontmatter["allowed-tools"] = skill.allowed_tools
31
+
32
+ # Track which Claude-specific fields were present and dropped
33
+ dropped = []
34
+ if skill.agent is not None:
35
+ dropped.append("agent")
36
+ if skill.context is not None:
37
+ dropped.append("context")
38
+ if skill.disable_model_invocation is not None:
39
+ dropped.append("disable-model-invocation")
40
+ if skill.user_invocable is not None:
41
+ dropped.append("user-invocable")
42
+
43
+ return ConvertedSkill(
44
+ frontmatter=frontmatter,
45
+ body=skill.body,
46
+ name=skill.name,
47
+ dropped_fields=sorted(dropped),
48
+ )
@@ -0,0 +1,16 @@
1
+ from c2roo.models.agent import Agent
2
+ from c2roo.models.command import Command
3
+ from c2roo.models.hook import Hook
4
+ from c2roo.models.mcp import McpServer
5
+ from c2roo.models.plugin import Plugin, PluginMetadata
6
+ from c2roo.models.skill import Skill
7
+
8
+ __all__ = [
9
+ "Agent",
10
+ "Command",
11
+ "Hook",
12
+ "McpServer",
13
+ "Plugin",
14
+ "PluginMetadata",
15
+ "Skill",
16
+ ]
c2roo/models/agent.py ADDED
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class Agent:
6
+ name: str
7
+ description: str
8
+ body: str
9
+ model: str | None = None
10
+ tools: list[str] = field(default_factory=list)
11
+ color: str | None = None
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Command:
6
+ name: str
7
+ description: str | None = None
8
+ argument_hint: str | None = None
9
+ allowed_tools: str | None = None
10
+ body: str = ""
c2roo/models/hook.py ADDED
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Hook:
6
+ event: str
7
+ command: str
8
+ matcher: str | None = None
9
+ timeout: int | None = None
c2roo/models/mcp.py ADDED
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class McpServer:
6
+ name: str
7
+ command: str | None = None
8
+ args: list[str] = field(default_factory=list)
9
+ env: dict[str, str] = field(default_factory=dict)
10
+ url: str | None = None
11
+ headers: dict[str, str] = field(default_factory=dict)
12
+ disabled: bool = False
13
+ always_allow: list[str] = field(default_factory=list)
14
+ timeout: int | None = None
c2roo/models/plugin.py ADDED
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+
4
+ from c2roo.models.agent import Agent
5
+ from c2roo.models.command import Command
6
+ from c2roo.models.hook import Hook
7
+ from c2roo.models.mcp import McpServer
8
+ from c2roo.models.skill import Skill
9
+
10
+
11
+ @dataclass
12
+ class PluginMetadata:
13
+ name: str
14
+ description: str
15
+ version: str | None = None
16
+ author: str | None = None
17
+ homepage: str | None = None
18
+ license: str | None = None
19
+
20
+
21
+ @dataclass
22
+ class Plugin:
23
+ metadata: PluginMetadata
24
+ source_path: Path
25
+ skills: list[Skill] = field(default_factory=list)
26
+ commands: list[Command] = field(default_factory=list)
27
+ agents: list[Agent] = field(default_factory=list)
28
+ hooks: list[Hook] = field(default_factory=list)
29
+ mcp_servers: dict[str, McpServer] = field(default_factory=dict)
c2roo/models/skill.py ADDED
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+
4
+
5
+ @dataclass
6
+ class Skill:
7
+ name: str
8
+ description: str
9
+ body: str
10
+ license: str | None = None
11
+ compatibility: str | None = None
12
+ metadata: dict[str, str] = field(default_factory=dict)
13
+ allowed_tools: str | None = None
14
+ resources: list[Path] = field(default_factory=list)
15
+ disable_model_invocation: bool | None = None
16
+ user_invocable: bool | None = None
17
+ context: str | None = None
18
+ agent: str | None = None
@@ -0,0 +1,3 @@
1
+ from c2roo.parser.plugin_parser import parse_plugin
2
+
3
+ __all__ = ["parse_plugin"]