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.
- claude_code_plugins_sdk-0.1.0/.github/workflows/ci.yml +38 -0
- claude_code_plugins_sdk-0.1.0/.github/workflows/release.yml +34 -0
- claude_code_plugins_sdk-0.1.0/.gitignore +13 -0
- claude_code_plugins_sdk-0.1.0/.python-version +1 -0
- claude_code_plugins_sdk-0.1.0/PKG-INFO +127 -0
- claude_code_plugins_sdk-0.1.0/README.md +103 -0
- claude_code_plugins_sdk-0.1.0/plans/20260221-1851-phase1-sdk.md +391 -0
- claude_code_plugins_sdk-0.1.0/plans/20260221-1911-remote-fetch.md +215 -0
- claude_code_plugins_sdk-0.1.0/plans/20260222-2141-validation.md +192 -0
- claude_code_plugins_sdk-0.1.0/pyproject.toml +73 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/__init__.py +80 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/_plugin.py +29 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/errors.py +20 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/__init__.py +3 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/_dispatcher.py +51 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/_git.py +54 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/fetchers/_http.py +24 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/loaders/__init__.py +10 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/loaders/marketplace.py +39 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/loaders/plugin.py +143 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/__init__.py +47 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/agent.py +19 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/command.py +13 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/hook.py +42 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/lsp.py +24 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/marketplace.py +108 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/mcp.py +16 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/plugin.py +32 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/models/skill.py +11 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/py.typed +0 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/__init__.py +39 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/_marketplace.py +112 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/_plugin.py +15 -0
- claude_code_plugins_sdk-0.1.0/src/claude_code_plugins_sdk/validation/_result.py +28 -0
- claude_code_plugins_sdk-0.1.0/tests/__init__.py +0 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/__init__.py +0 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/marketplace/.claude-plugin/marketplace.json +71 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/marketplace/.claude-plugin/minimal-marketplace.json +12 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/.claude-plugin/plugin.json +14 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/.lsp.json +13 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/.mcp.json +16 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/agents/minimal.md +7 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/agents/reviewer.md +15 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/commands/review.md +15 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/hooks/hooks.json +25 -0
- claude_code_plugins_sdk-0.1.0/tests/fixtures/plugin/skills/code-review/SKILL.md +12 -0
- claude_code_plugins_sdk-0.1.0/tests/test_errors.py +22 -0
- claude_code_plugins_sdk-0.1.0/tests/test_fetchers.py +151 -0
- claude_code_plugins_sdk-0.1.0/tests/test_loaders.py +132 -0
- claude_code_plugins_sdk-0.1.0/tests/test_models_components.py +142 -0
- claude_code_plugins_sdk-0.1.0/tests/test_models_marketplace.py +103 -0
- claude_code_plugins_sdk-0.1.0/tests/test_models_plugin.py +49 -0
- claude_code_plugins_sdk-0.1.0/tests/test_validation.py +185 -0
- 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 @@
|
|
|
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
|