Skip to content

Commit ca9add2

Browse files
TMHSDigitalclaude
andcommitted
fix(distribution): make the Cursor plugin installable and document the install
Following the README, a Cursor user got the 9 rules and none of the 16 skills: Cursor loads skills only from a skills folder or an installed plugin, and "reference skills by name in chat" does nothing. The repo's .cursor-plugin/plugin.json could not serve as that plugin: Cursor's schema (cursor/plugins schemas/plugin.schema.json) sets additionalProperties: false, and the manifest carried four inventory keys (snippets, templates, examples, showcase) that only mirrored the filesystem for this repo's own CI. jsonschema rejects it with exactly those four keys. - plugin.json drops the inventory keys (skills and rules stay) and adds homepage/repository; .cursor-plugin/marketplace.json is new. Both validate against Cursor's schemas. - The plugin-dist build ships the Cursor manifests, so a Cursor user clones the slim branch into ~/.cursor/plugins/local/ (README, landing install steps and the dist README say how; per-project copying of skills/ and rules/ is the fallback). - The landing page counts snippets, templates and examples from disk; Validate checks the manifest against the schema's allowed keys and the marketplace entry, instead of the inventory. CLAUDE.md, AGENTS.md and CONTRIBUTING.md drop the "list it in plugin.json" authoring step. Closes #347 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6dfa75c commit ca9add2

10 files changed

Lines changed: 101 additions & 214 deletions

File tree

‎.cursor-plugin/marketplace.json‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "blender-developer-tools",
3+
"owner": {
4+
"name": "TMHSDigital"
5+
},
6+
"metadata": {
7+
"description": "Blender Python skills and rules for Cursor: correct bpy for Blender 5.2 LTS and 4.5 LTS."
8+
},
9+
"plugins": [
10+
{
11+
"name": "blender-developer-tools",
12+
"source": "./",
13+
"description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development"
14+
}
15+
]
16+
}

‎.cursor-plugin/plugin.json‎

