claude-code-plugins-sdk 0.1.0__tar.gz

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.
Files changed (54) hide show
  1. claude_code_plugins_sdk-0.1.0/.github/workflows/ci.yml +38 -0
  2. claude_code_plugins_sdk-0.1.0/.github/workflows/release.yml +34 -0
  3. claude_code_plugins_sdk-0.1.0/.gitignore +13 -0
  4. claude_code_plugins_sdk-0.1.0/.python-version +1 -0
  5. claude_code_plugins_sdk-0.1.0/PKG-INFO +127 -0
  6. claude_code_plugins_sdk-0.1.0/README.md +103 -0
  7. claude_code_plugins_sdk-0.1.0/plans/20260221-1851-phase1-sdk.md +391 -0
  8. claude_code_plugins_sdk-0.1.0/plans/20260221-1911-remote-fetch.md +215 -0
  9. claude_code_plugins_sdk-0.1.0/plans/20260222-2141-validation.md +192 -0
  10. claude_code_plugins_sdk-0.1.0/pyproject.toml +73 -0
  11. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/__init__.py +80 -0
  12. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/_plugin.py +29 -0
  13. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/errors.py +20 -0
  14. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/__init__.py +3 -0
  15. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/_dispatcher.py +51 -0
  16. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/_git.py +54 -0
  17. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/_http.py +24 -0
  18. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/loaders/__init__.py +10 -0
  19. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/loaders/marketplace.py +39 -0
  20. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/loaders/plugin.py +143 -0
  21. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/__init__.py +47 -0
  22. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/agent.py +19 -0
  23. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/command.py +13 -0
  24. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/hook.py +42 -0
  25. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/lsp.py +24 -0
  26. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/marketplace.py +108 -0
  27. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/mcp.py +16 -0
  28. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/plugin.py +32 -0
  29. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/skill.py +11 -0
  30. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/py.typed +0 -0
  31. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/__init__.py +39 -0
  32. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/_marketplace.py +112 -0
  33. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/_plugin.py +15 -0
  34. claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/_result.py +28 -0
  35. claude_code_plugins_sdk-0.1.0/tests/__init__.py +0 -0
  36. claude_code_plugins_sdk-0.1.0/tests/fixtures/__init__.py +0 -0
  37. claude_code_plugins_sdk-0.1.0/tests/fixtures/marketplace/.claude-plugin/marketplace.json +71 -0
  38. claude_code_plugins_sdk-0.1.0/tests/fixtures/marketplace/.claude-plugin/minimal-marketplace.json +12 -0
  39. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/.claude-plugin/plugin.json +14 -0
  40. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/.lsp.json +13 -0
  41. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/.mcp.json +16 -0
  42. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/agents/minimal.md +7 -0
  43. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/agents/reviewer.md +15 -0
  44. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/commands/review.md +15 -0
  45. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/hooks/hooks.json +25 -0
  46. claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/skills/code-review/SKILL.md +12 -0
  47. claude_code_plugins_sdk-0.1.0/tests/test_errors.py +22 -0
  48. claude_code_plugins_sdk-0.1.0/tests/test_fetchers.py +151 -0
  49. claude_code_plugins_sdk-0.1.0/tests/test_loaders.py +132 -0
  50. claude_code_plugins_sdk-0.1.0/tests/test_models_components.py +142 -0
  51. claude_code_plugins_sdk-0.1.0/tests/test_models_marketplace.py +103 -0
  52. claude_code_plugins_sdk-0.1.0/tests/test_models_plugin.py +49 -0
  53. claude_code_plugins_sdk-0.1.0/tests/test_validation.py +185 -0
  54. claude_code_plugins_sdk-0.1.0/uv.lock +642 -0
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint-test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v4
22
+ with:
23
+ version: "latest"
24
+
25
+ - name: Set up Python
26
+ run: uv python install ${{ matrix.python-version }}
27
+
28
+ - name: Install dependencies
29
+ run: uv sync
30
+
31
+ - name: Ruff (lint)
32
+ run: uv run ruff check .
33
+
34
+ - name: Ruff (format)
35
+ run: uv run ruff format --check .
36
+
37
+ - name: Pytest
38
+ run: uv run pytest -m "not integration"
@@ -0,0 +1,34 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ id-token: write
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v4
24
+ with:
25
+ version: "latest"
26
+
27
+ - name: Set up Python
28
+ run: uv python install 3.12
29
+
30
+ - name: Build package
31
+ run: uv build
32
+
33
+ - name: Publish to PyPI
34
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .coverage
8
+ htmlcov/
9
+ .mypy_cache/
10
+ .pyrightcache/
11
+ .tycache/
12
+ .ruff_cache/
13
+ .pytest_cache/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-code-plugins-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Claude Code plugins and marketplaces
5
+ Project-URL: Repository, https://github.com/doron-cohen/claude-code-plugins-sdk
6
+ Author-email: Doron Cohen <4966182+doron-cohen@users.noreply.github.com>
7
+ License: MIT
8
+ Keywords: anthropic,claude,claude-code,marketplace,plugins,sdk
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.28.1
21
+ Requires-Dist: pydantic>=2.0
22
+ Requires-Dist: python-frontmatter>=1.1
23
+ Description-Content-Type: text/markdown
24
+
25
+ # claude-code-plugins-sdk
26
+
27
+ A Python library for working with Claude Code plugins and marketplaces.
28
+
29
+ Claude Code has a plugin ecosystem — agents, skills, commands, hooks, MCP servers — but no official Python tooling to work with it. This library lets you load, parse, and validate plugins and marketplaces from disk or from remote sources.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install claude-code-plugins-sdk
35
+ ```
36
+
37
+ Requires Python 3.10+.
38
+
39
+ ## What you can do
40
+
41
+ Load a local marketplace and inspect its plugins:
42
+
43
+ ```python
44
+ from pathlib import Path
45
+ from claude_code_plugins_sdk import load_marketplace
46
+
47
+ marketplace = load_marketplace(Path("./my-marketplace"))
48
+
49
+ for plugin in marketplace.plugins:
50
+ print(plugin.name, plugin.version)
51
+ ```
52
+
53
+ Fetch a marketplace from GitHub:
54
+
55
+ ```python
56
+ from claude_code_plugins_sdk import fetch_marketplace
57
+
58
+ # GitHub shorthand
59
+ marketplace = fetch_marketplace("anthropics/claude-code")
60
+
61
+ # Pinned to a tag
62
+ from claude_code_plugins_sdk import GitHubSource
63
+ marketplace = fetch_marketplace(GitHubSource(source="github", repo="anthropics/claude-code", ref="v1.0"))
64
+
65
+ # Raw URL to a marketplace.json
66
+ marketplace = fetch_marketplace("https://example.com/.claude-plugin/marketplace.json")
67
+ ```
68
+
69
+ Load a plugin directory and read its agents, skills, commands, and hooks:
70
+
71
+ ```python
72
+ from pathlib import Path
73
+ from claude_code_plugins_sdk import load_plugin
74
+
75
+ plugin = load_plugin(Path("./my-plugin"))
76
+
77
+ print(plugin.manifest.name) # plugin name from plugin.json
78
+ print(plugin.agents) # list of AgentDefinition
79
+ print(plugin.skills) # list of SkillDefinition
80
+ print(plugin.commands) # list of CommandDefinition
81
+ print(plugin.hooks) # HooksConfig or None
82
+ print(plugin.mcp_servers) # MCPServersConfig or None
83
+ ```
84
+
85
+ Load individual component files directly:
86
+
87
+ ```python
88
+ from claude_code_plugins_sdk import load_agent, load_skill, load_command
89
+
90
+ agent = load_agent(Path("./agents/reviewer.md"))
91
+ print(agent.name, agent.tools) # tools is always a list, even though YAML stores it as a comma string
92
+
93
+ skill = load_skill(Path("./skills/code-review/SKILL.md"))
94
+ command = load_command(Path("./commands/review.md"))
95
+ ```
96
+
97
+ ## Plugin structure
98
+
99
+ A Claude Code plugin is a directory with this layout:
100
+
101
+ ```
102
+ my-plugin/
103
+ ├── .claude-plugin/
104
+ │ └── plugin.json # optional manifest
105
+ ├── agents/ # *.md files with YAML frontmatter
106
+ ├── skills/ # */SKILL.md files
107
+ ├── commands/ # *.md files
108
+ ├── hooks/
109
+ │ └── hooks.json
110
+ ├── .mcp.json
111
+ └── .lsp.json
112
+ ```
113
+
114
+ A marketplace is a directory with `.claude-plugin/marketplace.json` listing plugins and where to find them.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ git clone git@github.com:doron-cohen/claude-code-plugins-sdk.git
120
+ cd claude-code-plugins-sdk
121
+ uv sync
122
+ uv run pytest
123
+ ```
124
+
125
+ ```bash
126
+ uv run pytest -m integration # hits real remote marketplaces, needs network
127
+ ```
@@ -0,0 +1,103 @@
1
+ # claude-code-plugins-sdk
2
+
3
+ A Python library for working with Claude Code plugins and marketplaces.
4
+
5
+ Claude Code has a plugin ecosystem — agents, skills, commands, hooks, MCP servers — but no official Python tooling to work with it. This library lets you load, parse, and validate plugins and marketplaces from disk or from remote sources.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install claude-code-plugins-sdk
11
+ ```
12
+
13
+ Requires Python 3.10+.
14
+
15
+ ## What you can do
16
+
17
+ Load a local marketplace and inspect its plugins:
18
+
19
+ ```python
20
+ from pathlib import Path
21
+ from claude_code_plugins_sdk import load_marketplace
22
+
23
+ marketplace = load_marketplace(Path("./my-marketplace"))
24
+
25
+ for plugin in marketplace.plugins:
26
+ print(plugin.name, plugin.version)
27
+ ```
28
+
29
+ Fetch a marketplace from GitHub:
30
+
31
+ ```python
32
+ from claude_code_plugins_sdk import fetch_marketplace
33
+
34
+ # GitHub shorthand
35
+ marketplace = fetch_marketplace("anthropics/claude-code")
36
+
37
+ # Pinned to a tag
38
+ from claude_code_plugins_sdk import GitHubSource
39
+ marketplace = fetch_marketplace(GitHubSource(source="github", repo="anthropics/claude-code", ref="v1.0"))
40
+
41
+ # Raw URL to a marketplace.json
42
+ marketplace = fetch_marketplace("https://example.com/.claude-plugin/marketplace.json")
43
+ ```
44
+
45
+ Load a plugin directory and read its agents, skills, commands, and hooks:
46
+
47
+ ```python
48
+ from pathlib import Path
49
+ from claude_code_plugins_sdk import load_plugin
50
+
51
+ plugin = load_plugin(Path("./my-plugin"))
52
+
53
+ print(plugin.manifest.name) # plugin name from plugin.json
54
+ print(plugin.agents) # list of AgentDefinition
55
+ print(plugin.skills) # list of SkillDefinition
56
+ print(plugin.commands) # list of CommandDefinition
57
+ print(plugin.hooks) # HooksConfig or None
58
+ print(plugin.mcp_servers) # MCPServersConfig or None
59
+ ```
60
+
61
+ Load individual component files directly:
62
+
63
+ ```python
64
+ from claude_code_plugins_sdk import load_agent, load_skill, load_command
65
+
66
+ agent = load_agent(Path("./agents/reviewer.md"))
67
+ print(agent.name, agent.tools) # tools is always a list, even though YAML stores it as a comma string
68
+
69
+ skill = load_skill(Path("./skills/code-review/SKILL.md"))
70
+ command = load_command(Path("./commands/review.md"))
71
+ ```
72
+
73
+ ## Plugin structure
74
+
75
+ A Claude Code plugin is a directory with this layout:
76
+
77
+ ```
78
+ my-plugin/
79
+ ├── .claude-plugin/
80
+ │ └── plugin.json # optional manifest
81
+ ├── agents/ # *.md files with YAML frontmatter
82
+ ├── skills/ # */SKILL.md files
83
+ ├── commands/ # *.md files
84
+ ├── hooks/
85
+ │ └── hooks.json
86
+ ├── .mcp.json
87
+ └── .lsp.json
88
+ ```
89
+
90
+ A marketplace is a directory with `.claude-plugin/marketplace.json` listing plugins and where to find them.
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ git clone git@github.com:doron-cohen/claude-code-plugins-sdk.git
96
+ cd claude-code-plugins-sdk
97
+ uv sync
98
+ uv run pytest
99
+ ```
100
+
101
+ ```bash
102
+ uv run pytest -m integration # hits real remote marketplaces, needs network
103
+ ```
@@ -0,0 +1,391 @@
1
+ # Phase 1 SDK — Core Models & Loaders
2
+
3
+ **Goal:** A working Python library that can load a marketplace file and a plugin directory, validate them against the official Claude Code schema, and expose typed Python objects.
4
+
5
+ **Scope:** Read-only. No HTTP, no installs, no CLI.
6
+
7
+ ---
8
+
9
+ ## What to build
10
+
11
+ ### 1. Package skeleton
12
+
13
+ `src/claude_code_plugins_sdk/` with a `src` layout (already scaffolded by uv).
14
+
15
+ ```
16
+ src/claude_code_plugins_sdk/
17
+ ├── __init__.py ← re-export the public API
18
+ ├── errors.py ← LoadError
19
+ ├── models/
20
+ │ ├── __init__.py
21
+ │ ├── plugin.py ← PluginManifest, Author
22
+ │ ├── marketplace.py ← MarketplaceManifest, PluginEntry, source types
23
+ │ ├── agent.py ← AgentDefinition
24
+ │ ├── skill.py ← SkillDefinition
25
+ │ ├── command.py ← CommandDefinition
26
+ │ ├── hook.py ← HooksConfig, HookEvent, HookEntry
27
+ │ ├── mcp.py ← MCPServersConfig
28
+ │ └── lsp.py ← LSPServersConfig
29
+ ├── loaders/
30
+ │ ├── __init__.py
31
+ │ ├── marketplace.py ← load_marketplace(path)
32
+ │ └── plugin.py ← load_plugin(path) → Plugin
33
+ └── _plugin.py ← Plugin dataclass (domain object)
34
+ ```
35
+
36
+ ### 2. Models
37
+
38
+ All models are Pydantic v2 with `model_config = ConfigDict(extra="allow", populate_by_name=True)`.
39
+ Never break on unknown fields — the ecosystem evolves.
40
+
41
+ #### `plugin.py` — `.claude-plugin/plugin.json`
42
+
43
+ ```python
44
+ class Author(BaseModel):
45
+ name: str
46
+ email: str | None = None
47
+ url: str | None = None
48
+
49
+ class PluginManifest(BaseModel):
50
+ name: str
51
+ version: str | None = None
52
+ description: str | None = None
53
+ author: Author | None = None
54
+ homepage: str | None = None
55
+ repository: str | None = None
56
+ license: str | None = None
57
+ keywords: list[str] = []
58
+ # component paths — each can be str, list[str], or dict (inline config)
59
+ commands: str | list[str] | None = None
60
+ agents: str | list[str] | None = None
61
+ skills: str | list[str] | None = None
62
+ hooks: str | list[str] | dict[str, Any] | None = None
63
+ mcp_servers: str | list[str] | dict[str, Any] | None = Field(None, alias="mcpServers")
64
+ output_styles: str | list[str] | None = Field(None, alias="outputStyles")
65
+ lsp_servers: str | list[str] | dict[str, Any] | None = Field(None, alias="lspServers")
66
+ ```
67
+
68
+ #### `marketplace.py` — `.claude-plugin/marketplace.json`
69
+
70
+ Plugin source is a discriminated union. A plain string is a relative path.
71
+ Object sources are discriminated on the `source` field: `"github"`, `"url"`, `"npm"`, `"pip"`.
72
+
73
+ ```python
74
+ class GitHubSource(BaseModel):
75
+ source: Literal["github"]
76
+ repo: str # "owner/repo"
77
+ ref: str | None = None
78
+ sha: str | None = None
79
+
80
+ class URLSource(BaseModel):
81
+ source: Literal["url"]
82
+ url: str # must end with .git
83
+ ref: str | None = None
84
+ sha: str | None = None
85
+
86
+ class NPMSource(BaseModel):
87
+ source: Literal["npm"]
88
+ package: str
89
+ version: str | None = None
90
+ registry: str | None = None
91
+
92
+ class PIPSource(BaseModel):
93
+ source: Literal["pip"]
94
+ package: str
95
+ version: str | None = None
96
+ registry: str | None = None
97
+
98
+ PluginSource = Annotated[
99
+ GitHubSource | URLSource | NPMSource | PIPSource,
100
+ Field(discriminator="source")
101
+ ]
102
+
103
+ class MarketplaceOwner(BaseModel):
104
+ name: str
105
+ email: str | None = None
106
+
107
+ class MarketplaceMetadata(BaseModel):
108
+ description: str | None = None
109
+ version: str | None = None
110
+ plugin_root: str | None = Field(None, alias="pluginRoot")
111
+
112
+ class PluginEntry(BaseModel):
113
+ # source is str (relative path) OR one of the typed source objects
114
+ name: str
115
+ source: str | PluginSource
116
+ description: str | None = None
117
+ version: str | None = None
118
+ author: Author | None = None
119
+ homepage: str | None = None
120
+ repository: str | None = None
121
+ license: str | None = None
122
+ keywords: list[str] = []
123
+ category: str | None = None
124
+ tags: list[str] = []
125
+ strict: bool = True
126
+ # inline component paths (same as PluginManifest)
127
+ commands: ...
128
+ agents: ...
129
+ hooks: ...
130
+ mcp_servers: ...
131
+ lsp_servers: ...
132
+
133
+ class MarketplaceManifest(BaseModel):
134
+ schema_url: str | None = Field(None, alias="$schema")
135
+ name: str
136
+ version: str | None = None
137
+ description: str | None = None
138
+ owner: MarketplaceOwner
139
+ metadata: MarketplaceMetadata | None = None
140
+ plugins: list[PluginEntry]
141
+ ```
142
+
143
+ #### `agent.py` — `agents/*.md` frontmatter
144
+
145
+ The `tools` field is a **comma-separated string** in the YAML, not a list. Parse it in a validator.
146
+
147
+ ```python
148
+ class AgentDefinition(BaseModel):
149
+ name: str
150
+ description: str
151
+ tools: list[str] = [] # stored as "Read, Write, Bash" in YAML
152
+ color: str | None = None
153
+ body: str = "" # markdown body (the agent system prompt)
154
+
155
+ @field_validator("tools", mode="before")
156
+ @classmethod
157
+ def parse_tools_string(cls, v):
158
+ if isinstance(v, str):
159
+ return [t.strip() for t in v.split(",") if t.strip()]
160
+ return v
161
+ ```
162
+
163
+ #### `skill.py` — `skills/<name>/SKILL.md` frontmatter
164
+
165
+ ```python
166
+ class SkillDefinition(BaseModel):
167
+ description: str | None = None
168
+ name: str | None = None
169
+ disable_model_invocation: bool = Field(False, alias="disable-model-invocation")
170
+ body: str = ""
171
+ ```
172
+
173
+ #### `command.py` — `commands/*.md` frontmatter
174
+
175
+ Note: `allowed-tools` is a **YAML list**, not a comma-separated string (opposite of agents).
176
+
177
+ ```python
178
+ class CommandDefinition(BaseModel):
179
+ name: str | None = None
180
+ description: str | None = None
181
+ argument_hint: str | None = Field(None, alias="argument-hint")
182
+ allowed_tools: list[str] = Field([], alias="allowed-tools")
183
+ agent: str | None = None
184
+ body: str = ""
185
+ ```
186
+
187
+ #### `hook.py` — `hooks/hooks.json`
188
+
189
+ ```python
190
+ class HookEntry(BaseModel):
191
+ type: Literal["command", "prompt", "agent"]
192
+ command: str | None = None # for type="command"
193
+ prompt: str | None = None # for type="prompt"
194
+ agent: str | None = None # for type="agent"
195
+
196
+ class HookMatcher(BaseModel):
197
+ matcher: str | None = None
198
+ hooks: list[HookEntry]
199
+
200
+ HookEvent = Literal[
201
+ "PreToolUse", "PostToolUse", "PostToolUseFailure",
202
+ "SessionStart", "SessionEnd",
203
+ "SubagentStart", "SubagentStop",
204
+ "Stop", "Notification",
205
+ "UserPromptSubmit", "PermissionRequest",
206
+ "PreCompact", "TaskCompleted", "TeammateIdle",
207
+ ]
208
+
209
+ class HooksConfig(BaseModel):
210
+ hooks: dict[str, list[HookMatcher]] # event name → matchers
211
+ ```
212
+
213
+ #### `mcp.py` — `.mcp.json`
214
+
215
+ ```python
216
+ class MCPServerConfig(BaseModel):
217
+ command: str
218
+ args: list[str] = []
219
+ env: dict[str, str] = {}
220
+ cwd: str | None = None
221
+
222
+ class MCPServersConfig(BaseModel):
223
+ mcp_servers: dict[str, MCPServerConfig] = Field({}, alias="mcpServers")
224
+ ```
225
+
226
+ #### `lsp.py` — `.lsp.json`
227
+
228
+ ```python
229
+ class LSPServerConfig(BaseModel):
230
+ command: str
231
+ extension_to_language: dict[str, str] = Field({}, alias="extensionToLanguage")
232
+ args: list[str] = []
233
+ transport: Literal["stdio", "socket"] = "stdio"
234
+ env: dict[str, str] = {}
235
+ # ... other optional fields
236
+
237
+ LSPServersConfig = dict[str, LSPServerConfig]
238
+ ```
239
+
240
+ ### 3. `Plugin` domain object
241
+
242
+ ```python
243
+ @dataclass
244
+ class Plugin:
245
+ root: Path
246
+ manifest: PluginManifest | None
247
+ agents: list[AgentDefinition]
248
+ commands: list[CommandDefinition]
249
+ skills: list[SkillDefinition]
250
+ hooks: HooksConfig | None
251
+ mcp_servers: MCPServersConfig | None
252
+ lsp_servers: LSPServersConfig | None
253
+ ```
254
+
255
+ ### 4. Loaders
256
+
257
+ #### `load_marketplace(path: Path) -> MarketplaceManifest`
258
+
259
+ - If `path` is a file: load it directly as JSON
260
+ - If `path` is a directory: look for `.claude-plugin/marketplace.json` inside it
261
+ - Parse with `json.loads`, validate with `MarketplaceManifest.model_validate(data)`
262
+ - Raise `LoadError` for missing files or JSON parse failures
263
+ - Let `pydantic.ValidationError` bubble up as-is
264
+
265
+ #### `load_plugin(path: Path) -> Plugin`
266
+
267
+ - `path` must be a directory (the plugin root)
268
+ - Load `.claude-plugin/plugin.json` if present (optional)
269
+ - Discover agents in `agents/*.md` using `python-frontmatter`
270
+ - Discover commands in `commands/*.md`
271
+ - Discover skills in `skills/*/SKILL.md`
272
+ - Load `hooks/hooks.json` if present
273
+ - Load `.mcp.json` if present
274
+ - Load `.lsp.json` if present
275
+ - Return `Plugin` dataclass
276
+
277
+ #### `load_agent(path: Path) -> AgentDefinition` / `load_skill(path)` / etc.
278
+
279
+ Individual loaders for each component — used internally by `load_plugin` but also exposed publicly.
280
+
281
+ ### 5. Public API (`__init__.py`)
282
+
283
+ ```python
284
+ from .loaders.marketplace import load_marketplace
285
+ from .loaders.plugin import load_plugin, load_agent, load_skill, load_command
286
+ from .models import *
287
+ from ._plugin import Plugin
288
+ from .errors import LoadError
289
+ ```
290
+
291
+ ### 6. `errors.py`
292
+
293
+ ```python
294
+ class LoadError(Exception):
295
+ def __init__(self, message: str, path: Path | None = None):
296
+ self.path = path
297
+ super().__init__(message)
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Tests
303
+
304
+ Test files mirror the modules. Use `tests/fixtures/` for real example files.
305
+
306
+ ### Fixtures to create
307
+
308
+ **`tests/fixtures/marketplace/.claude-plugin/marketplace.json`**
309
+ A full marketplace with one plugin per source type (relative, github, url, npm, pip).
310
+
311
+ **`tests/fixtures/plugin/.claude-plugin/plugin.json`**
312
+ A full plugin manifest.
313
+
314
+ **`tests/fixtures/plugin/agents/reviewer.md`**
315
+ Agent with `tools` as comma-separated string and a body.
316
+
317
+ **`tests/fixtures/plugin/skills/code-review/SKILL.md`**
318
+ Skill with frontmatter.
319
+
320
+ **`tests/fixtures/plugin/commands/review.md`**
321
+ Command with `allowed-tools` as a YAML list.
322
+
323
+ **`tests/fixtures/plugin/hooks/hooks.json`**
324
+ Hook config with PostToolUse and SessionStart.
325
+
326
+ **`tests/fixtures/plugin/.mcp.json`**
327
+ MCP server config.
328
+
329
+ **`tests/fixtures/plugin/.lsp.json`**
330
+ LSP server config.
331
+
332
+ ### Test files
333
+
334
+ - `tests/test_models_marketplace.py` — parse all source types, required field validation
335
+ - `tests/test_models_plugin.py` — parse plugin manifest
336
+ - `tests/test_models_components.py` — agent tools string, command allowed-tools list, skill frontmatter
337
+ - `tests/test_loaders.py` — load_marketplace from file and directory, load_plugin end-to-end
338
+ - `tests/test_errors.py` — LoadError on missing file, ValidationError on bad schema
339
+
340
+ ---
341
+
342
+ ## How to parallelize
343
+
344
+ These tasks have no dependencies on each other and can be done in parallel:
345
+
346
+ | Track A | Track B | Track C |
347
+ |---|---|---|
348
+ | `models/plugin.py` | `models/agent.py` | test fixtures (JSON/MD files) |
349
+ | `models/marketplace.py` | `models/skill.py` + `models/command.py` | |
350
+ | `models/hook.py` + `models/mcp.py` + `models/lsp.py` | `errors.py` | |
351
+
352
+ Then in parallel:
353
+ - `loaders/marketplace.py` (depends on marketplace model)
354
+ - `loaders/plugin.py` + `_plugin.py` (depends on all models)
355
+
356
+ Finally:
357
+ - `__init__.py` (depends on everything)
358
+ - All test files (can be written alongside their corresponding module)
359
+
360
+ ---
361
+
362
+ ## Validation
363
+
364
+ ```bash
365
+ # run tests
366
+ uv run pytest
367
+
368
+ # type check
369
+ uvx ty check
370
+
371
+ # lint
372
+ uv run ruff check src/ tests/
373
+
374
+ # compare against claude CLI
375
+ claude plugin validate tests/fixtures/marketplace
376
+ claude plugin validate tests/fixtures/plugin
377
+ ```
378
+
379
+ The Claude CLI's `claude plugin validate` command is the ground truth. If our validation rejects something the CLI accepts, we're wrong. If we accept something the CLI rejects, we should add a validator.
380
+
381
+ ---
382
+
383
+ ## Done when
384
+
385
+ - [ ] `uv run pytest` passes with >90% coverage
386
+ - [ ] `uvx ty check` exits 0
387
+ - [ ] `uv run ruff check src/ tests/` exits 0
388
+ - [ ] `load_marketplace("tests/fixtures/marketplace")` returns a valid `MarketplaceManifest`
389
+ - [ ] `load_plugin("tests/fixtures/plugin")` returns a `Plugin` with agents, skills, etc.
390
+ - [ ] `claude plugin validate tests/fixtures/marketplace` passes
391
+ - [ ] `claude plugin validate tests/fixtures/plugin` passes