Release wordcraft-mcp and gridcraft-mcp 0.3.0: client-neutral skills, Cursor plugins.
This commit is contained in:
1 parent
debdf92877
commit
4c7e3ff720
26 files changed
+1008
-236
No files matched your search
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds dist/cursor/ from skills/: flat skill copies, mcp snippets, one plugin per skill.
|
||||
# Usage: scripts/build-cursor.sh [skill-name]
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
name_filter="${1:-}"
|
||||
CURSOR_DIST="$DIST_DIR/cursor"
|
||||
|
||||
python3 - "$ROOT" "$SKILLS_DIR" "$CURSOR_DIST" "$name_filter" <<'PY'
|
||||
import json, os, re, shutil, sys
|
||||
from pathlib import Path
|
||||
|
||||
root, skills_dir, cursor_dist, name_filter = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]), sys.argv[4]
|
||||
SERVER_KEYS = {"wordcraft-mcp": "wordcraft", "gridcraft-mcp": "gridcraft"}
|
||||
|
||||
def skill_names():
|
||||
names = sorted([p.name for p in skills_dir.iterdir() if p.is_dir()], key=lambda s: s)
|
||||
if name_filter:
|
||||
names = [n for n in names if n == name_filter]
|
||||
return names
|
||||
|
||||
def skill_version(d: Path) -> str:
|
||||
p = d / "CHANGELOG.md"
|
||||
if not p.exists():
|
||||
return ""
|
||||
for line in p.read_text(encoding="utf-8").splitlines():
|
||||
m = re.match(r"^## (\d+\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)\s*$", line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
def frontmatter(path: Path):
|
||||
text = path.read_text(encoding="utf-8").replace("\r", "")
|
||||
lines = text.split("\n")
|
||||
if not lines or lines[0] != "---":
|
||||
return None
|
||||
out = []
|
||||
for line in lines[1:]:
|
||||
if line == "---":
|
||||
return out
|
||||
out.append(line)
|
||||
return None
|
||||
|
||||
def fm_value(lines, key):
|
||||
for line in lines:
|
||||
m = re.match(rf"^{re.escape(key)}\s*:(.*)$", line)
|
||||
if m:
|
||||
v = m.group(1).strip()
|
||||
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
|
||||
v = v[1:-1]
|
||||
return v
|
||||
return None
|
||||
|
||||
def server_key(name):
|
||||
if name in SERVER_KEYS:
|
||||
return SERVER_KEYS[name]
|
||||
m = re.match(r"^(.+)-mcp$", name)
|
||||
return m.group(1) if m else name
|
||||
|
||||
def map_args_plugin(args):
|
||||
out = []
|
||||
for a in args:
|
||||
s = str(a)
|
||||
if s == "${user_config.author}":
|
||||
out.append("${AUTHOR}")
|
||||
elif s == "${user_config.read_root}":
|
||||
out.append("${READ_ROOT}")
|
||||
elif s == "${user_config.write_root}":
|
||||
out.append("${WRITE_ROOT}")
|
||||
else:
|
||||
out.append(s)
|
||||
return out
|
||||
|
||||
def map_args_home(args):
|
||||
out = []
|
||||
for a in args:
|
||||
s = str(a)
|
||||
if s in ("${user_config.author}", "${AUTHOR}"):
|
||||
out.append("Claude")
|
||||
elif s in ("${user_config.read_root}", "${READ_ROOT}"):
|
||||
out.append("${userHome}/Documents")
|
||||
elif s in ("${user_config.write_root}", "${WRITE_ROOT}"):
|
||||
out.append("${userHome}/Documents/Claude")
|
||||
else:
|
||||
out.append(s)
|
||||
return out
|
||||
|
||||
ok = True
|
||||
cursor_dist.mkdir(parents=True, exist_ok=True)
|
||||
for name in skill_names():
|
||||
d = skills_dir / name
|
||||
ver = skill_version(d)
|
||||
skill = d / "SKILL.md"
|
||||
fm = frontmatter(skill)
|
||||
if not ver or fm is None:
|
||||
print(f"FEHLER {name}: Version/Frontmatter", file=sys.stderr)
|
||||
ok = False
|
||||
continue
|
||||
if fm_value(fm, "name") != name:
|
||||
print(f"FEHLER {name}: name mismatch", file=sys.stderr)
|
||||
ok = False
|
||||
continue
|
||||
desc = fm_value(fm, "description") or ""
|
||||
if not desc or len(desc) > 1024:
|
||||
print(f"FEHLER {name}: description", file=sys.stderr)
|
||||
ok = False
|
||||
continue
|
||||
|
||||
flat = cursor_dist / "skills" / name
|
||||
flat.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(skill, flat / "SKILL.md")
|
||||
|
||||
plugin = cursor_dist / "plugins" / name
|
||||
if plugin.exists():
|
||||
shutil.rmtree(plugin)
|
||||
(plugin / ".cursor-plugin").mkdir(parents=True)
|
||||
(plugin / "skills" / name).mkdir(parents=True)
|
||||
shutil.copy2(skill, plugin / "skills" / name / "SKILL.md")
|
||||
|
||||
display_desc = desc
|
||||
author = "Dominic Gundel"
|
||||
variables = None
|
||||
man_path = d / "extension" / "manifest.json"
|
||||
sk = server_key(name)
|
||||
|
||||
if man_path.exists():
|
||||
man = json.loads(man_path.read_text(encoding="utf-8"))
|
||||
if man.get("version") != ver:
|
||||
print(f"FEHLER {name}: manifest version != CHANGELOG", file=sys.stderr)
|
||||
ok = False
|
||||
continue
|
||||
dn = man.get("display_name") or ""
|
||||
if dn.endswith(" for Claude"):
|
||||
display_desc = dn.replace(" for Claude", " for Cursor") + ". " + desc
|
||||
elif man.get("description"):
|
||||
display_desc = man["description"]
|
||||
if man.get("author", {}).get("name"):
|
||||
author = man["author"]["name"]
|
||||
cmd = man["server"]["mcp_config"]["command"]
|
||||
args = list(man["server"]["mcp_config"].get("args") or [])
|
||||
mcp_plugin = {"mcpServers": {sk: {"command": cmd, "args": map_args_plugin(args)}}}
|
||||
mcp_home = {"mcpServers": {sk: {"command": cmd, "args": map_args_home(args)}}}
|
||||
(cursor_dist / "mcp").mkdir(parents=True, exist_ok=True)
|
||||
(cursor_dist / "mcp" / f"{name}.json").write_text(
|
||||
json.dumps(mcp_home, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
||||
)
|
||||
(plugin / "mcp.json").write_text(
|
||||
json.dumps(mcp_plugin, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
||||
)
|
||||
uc = man.get("user_config") or {}
|
||||
props = {}
|
||||
required = []
|
||||
if "read_root" in uc:
|
||||
props["READ_ROOT"] = {
|
||||
"type": "string",
|
||||
"title": "Lese-Ordner",
|
||||
"description": "Weit fassen (Standard: Dokumente).",
|
||||
"default": "${userHome}/Documents",
|
||||
}
|
||||
required.append("READ_ROOT")
|
||||
if "write_root" in uc:
|
||||
props["WRITE_ROOT"] = {
|
||||
"type": "string",
|
||||
"title": "Schreib-Ordner",
|
||||
"description": "Eng halten (Standard: Dokumente/Claude).",
|
||||
"default": "${userHome}/Documents/Claude",
|
||||
}
|
||||
required.append("WRITE_ROOT")
|
||||
if "author" in uc:
|
||||
props["AUTHOR"] = {
|
||||
"type": "string",
|
||||
"title": "Autorname",
|
||||
"description": "Name fuer nachverfolgte Aenderungen.",
|
||||
"default": "Claude",
|
||||
}
|
||||
if props:
|
||||
variables = {"type": "object", "properties": props}
|
||||
if required:
|
||||
variables["required"] = required
|
||||
|
||||
plugin_json = {
|
||||
"name": name,
|
||||
"version": ver,
|
||||
"description": display_desc,
|
||||
"author": {"name": author},
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.gundel.dev/dg/claude-skills",
|
||||
"repository": "https://git.gundel.dev/dg/claude-skills.git",
|
||||
}
|
||||
if variables:
|
||||
plugin_json["variables"] = variables
|
||||
(plugin / ".cursor-plugin" / "plugin.json").write_text(
|
||||
json.dumps(plugin_json, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
||||
)
|
||||
print(f"OK cursor plugin {name} {ver}")
|
||||
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
Reference in new issue
Block a user