Lines changed: 2 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"author": {
77
"name": "TMHSDigital"
88
},
9+
"homepage": "https://tmhsdigital.github.io/Blender-Developer-Tools/",
10+
"repository": "https://github.com/TMHSDigital/Blender-Developer-Tools",
911
"license": "CC-BY-NC-ND-4.0 AND MIT",
1012
"keywords": [
1113
"cursor-plugin",
@@ -44,183 +46,5 @@
4446
"rules/validate-imported-mesh-scale.mdc",
4547
"rules/no-unapplied-modifiers-on-export.mdc",
4648
"rules/use-correct-axis-rna-per-exporter.mdc"
47-
],
48-
"snippets": [
49-
"snippets/action-ensure-channelbag-for-slot.py",
50-
"snippets/app-handler-registration.py",
51-
"snippets/bake_normal_high_to_low.py",
52-
"snippets/bmesh-load-edit-free.py",
53-
"snippets/canonical-object-creation.py",
54-
"snippets/canonical-object-deletion.py",
55-
"snippets/convex_hull_collider.py",
56-
"snippets/cross-version-property-delete.py",
57-
"snippets/decimate_to_budget.py",
58-
"snippets/depsgraph-evaluated-mesh.py",
59-
"snippets/driver-with-custom-function.py",
60-
"snippets/export_preset_godot.py",
61-
"snippets/export_preset_unity.py",
62-
"snippets/export_preset_unreal.py",
63-
"snippets/foreach-get-vertices.py",
64-
"snippets/foreach-set-vertices.py",
65-
"snippets/gltf_draco_export.py",
66-
"snippets/lod_chain.py",
67-
"snippets/pointerproperty-binding.py",
68-
"snippets/principled-bsdf-material.py",
69-
"snippets/register-classes-factory.py",
70-
"snippets/save_baked_image.py",
71-
"snippets/shader-node-group.py",
72-
"snippets/setup_bake_target_image.py",
73-
"snippets/temp-override-context.py",
74-
"snippets/usd-export-evaluation-mode.py",
75-
"snippets/version-branch-skeleton.py"
76-
],
77-
"templates": [
78-
"templates/ai-asset-pipeline-template",
79-
"templates/extension-addon-template",
80-
"templates/headless-batch-script-template"
81-
],
82-
"examples": [
83-
"examples/armature-bend",
84-
"examples/attribute-domain-shear",
85-
"examples/bake-normal-high-to-low",
86-
"examples/bmesh-gear",
87-
"examples/car-mirror-symmetry",
88-
"examples/coincident-vert-weld",
89-
"examples/collision-hull-proxy",
90-
"examples/color-attribute-wheel",
91-
"examples/compositor-glare",
92-
"examples/cross-version-property-delete",
93-
"examples/curve-bevel-arc",
94-
"examples/custom-normals-shade",
95-
"examples/damped-track-aim",
96-
"examples/degenerate-bevel-weld",
97-
"examples/depsgraph-export",
98-
"examples/driver-wave",
99-
"examples/eval-mesh-datablock-name",
100-
"examples/exit-pre-sidecar",
101-
"examples/export-preset-axis",
102-
"examples/gltf-export-roundtrip",
103-
"examples/gltf-skin-roundtrip",
104-
"examples/gn-bundle-roundtrip",
105-
"examples/gn-instance-grid",
106-
"examples/gn-modifier-inputs",
107-
"examples/gn-sdf-remesh",
108-
"examples/gn-sim-fountain",
109-
"examples/gn-socket-rename",
110-
"examples/gn-zone-iterate",
111-
"examples/gp-lineart-contour",
112-
"examples/grease-pencil-rosette",
113-
"examples/image-pixels-testcard",
114-
"examples/light-link-studio",
115-
"examples/lightmap-uv-channel",
116-
"examples/lod-decimate-chain",
117-
"examples/mesh-automasking-settings",
118-
"examples/mesh-hygiene-audit",
119-
"examples/modular-kit-snap",
120-
"examples/ngon-triangulate",
121-
"examples/parent-inverse-orrery",
122-
"examples/png-exr-alpha",
123-
"examples/prop-origin-transform",
124-
"examples/shader-node-group",
125-
"examples/shape-key-blend",
126-
"examples/sky-texture-sun-elevation",
127-
"examples/soccer-ball-goldberg",
128-
"examples/socket-attach-points",
129-
"examples/swatch-grid",
130-
"examples/temp-override-join",
131-
"examples/text-version-stamp",
132-
"examples/triangulate-tangents",
133-
"examples/turntable",
134-
"examples/unapplied-scale-gltf",
135-
"examples/usd-export-evaluation-mode",
136-
"examples/uv-layer-grid",
137-
"examples/vertex-color-ao",
138-
"examples/vertex-weight-limit",
139-
"examples/vse-cut-list",
140-
"examples/vse-gamma-cross",
141-
"examples/vse-linear-modifiers",
142-
"examples/wave-displace",
143-
"examples/ray-cast-space",
144-
"examples/lattice-deform",
145-
"examples/solidify-even-thickness",
146-
"examples/boolean-exact-volume"
147-
],
148-
"showcase": [
149-
"showcase/shipping-crate",
150-
"showcase/stone-well",
151-
"showcase/wooden-barrel",
152-
"showcase/campfire",
153-
"showcase/market-stall",
154-
"showcase/street-lantern",
155-
"showcase/treasure-chest",
156-
"showcase/terrain-scatter",
157-
"showcase/fence-kit",
158-
"showcase/watchtower",
159-
"showcase/cart",
160-
"showcase/park-bench",
161-
"showcase/wheelbarrow",
162-
"showcase/anvil",
163-
"showcase/water-trough",
164-
"showcase/hitching-post",
165-
"showcase/grindstone",
166-
"showcase/hand-pump",
167-
"showcase/signpost",
168-
"showcase/chopping-block",
169-
"showcase/wooden-bucket",
170-
"showcase/wall-torch",
171-
"showcase/tavern-stool",
172-
"showcase/iron-cauldron",
173-
"showcase/wooden-ladder",
174-
"showcase/hay-bale",
175-
"showcase/crate-stack",
176-
"showcase/stone-archway",
177-
"showcase/rope-bridge",
178-
"showcase/grain-sacks",
179-
"showcase/brazier",
180-
"showcase/apothecary-shelf",
181-
"showcase/wooden-yoke",
182-
"showcase/butter-churn",
183-
"showcase/bookshelf",
184-
"showcase/book-trolley",
185-
"showcase/basketball-hoop",
186-
"showcase/pine-tree",
187-
"showcase/desk-lamp",
188-
"showcase/quad-drone",
189-
"showcase/soccer-goal",
190-
"showcase/fallen-log",
191-
"showcase/office-chair",
192-
"showcase/hover-bike",
193-
"showcase/weight-rack",
194-
"showcase/boulder-cluster",
195-
"showcase/floor-fan",
196-
"showcase/motor-scooter",
197-
"showcase/bowling-pins",
198-
"showcase/pond-edge",
199-
"showcase/stand-mixer",
200-
"showcase/cargo-loader",
201-
"showcase/archery-target",
202-
"showcase/cactus-garden",
203-
"showcase/espresso-machine",
204-
"showcase/road-bicycle",
205-
"showcase/skate-ramp",
206-
"showcase/palm-tree",
207-
"showcase/wingback-armchair",
208-
"showcase/planet-rover",
209-
"showcase/traffic-cones",
210-
"showcase/mushroom-stump",
211-
"showcase/go-kart",
212-
"showcase/broadleaf-oak",
213-
"showcase/farm-tractor",
214-
"showcase/fern-mossy-rock",
215-
"showcase/sea-stack-arch",
216-
"showcase/moka-pot",
217-
"showcase/cricket-wicket",
218-
"showcase/bamboo-clump",
219-
"showcase/toboggan",
220-
"showcase/sundial",
221-
"showcase/mine-cart",
222-
"showcase/garden-gate",
223-
"showcase/ships-wheel",
224-
"showcase/sawhorse-plank"
22549
]
22650
}

