Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "blender-developer-tools",
"owner": {
"name": "TMHSDigital",
"url": "https://github.com/TMHSDigital"
},
"plugins": [
{
"name": "blender-developer-tools",
"source": "./",
"description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development",
"version": "0.131.2"
}
]
}
20 changes: 20 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "blender-developer-tools",
"description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development",
"version": "0.131.2",
"author": {
"name": "TMHSDigital",
"url": "https://github.com/TMHSDigital"
},
"homepage": "https://github.com/TMHSDigital/Blender-Developer-Tools",
"repository": "https://github.com/TMHSDigital/Blender-Developer-Tools",
"license": "CC-BY-NC-ND-4.0",
"keywords": [
"claude-code",
"blender",
"bpy",
"blender-addon",
"blender-python",
"developer-tools"
]
}
9 changes: 6 additions & 3 deletions .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
"description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development",
"version": "0.131.2",
"author": {
"name": "TMHSDigital",
"email": "contact@users.noreply.github.com"
"name": "TMHSDigital"
},
"license": "CC-BY-NC-ND-4.0",
"keywords": [
"cursor-plugin",
"claude-code",
"developer-tools",
"blender"
"blender",
"bpy",
"blender-addon",
"blender-python"
],
"skills": [
"skills/addon-scaffolding/SKILL.md",
Expand Down
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ updates:
schedule:
interval: "daily"
target-branch: "main"
- package-ecosystem: "pip"
directory: "/scripts/site"
schedule:
interval: "weekly"
target-branch: "main"
13 changes: 12 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ jobs:
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
cache-dependency-path: scripts/site/requirements.txt

- run: pip install -r scripts/site/requirements.txt

Expand All @@ -92,9 +94,18 @@ jobs:

- uses: actions/configure-pages@v6

- name: Stage the public site (leave out internal docs and unlinked sheets)
# Everything stays in the repo; only the Pages artifact shrinks. No
# published page links to any of these (tests/check_site_links.py).
run: |
mkdir -p _site
cp -r docs/. _site/
rm -rf _site/gallery/contact-sheets _site/gallery/asset-sheets _site/gallery/DESIGN_NOTES.md
rm -f _site/*.md

- uses: actions/upload-pages-artifact@v5
with:
path: docs
path: _site

- uses: actions/deploy-pages@v5
id: deployment
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ jobs:
flags=re.M,
)
open(path, 'w').write(text)

# Claude Code manifests: same version, rewritten via JSON.
import json
for path in ('.claude-plugin/plugin.json', '.claude-plugin/marketplace.json'):
data = json.load(open(path, encoding='utf-8'))
if 'version' in data:
data['version'] = os.environ['NEW_VERSION']
for entry in data.get('plugins', []):
entry['version'] = os.environ['NEW_VERSION']
with open(path, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(json.dumps(data, indent=2) + '\n')
PYEOF

- name: Commit version bump
Expand Down
90 changes: 90 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ jobs:
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
cache-dependency-path: scripts/site/requirements.txt

- run: pip install -r scripts/site/requirements.txt

Expand Down Expand Up @@ -275,13 +277,71 @@ jobs:
print(f'Manifest verified at v{version}: {counts}')
PYEOF

validate-claude-packaging:
name: Validate Claude Code packaging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Check .claude-plugin manifests match VERSION and the skills on disk
run: |
python3 << 'PYEOF'
import glob
import json
import os
import sys

errors = []
version = open('VERSION').read().strip()
plugin = json.load(open('.claude-plugin/plugin.json', encoding='utf-8'))
market = json.load(open('.claude-plugin/marketplace.json', encoding='utf-8'))

if plugin.get('version') != version:
errors.append(f"claude plugin.json version {plugin.get('version')!r} != VERSION {version!r}")
if plugin.get('name') != 'blender-developer-tools':
errors.append('claude plugin.json name must be blender-developer-tools')
entries = market.get('plugins', [])
if len(entries) != 1 or entries[0].get('name') != plugin.get('name'):
errors.append('marketplace.json must list exactly the one plugin from plugin.json')
elif entries[0].get('version') != version:
errors.append(f"marketplace.json plugin version {entries[0].get('version')!r} != VERSION {version!r}")
elif not os.path.isdir(os.path.join(entries[0].get('source', ''), 'skills')):
errors.append('marketplace.json source must point at a directory containing skills/')

# Claude Code discovers skills/<name>/SKILL.md; each needs name + description.
for path in sorted(glob.glob('skills/*/SKILL.md')):
parts = open(path, encoding='utf-8').read().replace('\r\n', '\n').split('---')
front = parts[1] if len(parts) > 2 else ''
for key in ('name:', 'description:'):
if key not in front:
errors.append(f'{path}: frontmatter missing {key}')

if errors:
for e in errors:
print(f'::error::{e}', file=sys.stderr)
sys.exit(1)
print('Claude Code packaging verified')
PYEOF

- name: Check CLAUDE.md carries no personal plugin routing block
run: |
if grep -nE 'context-mode|MANDATORY routing rules|ctx_(execute|batch_execute|search|fetch_and_index)' CLAUDE.md; then
echo "::error::CLAUDE.md contains a personal plugin routing block (see #288)"
exit 1
fi

- name: Check claude/blender-rules.md is regenerated from rules/*.mdc
run: python3 scripts/build_claude_rules.py --check

validate-counts:
name: Validate content counts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Check content counts match README
env:
GH_TOKEN: ${{ github.token }}
run: |
python3 << 'PYEOF'
import os
Expand Down Expand Up @@ -338,6 +398,36 @@ jobs:
f'README showcase count mismatch (expected "{showcase_needle}" substring)'
)

# Documented snippet length cap (README, CLAUDE.md, CONTRIBUTING.md).
for f in sorted(os.listdir('snippets')):
if f.endswith('.py'):
n = len(open(os.path.join('snippets', f), encoding='utf-8').read().splitlines())
if not 5 <= n <= 75:
errors.append(f'snippets/{f} is {n} lines (documented range is 5 to 75)')

# Secondary docs must not carry stale aggregate counts.
import re
for doc in ('AGENTS.md', 'CONTRIBUTING.md'):
text = open(doc, encoding='utf-8').read()
for kind, actual in (('examples', example_count), ('showcase pieces', showcase_count)):
for m in re.finditer(r'(\d+) ' + kind, text):
if int(m.group(1)) != actual:
errors.append(f'{doc} says "{m.group(0)}" but the repo has {actual}')

# The GitHub About text drifts silently otherwise. Warn only: it is
# repo metadata edited by hand and must not block content PRs.
import subprocess
try:
about = subprocess.run(
['gh', 'repo', 'view', '--json', 'description', '--jq', '.description'],
capture_output=True, text=True, timeout=60, check=True,
).stdout
for needle in (f'{example_count} examples', f'{showcase_count} showcase'):
if needle not in about:
print(f'::warning::GitHub repo description is stale (missing "{needle}"): {about.strip()}')
except (subprocess.SubprocessError, OSError) as exc:
print(f'::warning::could not read the GitHub repo description: {exc}')

if errors:
for e in errors:
print(f'::error::{e}', file=sys.stderr)
Expand Down
65 changes: 1 addition & 64 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ matching its `globs` does. Changing a glob changes when the rule fires.

## Snippets (27)

Small standalone `.py` files at `snippets/<name>.py`, each 5 to 50 lines.
Small standalone `.py` files at `snippets/<name>.py`, each 5 to 75 lines.

v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh load-edit-free, temp_override context, foreach_set vertex bulk write, register_classes_factory, PointerProperty binding, cross-version property delete, and the `action_ensure_channelbag_for_slot` slotted-actions bridge.

Expand Down Expand Up @@ -239,66 +239,3 @@ When adding content to a future version:
3. Update ROADMAP.md candidate pool entries.
4. Use `feat:` for new content, `fix:` for corrections.
5. Push. The release pipeline handles VERSION, tags, CHANGELOG, the `**Version:**` line here, and the `**Current:**` line in ROADMAP.md.

# context-mode — MANDATORY routing rules

You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session.

## BLOCKED commands — do NOT attempt these

### curl / wget — BLOCKED
Any Bash command containing `curl` or `wget` is intercepted and replaced with an error message. Do NOT retry.
Instead use:
- `ctx_fetch_and_index(url, source)` to fetch and index web pages
- `ctx_execute(language: "javascript", code: "const r = await fetch(...)")` to run HTTP calls in sandbox

### Inline HTTP — BLOCKED
Any Bash command containing `fetch('http`, `requests.get(`, `requests.post(`, `http.get(`, or `http.request(` is intercepted and replaced with an error message. Do NOT retry with Bash.
Instead use:
- `ctx_execute(language, code)` to run HTTP calls in sandbox — only stdout enters context

### WebFetch — BLOCKED
WebFetch calls are denied entirely. The URL is extracted and you are told to use `ctx_fetch_and_index` instead.
Instead use:
- `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` to query the indexed content

## REDIRECTED tools — use sandbox equivalents

### Bash (>20 lines output)
Bash is ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`, and other short-output commands.
For everything else, use:
- `ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
- `ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context

### Read (for analysis)
If you are reading a file to **Edit** it → Read is correct (Edit needs content in context).
If you are reading to **analyze, explore, or summarize** → use `ctx_execute_file(path, language, code)` instead. Only your printed summary enters context. The raw file content stays in the sandbox.

### Grep (large results)
Grep results can flood context. Use `ctx_execute(language: "shell", code: "grep ...")` to run searches in sandbox. Only your printed summary enters context.

## Tool selection hierarchy

1. **GATHER**: `ctx_batch_execute(commands, queries)` — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls.
2. **FOLLOW-UP**: `ctx_search(queries: ["q1", "q2", ...])` — Query indexed content. Pass ALL questions as array in ONE call.
3. **PROCESSING**: `ctx_execute(language, code)` | `ctx_execute_file(path, language, code)` — Sandbox execution. Only stdout enters context.
4. **WEB**: `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` — Fetch, chunk, index, query. Raw HTML never enters context.
5. **INDEX**: `ctx_index(content, source)` — Store content in FTS5 knowledge base for later search.

## Subagent routing

When spawning subagents (Agent/Task tool), the routing block is automatically injected into their prompt. Bash-type subagents are upgraded to general-purpose so they have access to MCP tools. You do NOT need to manually instruct subagents about context-mode.

## Output constraints

- Keep responses under 500 words.
- Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description.
- When indexing content, use descriptive source labels so others can `ctx_search(source: "label")` later.

## ctx commands

| Command | Action |
|---------|--------|
| `ctx stats` | Call the `ctx_stats` MCP tool and display the full output verbatim |
| `ctx doctor` | Call the `ctx_doctor` MCP tool, run the returned shell command, display as checklist |
| `ctx upgrade` | Call the `ctx_upgrade` MCP tool, run the returned shell command, display as checklist |
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ showcase/

- **`skills/`** - one directory per skill, each containing `SKILL.md` with YAML frontmatter (`name`, `description`, `standards-version`).
- **`rules/`** - Cursor-style rules as `.mdc` files with YAML frontmatter (`description`, `alwaysApply`, `globs`, `standards-version`).
- **`snippets/`** - small standalone `.py` files (5 to 50 lines) demonstrating a single canonical pattern.
- **`snippets/`** - small standalone `.py` files (5 to 75 lines) demonstrating a single canonical pattern.
- **`templates/`** - copy-paste starting points; one directory per template.
- **`showcase/`** - budget-conformance props, sibling of `examples/`. Not API
contracts. Conventions: [`showcase/README.md`](showcase/README.md).
Expand Down Expand Up @@ -97,7 +97,7 @@ showcase/
## Adding a Snippet

1. Add a `.py` file under `snippets/`, e.g. `snippets/depsgraph-evaluated-mesh.py`.
2. Keep it 5 to 50 lines, fully working code, with a header comment naming the snippet and citing the relevant Blender doc URL or research section.
2. Keep it 5 to 75 lines, fully working code, with a header comment naming the snippet and citing the relevant Blender doc URL or research section.
3. Snippets are validated for Python syntax in CI.

## Adding a Template
Expand Down Expand Up @@ -307,7 +307,7 @@ The drift-check workflow enforces these on every push and PR.

## Aggregate Counts

`README.md` declares aggregate counts (e.g. "16 skills, 9 rules, 3 templates, 27 snippets, 59 examples, and 26 showcase pieces"). The `validate-counts` job in `.github/workflows/validate.yml` enforces these substrings against the filesystem on every push and PR. Showcase pieces are counted separately from examples. When you add or remove content, update the README counts in the same commit.
`README.md` declares aggregate counts (e.g. "16 skills, 9 rules, 3 templates, 27 snippets, 64 examples, and 76 showcase pieces"). The `validate-counts` job in `.github/workflows/validate.yml` enforces these substrings against the filesystem on every push and PR. Showcase pieces are counted separately from examples. When you add or remove content, update the README counts in the same commit.

## Pull Request Process

Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ The content is consumed by AI coding agents reading these files directly from a
git clone https://github.com/TMHSDigital/Blender-Developer-Tools.git
```

- **Cursor** — point Cursor at the checkout (or symlink `rules/` into your project). The `.mdc` rules apply automatically by glob scope; skills are referenced by name in chat.
- **Claude Code** — copy `skills/` and `rules/` into your project workspace, or keep this repo as a checkout that Claude Code references directly.
- **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.
- **Claude Code** — install as a plugin, then run `/skills` to see all 16 skills:

```text
/plugin marketplace add TMHSDigital/Blender-Developer-Tools
/plugin install blender-developer-tools@blender-developer-tools
```

Claude Code does not read Cursor `.mdc` rules. For the rules, add one line to your project's `CLAUDE.md` that imports the generated summary from a checkout: `@/path/to/Blender-Developer-Tools/claude/blender-rules.md`. Without the plugin, copy `skills/*` into your project's `.claude/skills/` instead.
- **Get Blender** — download **5.2 LTS** (primary target) or **4.5 LTS** (supported fallback) from [blender.org/download/lts](https://www.blender.org/download/lts/); current stable lives at [blender.org/download](https://www.blender.org/download/). The `blender` command below is that binary — on macOS it is inside the app bundle at `/Applications/Blender.app/Contents/MacOS/Blender`.
- **Run an example** — every example is a self-checking headless script (exit non-zero on failure, no GPU needed for the check):

Expand Down Expand Up @@ -1451,7 +1458,7 @@ the duplicates, then glTF ships 24 tris / 48 positions / 8 unique.
skills/<name>/SKILL.md - 16 skill files, YAML frontmatter, one canonical pattern each
rules/<name>.mdc - 9 rule files, anti-pattern + correction
templates/<name>/ - 3 template directories (extension-addon-template, headless-batch-script-template, ai-asset-pipeline-template)
snippets/<name>.py - 27 standalone Python snippets, 5 to 50 lines each
snippets/<name>.py - 27 standalone Python snippets, 5 to 75 lines each
```

## Using rules in Cursor
Expand All @@ -1462,13 +1469,13 @@ The `.mdc` files in `rules/` apply automatically when Cursor opens a Blender Pyt
- `always-free-bmesh`: flags `bmesh.new()` without paired `bm.free()` in `try`/`finally`
- `target-extensions-platform-format`: flags add-ons missing `blender_manifest.toml`
- `type-annotate-props-and-defend-context`: flags `bpy.props` assignment form and unguarded `context.active_object`
- `prefer-temp-override-over-context-copy`: flags `bpy.context.copy()` passed to operators (deprecated 4.x, removed 5.x)
- `prefer-temp-override-over-context-copy`: flags `bpy.context.copy()` passed to operators (deprecated 3.2, removed 4.0)
- `use-foreach-set-for-bulk-data`: flags Python loops over `mesh.vertices` setting `co`, normals, or other per-element bulk data
- `validate-imported-mesh-scale`: flags glTF/FBX import then mesh work with no `transform_apply` and no unit-scale check
- `no-unapplied-modifiers-on-export`: flags export of objects with live modifiers when the export does not request evaluated geometry
- `use-correct-axis-rna-per-exporter`: flags `export_scene.gltf` calls that pass FBX `axis_forward` / `axis_up`, and `export_scene.fbx` calls that pass glTF `export_yup`

Symlink or clone this repo, then point Cursor at it as a skills/rules source.
Cursor: copy `rules/*.mdc` into `.cursor/rules/`. Claude Code: install the plugin (see Install) and import `claude/blender-rules.md` from your `CLAUDE.md`.

## Using the templates

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ that comparison is ever needed.
## Asset-quality survey worklist (measured 2026-07-24, `gallery_asset_quality` floors)

Full-gallery floor survey after the gate landed (`feat/asset-quality-gate`).
26 of 42 examples pass all floors. The 14 below-floor entries, with
26 of the 42 examples that existed on 2026-07-24 passed all floors (historical; the gallery has grown since). The 14 below-floor entries, with
measured numbers — every one is a **contract-vehicle or display subject**
(testcard TV, VSE monitors, text bars, swatch/display rigs, single honest
primitives), not a game asset meant for reuse, so none is a remodeling
Expand Down
Loading
Loading