‎.github/workflows/validate.yml‎

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ jobs:
163163
import glob
164164
import json
165165
import os
166+
import re
166167
import sys
167168
168169
errors = []
@@ -174,33 +175,45 @@ jobs:
174175
f"plugin.json version '{manifest.get('version')}' != VERSION '{version}'"
175176
)
176177
177-
# Every manifest path must exist on disk.
178-
for key in ('skills', 'rules', 'snippets', 'templates', 'examples', 'showcase'):
179-
for path in manifest.get(key, []):
180-
if not os.path.exists(path):
181-
errors.append(f'{key}: manifest lists missing path {path}')
182-
183-
# Every content file/dir on disk must be listed in the manifest.
178+
# Cursor's plugin schema (github.com/cursor/plugins schemas/
179+
# plugin.schema.json) sets additionalProperties: false, so any other
180+
# key makes the manifest invalid for Cursor. The old snippets /
181+
# templates / examples / showcase inventory keys did exactly that (#347).
182+
allowed = {'name', 'displayName', 'description', 'version',
183+
'minClientVersions', 'author', 'publisher', 'homepage',
184+
'repository', 'license', 'logo', 'keywords', 'category',
185+
'tags', 'commands', 'agents', 'skills', 'rules', 'hooks',
186+
'variables', 'mcpServers'}
187+
for key in sorted(set(manifest) - allowed):
188+
errors.append(f'plugin.json: {key!r} is not in the Cursor plugin schema')
189+
if not re.fullmatch(r'[a-z0-9]([a-z0-9.-]*[a-z0-9])?', manifest.get('name', '')):
190+
errors.append('plugin.json: name must be kebab-case')
191+
192+
# skills / rules: every listed path exists and every one on disk is listed.
184193
expected = {
185194
'skills': sorted(glob.glob('skills/*/SKILL.md')),
186195
'rules': sorted(glob.glob('rules/*.mdc')),
187-
'snippets': sorted(glob.glob('snippets/*.py')),
188-
'templates': sorted(
189-
d for d in glob.glob('templates/*') if os.path.isdir(d)
190-
),
191-
'examples': sorted(
192-
d for d in glob.glob('examples/*') if os.path.isdir(d)
193-
),
194-
'showcase': sorted(
195-
d for d in glob.glob('showcase/*') if os.path.isdir(d)
196-
),
197196
}
198197
for key, paths in expected.items():
199198
listed = {p.replace('\\', '/') for p in manifest.get(key, [])}
199+
for path in listed:
200+
if not os.path.exists(path):
201+
errors.append(f'{key}: manifest lists missing path {path}')
200202
for path in paths:
201203
if path.replace('\\', '/') not in listed:
202204
errors.append(f'{key}: {path} on disk but not in manifest')
203205
206+
# The marketplace that lets Cursor import this repo as a plugin.
207+
market = json.load(open('.cursor-plugin/marketplace.json'))
208+
for key in sorted(set(market) - {'name', 'owner', 'metadata', 'plugins'}):
209+
errors.append(f'marketplace.json: {key!r} is not in the Cursor marketplace schema')
210+
entries = market.get('plugins') or []
211+
if [e.get('name') for e in entries] != [manifest.get('name')]:
212+
errors.append('marketplace.json: must list exactly this plugin by name')
213+
for e in entries:
214+
for key in sorted(set(e) - {'name', 'source', 'description', 'minClientVersions'}):
215+
errors.append(f'marketplace.json plugin entry: {key!r} not allowed')
216+
204217
# Every showcase piece names a category the gallery build knows.
205218
sys.path.insert(0, 'scripts')
206219
from build_gallery import CATEGORIES

‎AGENTS.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ way, and a one-paragraph rationale. 30 to 80 lines is the right size.
164164
examples, and showcase pieces) match filesystem reality. The counts language in `README.md`
165165
is load-bearing: the job greps for it.
166166
- `validate.yml` also runs a `validate-manifest` job that checks
167-
`.cursor-plugin/plugin.json` against reality: every listed path must exist,
168-
every skill, rule, snippet, template, example, and showcase piece on disk must be listed,
169-
and the manifest `version` must equal `VERSION`. The release pipeline owns
167+
`.cursor-plugin/plugin.json` against Cursor's plugin schema (no keys outside
168+
it; Cursor rejects unknown ones) and against reality: every listed skill
169+
and rule exists and every one on disk is listed, `marketplace.json` lists
170+
this plugin, and the manifest `version` must equal `VERSION`. The release pipeline owns
170171
the manifest `version` line (see `release.yml` below) — never hand-edit it.
171172
- `blender-smoke.yml` executes every shipped example (check-only, no render)
172173
plus snippet/template smoke tests inside REAL headless Blender, on

‎CLAUDE.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,11 @@ example still carries a `tests/smoke/catalog.json` row and a falsifier.
123123
`examples/gallery.json` is the
124124
gallery's source of truth. When authoring a new gallery example, copy the anatomy of
125125
`examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and
126-
wire all of: gallery.json entry (with an `alt` sentence describing the still; the build fails without one), `.cursor-plugin/plugin.json` examples array (CI-gated),
126+
wire all of: gallery.json entry (with an `alt` sentence describing the still; the build fails without one),
127127
a `tests/smoke/catalog.json` row, hero webp (1280×720) in
128128
`docs/gallery/assets/` + its 640×360 card variant `<name>-hero-640.webp` (`python scripts/make_thumbs.py`; the gallery build exits 5 without it) + preview webp (1200×675), then run `python scripts/build_gallery.py`.
129-
A check-only example wires only the `.cursor-plugin/plugin.json` entry, the
130-
`tests/smoke/catalog.json` row, and its README — it is deliberately absent from
129+
A check-only example wires only the
130+
`tests/smoke/catalog.json` row and its README — it is deliberately absent from
131131
`examples/gallery.json` and `docs/gallery/`.
132132
Renders must conform to the gallery look spec at `docs/VISUAL-STYLE.md`.
133133
Render paths gate framing through the shared helper `examples/gallery_framing.py` —

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ budget conformance, never an API contract.
113113
1. Add `showcase/<kebab-name>/` with a script, a README that includes an
114114
exit-code table, and a falsifier that breaks one pipeline stage so a
115115
**named** budget fails.
116-
2. List the directory in `.cursor-plugin/plugin.json` `"showcase"` and add a
116+
2. Add a
117117
`tests/smoke/catalog.json` row with at least one entry in `falsifiers`
118118
(`{"args": ["--your-flag"], "expect_exit": N}`, matching the README
119119
falsifier table). The runner takes opaque script paths.

‎README.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,13 @@ The content is consumed by AI coding agents reading these files directly from a
6060
git clone https://github.com/TMHSDigital/Blender-Developer-Tools.git
6161
```
6262

63-
- **Cursor** — open your project with this checkout available and copy (or symlink) `rules/*.mdc` into your project's `.cursor/rules/`. The rules apply automatically by glob scope; skills are referenced by name in chat.
63+
- **Cursor** — install it as a local plugin from the slim `plugin-dist` branch, then run **Developer: Reload Window**. **Customize** lists the 16 skills and 9 rules; the rules apply by glob scope and the agent loads a skill when its description matches the task.
64+
65+
```bash
66+
git clone --branch plugin-dist --single-branch https://github.com/TMHSDigital/Blender-Developer-Tools.git ~/.cursor/plugins/local/blender-developer-tools
67+
```
68+
69+
(On Windows the folder is `%USERPROFILE%\.cursor\plugins\local\`. Cursor skips symlinks that point outside that folder, so clone or copy rather than link.) Teams can import the repository as a team marketplace instead; it carries `.cursor-plugin/marketplace.json`. For one project only, copy `skills/*` into the project's `.cursor/skills/` and `rules/*.mdc` into `.cursor/rules/` from a checkout. Copying only the rules gives you no skills.
6470
- **Claude Code** — install as a plugin, then run `/skills` to see all 16 skills plus `blender-rules`:
6571

6672
```text
@@ -83,7 +89,8 @@ blender --background --python examples/bmesh-gear/bmesh_gear.py --
8389
Releases ship often; [CHANGELOG.md](CHANGELOG.md) lists what each one changed.
8490

8591
- **Claude Code plugin** — `/plugin marketplace update blender-developer-tools` refreshes the marketplace and its plugins (from a shell: `claude plugin update blender-developer-tools@blender-developer-tools`). Remove it with `/plugin uninstall blender-developer-tools`, then `/plugin marketplace remove blender-developer-tools`.
86-
- **Checkout** (Cursor rules, the optional always-on Claude rules import, examples) — `git pull` in the clone. Copied `.mdc` files do not update themselves: symlink `rules/*.mdc` into `.cursor/rules/` instead of copying, or re-copy after each pull. To uninstall, delete the copied or linked rules and the `@.../blender-rules.md` line from your `CLAUDE.md`.
92+
- **Cursor local plugin** — `git pull` in `~/.cursor/plugins/local/blender-developer-tools`, then reload the window. Remove that folder to uninstall.
93+
- **Checkout** (per-project Cursor copies, the optional always-on Claude rules import, examples) — `git pull` in the clone. Copied `.mdc` files do not update themselves: symlink `rules/*.mdc` into `.cursor/rules/` instead of copying, or re-copy after each pull. To uninstall, delete the copied or linked rules and the `@.../blender-rules.md` line from your `CLAUDE.md`.
8794

8895
## Supported Blender versions
8996

‎scripts/build_plugin_dist.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
as a self-contained marketplace (its marketplace.json points at "./"):
77
88
skills/ claude/ rules/ snippets/ templates/ LICENSE files
9-
.claude-plugin/plugin.json + marketplace.json, and a short README
9+
.claude-plugin/ and .cursor-plugin/ manifests, and a short README
1010
1111
python scripts/build_plugin_dist.py --out DIR # build (DIR must be absent or empty)
1212
python scripts/build_plugin_dist.py --out DIR --force # replace a previous build in DIR
@@ -27,7 +27,10 @@
2727

2828
ROOT = Path(__file__).resolve().parent.parent
2929
DIRS = ("skills", "claude", "rules", "snippets", "templates")
30-
FILES = ("LICENSE", ".claude-plugin/plugin.json")
30+
# The Cursor manifest ships too, so Cursor users can install the same slim
31+
# tree as a local plugin (~/.cursor/plugins/local/) instead of the full repo.
32+
FILES = ("LICENSE", ".claude-plugin/plugin.json",
33+
".cursor-plugin/plugin.json", ".cursor-plugin/marketplace.json")
3134
MAX_BYTES = 2_000_000
3235
IGNORE = shutil.ignore_patterns("__pycache__", "*.pyc")
3336

@@ -43,6 +46,9 @@
4346
/plugin marketplace add TMHSDigital/Blender-Developer-Tools@plugin-dist
4447
/plugin install blender-developer-tools@blender-developer-tools
4548
```
49+
50+
Cursor: clone this branch into `~/.cursor/plugins/local/blender-developer-tools`
51+
and reload the window (Customize then lists the 16 skills and 9 rules).
4652
"""
4753

4854

@@ -92,6 +98,11 @@ def verify(out: Path, size: int) -> list[str]:
9298
errors.append(f"{d.relative_to(out)} has no SKILL.md")
9399
if not (out / "claude" / "skills" / "blender-rules" / "SKILL.md").is_file():
94100
errors.append("blender-rules skill missing")
101+
cursor = json.loads((out / ".cursor-plugin" / "plugin.json").read_text(encoding="utf-8"))
102+
for key in ("skills", "rules"):
103+
for rel in cursor.get(key, []):
104+
if not (out / rel).is_file():
105+
errors.append(f"Cursor manifest {key} path {rel} missing from the dist")
95106
if size > MAX_BYTES:
96107
errors.append(f"dist is {size} bytes, over the {MAX_BYTES} budget")
97108
return errors

0 commit comments

Comments
 (0)