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,197 @@
|
||||
# Builds dist\cursor\ from skills\: flat skill copies, mcp snippets, one plugin per skill.
|
||||
# Usage: scripts\build-cursor.ps1 [-Name <skill-name>]
|
||||
param([string]$Name)
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
. (Join-Path $PSScriptRoot 'cursor-deeplink.ps1')
|
||||
|
||||
$CursorDist = Join-Path $DistDir 'cursor'
|
||||
$ServerKeys = @{
|
||||
'wordcraft-mcp' = 'wordcraft'
|
||||
'gridcraft-mcp' = 'gridcraft'
|
||||
}
|
||||
|
||||
function Get-McpServerKey([string]$SkillName) {
|
||||
if ($ServerKeys.ContainsKey($SkillName)) { return $ServerKeys[$SkillName] }
|
||||
# fallback: strip trailing -mcp
|
||||
if ($SkillName -match '^(.+)-mcp$') { return $Matches[1] }
|
||||
return $SkillName
|
||||
}
|
||||
|
||||
function Convert-ClaudeArgsToCursor([object[]]$CliArgs) {
|
||||
$out = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($a in $CliArgs) {
|
||||
$s = [string]$a
|
||||
if ($s -eq '${user_config.author}') { $out.Add('${AUTHOR}'); continue }
|
||||
if ($s -eq '${user_config.read_root}') { $out.Add('${READ_ROOT}'); continue }
|
||||
if ($s -eq '${user_config.write_root}') { $out.Add('${WRITE_ROOT}'); continue }
|
||||
$out.Add($s)
|
||||
}
|
||||
return , $out.ToArray()
|
||||
}
|
||||
|
||||
function Convert-ArgsToUserHomeDefaults([object[]]$CliArgs) {
|
||||
$out = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($a in $CliArgs) {
|
||||
$s = [string]$a
|
||||
if ($s -eq '${user_config.author}' -or $s -eq '${AUTHOR}') { $out.Add('Claude'); continue }
|
||||
if ($s -eq '${user_config.read_root}' -or $s -eq '${READ_ROOT}') { $out.Add('${userHome}/Documents'); continue }
|
||||
if ($s -eq '${user_config.write_root}' -or $s -eq '${WRITE_ROOT}') { $out.Add('${userHome}/Documents/Claude'); continue }
|
||||
$out.Add($s)
|
||||
}
|
||||
return , $out.ToArray()
|
||||
}
|
||||
|
||||
function Build-OneSkill([string]$N) {
|
||||
$dir = Join-Path $SkillsDir $N
|
||||
if (-not (Test-Path $dir -PathType Container)) {
|
||||
Write-Host "skills/$N fehlt."
|
||||
return $false
|
||||
}
|
||||
$ver = Get-SkillVersion $dir
|
||||
if (-not $ver) {
|
||||
Write-Host "CHANGELOG von $N hat keine Version."
|
||||
return $false
|
||||
}
|
||||
|
||||
$skillPath = Join-Path $dir 'SKILL.md'
|
||||
$fm = Get-Frontmatter $skillPath
|
||||
if ($null -eq $fm) {
|
||||
Write-Host "${N}: kein Frontmatter."
|
||||
return $false
|
||||
}
|
||||
$fmName = Get-FmValue $fm 'name'
|
||||
$desc = Get-FmValue $fm 'description'
|
||||
if ($fmName -cne $N) {
|
||||
Write-Host "${N}: Frontmatter name '$fmName' != Ordner."
|
||||
return $false
|
||||
}
|
||||
if (-not $desc -or $desc.Length -gt 1024) {
|
||||
Write-Host "${N}: description fehlt oder > 1024."
|
||||
return $false
|
||||
}
|
||||
|
||||
# Flat skill copy
|
||||
$flatDir = Join-Path (Join-Path $CursorDist 'skills') $N
|
||||
New-Item -ItemType Directory -Force -Path $flatDir | Out-Null
|
||||
Copy-Item -LiteralPath $skillPath -Destination (Join-Path $flatDir 'SKILL.md') -Force
|
||||
|
||||
$extManPath = Join-Path $dir 'extension\manifest.json'
|
||||
$hasExt = Test-Path $extManPath
|
||||
$pluginRoot = Join-Path (Join-Path $CursorDist 'plugins') $N
|
||||
if (Test-Path $pluginRoot) { Remove-Item $pluginRoot -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $pluginRoot '.cursor-plugin') | Out-Null
|
||||
$pluginSkillDir = Join-Path (Join-Path $pluginRoot 'skills') $N
|
||||
New-Item -ItemType Directory -Force -Path $pluginSkillDir | Out-Null
|
||||
Copy-Item -LiteralPath $skillPath -Destination (Join-Path $pluginSkillDir 'SKILL.md') -Force
|
||||
|
||||
$displayDesc = $desc
|
||||
$authorName = 'Dominic Gundel'
|
||||
$pluginVars = $null
|
||||
$mcpForPlugin = $null
|
||||
$mcpStandalone = $null
|
||||
$serverKey = Get-McpServerKey $N
|
||||
|
||||
if ($hasExt) {
|
||||
$man = (Read-Text $extManPath) | ConvertFrom-Json
|
||||
if ($man.version -cne $ver) {
|
||||
Write-Host "${N}: manifest version '$($man.version)' != CHANGELOG '$ver'."
|
||||
return $false
|
||||
}
|
||||
$dn = [string]$man.display_name
|
||||
if ($dn -match ' for Claude$') {
|
||||
$displayDesc = ($dn -replace ' for Claude$', ' for Cursor') + '. ' + $desc
|
||||
}
|
||||
elseif ($man.description) {
|
||||
$displayDesc = [string]$man.description
|
||||
}
|
||||
if ($man.author -and $man.author.name) { $authorName = [string]$man.author.name }
|
||||
|
||||
$cmd = [string]$man.server.mcp_config.command
|
||||
$argsRaw = @($man.server.mcp_config.args)
|
||||
$argsPlugin = Convert-ClaudeArgsToCursor $argsRaw
|
||||
$argsHome = Convert-ArgsToUserHomeDefaults $argsRaw
|
||||
|
||||
$transportPlugin = @{ command = $cmd; args = @($argsPlugin) }
|
||||
$transportHome = @{ command = $cmd; args = @($argsHome) }
|
||||
|
||||
$mcpForPlugin = @{ mcpServers = @{ $serverKey = $transportPlugin } }
|
||||
$mcpStandalone = @{ mcpServers = @{ $serverKey = $transportHome } }
|
||||
|
||||
if ($man.user_config) {
|
||||
$props = @{}
|
||||
$required = @()
|
||||
if ($man.user_config.read_root) {
|
||||
$props['READ_ROOT'] = @{
|
||||
type = 'string'
|
||||
title = 'Lese-Ordner'
|
||||
description = 'Weit fassen (Standard: Dokumente).'
|
||||
default = '${userHome}/Documents'
|
||||
}
|
||||
$required += 'READ_ROOT'
|
||||
}
|
||||
if ($man.user_config.write_root) {
|
||||
$props['WRITE_ROOT'] = @{
|
||||
type = 'string'
|
||||
title = 'Schreib-Ordner'
|
||||
description = 'Eng halten (Standard: Dokumente/Claude).'
|
||||
default = '${userHome}/Documents/Claude'
|
||||
}
|
||||
$required += 'WRITE_ROOT'
|
||||
}
|
||||
if ($man.user_config.author) {
|
||||
$props['AUTHOR'] = @{
|
||||
type = 'string'
|
||||
title = 'Autorname'
|
||||
description = 'Name fuer nachverfolgte Aenderungen.'
|
||||
default = 'Claude'
|
||||
}
|
||||
}
|
||||
if ($props.Count -gt 0) {
|
||||
$pluginVars = @{
|
||||
type = 'object'
|
||||
properties = $props
|
||||
}
|
||||
if ($required.Count -gt 0) { $pluginVars['required'] = $required }
|
||||
}
|
||||
}
|
||||
|
||||
$mcpDir = Join-Path $CursorDist 'mcp'
|
||||
New-Item -ItemType Directory -Force -Path $mcpDir | Out-Null
|
||||
$snippetPath = Join-Path $mcpDir "$N.json"
|
||||
Write-Text $snippetPath (($mcpStandalone | ConvertTo-Json -Depth 20) + "`n")
|
||||
}
|
||||
|
||||
$pluginJson = @{
|
||||
name = $N
|
||||
version = $ver
|
||||
description = $displayDesc
|
||||
author = @{ name = $authorName }
|
||||
license = 'MIT'
|
||||
homepage = 'https://git.gundel.dev/dg/claude-skills'
|
||||
repository = 'https://git.gundel.dev/dg/claude-skills.git'
|
||||
}
|
||||
if ($pluginVars) { $pluginJson['variables'] = $pluginVars }
|
||||
|
||||
Write-Text (Join-Path $pluginRoot '.cursor-plugin\plugin.json') (($pluginJson | ConvertTo-Json -Depth 20) + "`n")
|
||||
if ($mcpForPlugin) {
|
||||
Write-Text (Join-Path $pluginRoot 'mcp.json') (($mcpForPlugin | ConvertTo-Json -Depth 20) + "`n")
|
||||
}
|
||||
|
||||
# Validate JSON round-trip
|
||||
$null = (Read-Text (Join-Path $pluginRoot '.cursor-plugin\plugin.json')) | ConvertFrom-Json
|
||||
if ($mcpForPlugin) {
|
||||
$null = (Read-Text (Join-Path $pluginRoot 'mcp.json')) | ConvertFrom-Json
|
||||
}
|
||||
|
||||
Write-Host "OK cursor plugin $N $ver"
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($Name) { $names = @($Name) } else { $names = @(Get-SkillNames) }
|
||||
New-Item -ItemType Directory -Force -Path $CursorDist | Out-Null
|
||||
$ok = $true
|
||||
foreach ($n in $names) {
|
||||
if (-not (Build-OneSkill $n)) { $ok = $false }
|
||||
}
|
||||
if (-not $ok) { exit 1 }
|
||||
exit 0
|
||||
@@ -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
|
||||
@@ -0,0 +1,38 @@
|
||||
# Builds a Cursor MCP install deeplink from a server name and config object (hashtable/JSON).
|
||||
# Usage: . scripts\cursor-deeplink.ps1; New-CursorDeeplink -Name wordcraft -Config @{ command='...'; args=@(...) }
|
||||
# Or: scripts\cursor-deeplink.ps1 -Name wordcraft -ConfigJson '{...}'
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$ConfigJson
|
||||
)
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
function New-CursorDeeplink([string]$ServerName, $Config) {
|
||||
# Config = server transport object (command/args/...) OR { serverName = transport }.
|
||||
# Deeplink uses name= + base64(JSON of the transport object keyed by name), per Cursor docs.
|
||||
if ($Config -is [string]) {
|
||||
$obj = $Config | ConvertFrom-Json
|
||||
}
|
||||
else {
|
||||
$obj = $Config
|
||||
}
|
||||
# Normalize to { name: transport }
|
||||
$props = @($obj.PSObject.Properties.Name)
|
||||
if ($props.Count -eq 1 -and $props[0] -eq $ServerName) {
|
||||
$payload = $obj
|
||||
}
|
||||
elseif ($obj.command -or $obj.PSObject.Properties['command']) {
|
||||
$payload = @{ $ServerName = $obj }
|
||||
}
|
||||
else {
|
||||
$payload = $obj
|
||||
}
|
||||
$json = ($payload | ConvertTo-Json -Compress -Depth 20)
|
||||
$bytes = $Utf8NoBom.GetBytes($json)
|
||||
$b64 = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
|
||||
return "cursor://anysphere.cursor-deeplink/mcp/install?name=$([Uri]::EscapeDataString($ServerName))&config=$b64"
|
||||
}
|
||||
|
||||
if ($Name -and $ConfigJson) {
|
||||
Write-Output (New-CursorDeeplink -ServerName $Name -Config $ConfigJson)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prints a Cursor MCP install deeplink.
|
||||
# Usage: scripts/cursor-deeplink.sh <server-name> '<transport-json>'
|
||||
# transport-json = {"command":"...","args":[...]}
|
||||
set -euo pipefail
|
||||
name="${1:-}"
|
||||
config="${2:-}"
|
||||
if [[ -z "$name" || -z "$config" ]]; then
|
||||
echo "Aufruf: scripts/cursor-deeplink.sh <server-name> '<transport-json>'" >&2
|
||||
exit 1
|
||||
fi
|
||||
python3 -c '
|
||||
import base64, json, sys, urllib.parse
|
||||
name, raw = sys.argv[1], sys.argv[2]
|
||||
transport = json.loads(raw)
|
||||
payload = {name: transport}
|
||||
b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode("utf-8")).decode("ascii").rstrip("=")
|
||||
print(f"cursor://anysphere.cursor-deeplink/mcp/install?name={urllib.parse.quote(name)}&config={b64}")
|
||||
' "$name" "$config"
|
||||
+37
-1
@@ -1,7 +1,10 @@
|
||||
# Builds ZIP for every skill and .mcpb when skills\<name>\extension\ exists.
|
||||
# Builds ZIP for every skill, .mcpb when extension/ exists, and Cursor plugin ZIPs.
|
||||
# Usage: scripts\package-all.ps1
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
& (Join-Path $PSScriptRoot 'package.ps1')
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
|
||||
@@ -12,3 +15,36 @@ foreach ($n in (Get-SkillNames)) {
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot 'build-cursor.ps1')
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
|
||||
foreach ($n in (Get-SkillNames)) {
|
||||
$pluginDir = Join-Path $DistDir "cursor\plugins\$n"
|
||||
if (-not (Test-Path $pluginDir)) { continue }
|
||||
$ver = Get-SkillVersion (Join-Path $SkillsDir $n)
|
||||
$out = Join-Path $DistDir "$n-cursor-v$ver.zip"
|
||||
if (Test-Path $out) { Remove-Item $out -Force }
|
||||
|
||||
$src = (Get-Item $pluginDir).FullName
|
||||
$files = @(Get-ChildItem -Path $src -Recurse -File -Force | ForEach-Object { $_.FullName })
|
||||
[Array]::Sort($files, [StringComparer]::Ordinal)
|
||||
|
||||
$zip = [IO.Compression.ZipFile]::Open($out, [IO.Compression.ZipArchiveMode]::Create)
|
||||
try {
|
||||
[void]$zip.CreateEntry("$n/")
|
||||
foreach ($f in $files) {
|
||||
$rel = $f.Substring($src.Length).TrimStart('\', '/') -replace '\\', '/'
|
||||
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $f, "$n/$rel")
|
||||
}
|
||||
}
|
||||
finally { $zip.Dispose() }
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "dist/$n-cursor-v$ver.zip"
|
||||
$read = [IO.Compression.ZipFile]::OpenRead($out)
|
||||
try {
|
||||
foreach ($e in $read.Entries) { Write-Host ('{0,9} {1}' -f $e.Length, $e.FullName) }
|
||||
}
|
||||
finally { $read.Dispose() }
|
||||
}
|
||||
+16
-1
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds ZIP for every skill and .mcpb when skills/<name>/extension/ exists.
|
||||
# Builds ZIP for every skill, .mcpb when extension/ exists, and Cursor plugin ZIPs.
|
||||
# Usage: scripts/package-all.sh
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
@@ -12,3 +12,18 @@ while IFS= read -r name; do
|
||||
bash "$(dirname "$0")/package-mcpb.sh" "$name"
|
||||
fi
|
||||
done < <(skill_names)
|
||||
|
||||
bash "$(dirname "$0")/build-cursor.sh"
|
||||
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
plugin="$DIST_DIR/cursor/plugins/$name"
|
||||
[[ -d "$plugin" ]] || continue
|
||||
ver="$(skill_version "$SKILLS_DIR/$name")"
|
||||
out="$DIST_DIR/$name-cursor-v$ver.zip"
|
||||
rm -f "$out"
|
||||
(cd "$DIST_DIR/cursor/plugins" && zip -r "$out" "$name")
|
||||
echo
|
||||
echo "dist/$name-cursor-v$ver.zip"
|
||||
unzip -l "$out"
|
||||
done < <(skill_names)
|
||||
@@ -135,7 +135,38 @@ if (Test-Path (Join-Path (Join-Path $dir 'extension') 'manifest.json')) {
|
||||
}
|
||||
}
|
||||
|
||||
$cursorName = "$Name-cursor-v$ver.zip"
|
||||
$cursorPath = Join-Path $DistDir $cursorName
|
||||
$cursorAsset = $null
|
||||
if (-not (Test-Path $cursorPath)) {
|
||||
& (Join-Path $PSScriptRoot 'build-cursor.ps1') -Name $Name
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$pluginDir = Join-Path $DistDir "cursor\plugins\$Name"
|
||||
if (Test-Path $pluginDir) {
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$src = (Get-Item $pluginDir).FullName
|
||||
$files = @(Get-ChildItem -Path $src -Recurse -File -Force | ForEach-Object { $_.FullName })
|
||||
[Array]::Sort($files, [StringComparer]::Ordinal)
|
||||
$zip = [IO.Compression.ZipFile]::Open($cursorPath, [IO.Compression.ZipArchiveMode]::Create)
|
||||
try {
|
||||
[void]$zip.CreateEntry("$Name/")
|
||||
foreach ($f in $files) {
|
||||
$rel = $f.Substring($src.Length).TrimStart('\', '/') -replace '\\', '/'
|
||||
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $f, "$Name/$rel")
|
||||
}
|
||||
}
|
||||
finally { $zip.Dispose() }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Test-Path $cursorPath) {
|
||||
$cursorUrl = "$RepoApi/releases/$($created.id)/assets?name=$([Uri]::EscapeDataString($cursorName))"
|
||||
$cursorAsset = Invoke-ReleaseApi 'POST' $cursorUrl ([IO.File]::ReadAllBytes($cursorPath)) 'application/octet-stream' | ConvertFrom-Json
|
||||
}
|
||||
|
||||
if ($created.html_url) { Write-Host $created.html_url }
|
||||
if ($asset.browser_download_url) { Write-Host $asset.browser_download_url }
|
||||
if ($mcpbAsset -and $mcpbAsset.browser_download_url) { Write-Host $mcpbAsset.browser_download_url }
|
||||
if ($cursorAsset -and $cursorAsset.browser_download_url) { Write-Host $cursorAsset.browser_download_url }
|
||||
Write-Host "Release $tag angelegt."
|
||||
@@ -121,5 +121,17 @@ if [[ -f "$dir/extension/manifest.json" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
cursor_name="$name-cursor-v$version.zip"
|
||||
cursor_path="$DIST_DIR/$cursor_name"
|
||||
if [[ ! -f "$cursor_path" ]]; then
|
||||
bash "$(dirname "$0")/package-all.sh" >/dev/null 2>&1 || bash "$(dirname "$0")/build-cursor.sh" "$name" || true
|
||||
if [[ -d "$DIST_DIR/cursor/plugins/$name" ]]; then
|
||||
(cd "$DIST_DIR/cursor/plugins" && zip -r "$cursor_path" "$name")
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$cursor_path" ]]; then
|
||||
upload_asset "$cursor_path" "$cursor_name"
|
||||
fi
|
||||
|
||||
[[ -n "$html" ]] && echo "$html"
|
||||
echo "Release $tag angelegt."
|
||||
@@ -3,6 +3,7 @@
|
||||
# Aufruf: scripts\update-index.ps1 [-Check]
|
||||
param([switch]$Check)
|
||||
. (Join-Path $PSScriptRoot 'lib.ps1')
|
||||
. (Join-Path $PSScriptRoot 'cursor-deeplink.ps1')
|
||||
|
||||
$RepoWeb = 'https://git.gundel.dev/dg/claude-skills'
|
||||
$readme = Join-Path $Root 'README.md'
|
||||
@@ -15,7 +16,21 @@ if ($start -lt 0 -or $end -lt $start) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
$table = @('| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |', '|---|---|---|---|---|---|')
|
||||
$ServerKeys = @{
|
||||
'wordcraft-mcp' = 'wordcraft'
|
||||
'gridcraft-mcp' = 'gridcraft'
|
||||
}
|
||||
|
||||
function Get-McpServerKey([string]$SkillName) {
|
||||
if ($ServerKeys.ContainsKey($SkillName)) { return $ServerKeys[$SkillName] }
|
||||
if ($SkillName -match '^(.+)-mcp$') { return $Matches[1] }
|
||||
return $SkillName
|
||||
}
|
||||
|
||||
$table = @(
|
||||
'| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) | Cursor-Plugin | Cursor 1-Klick |',
|
||||
'|---|---|---|---|---|---|---|---|'
|
||||
)
|
||||
foreach ($n in (Get-SkillNames)) {
|
||||
$dir = Join-Path $SkillsDir $n
|
||||
$desc = ''
|
||||
@@ -32,10 +47,28 @@ foreach ($n in (Get-SkillNames)) {
|
||||
$tagEnc = [Uri]::EscapeDataString("$n/v$ver")
|
||||
$zipLink = "[{0}-v{1}.zip]({2}/releases/download/{3}/{0}-v{1}.zip)" -f $n, $ver, $RepoWeb, $tagEnc
|
||||
$mcpb = ''
|
||||
if (Test-Path (Join-Path $dir 'extension\manifest.json')) {
|
||||
$cursorZip = ''
|
||||
$deeplink = ''
|
||||
$extPath = Join-Path $dir 'extension\manifest.json'
|
||||
if (Test-Path $extPath) {
|
||||
$mcpb = "[{0}-v{1}.mcpb]({2}/releases/download/{3}/{0}-v{1}.mcpb)" -f $n, $ver, $RepoWeb, $tagEnc
|
||||
$cursorZip = "[{0}-cursor-v{1}.zip]({2}/releases/download/{3}/{0}-cursor-v{1}.zip)" -f $n, $ver, $RepoWeb, $tagEnc
|
||||
$man = (Read-Text $extPath) | ConvertFrom-Json
|
||||
$serverKey = Get-McpServerKey $n
|
||||
$cmd = [string]$man.server.mcp_config.command
|
||||
$argsList = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($a in @($man.server.mcp_config.args)) {
|
||||
$s = [string]$a
|
||||
if ($s -eq '${user_config.author}') { $argsList.Add('Claude'); continue }
|
||||
if ($s -eq '${user_config.read_root}') { $argsList.Add('${userHome}/Documents'); continue }
|
||||
if ($s -eq '${user_config.write_root}') { $argsList.Add('${userHome}/Documents/Claude'); continue }
|
||||
$argsList.Add($s)
|
||||
}
|
||||
$transport = @{ command = $cmd; args = @($argsList) }
|
||||
$link = New-CursorDeeplink -ServerName $serverKey -Config $transport
|
||||
$deeplink = "[Add to Cursor]($link)"
|
||||
}
|
||||
$table += ('| [{0}](skills/{0}/) | {1} | {2} | [README](skills/{0}/README.md) | {3} | {4} |' -f $n, $desc, $ver, $zipLink, $mcpb)
|
||||
$table += ('| [{0}](skills/{0}/) | {1} | {2} | [README](skills/{0}/README.md) | {3} | {4} | {5} | {6} |' -f $n, $desc, $ver, $zipLink, $mcpb, $cursorZip, $deeplink)
|
||||
}
|
||||
|
||||
$newLines = @()
|
||||
|
||||
+33
-5
@@ -18,8 +18,8 @@ fi
|
||||
table="$(mktemp)"
|
||||
trap 'rm -f "$table"' EXIT
|
||||
{
|
||||
echo "| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |"
|
||||
echo "|---|---|---|---|---|---|"
|
||||
echo '| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) | Cursor-Plugin | Cursor 1-Klick |'
|
||||
echo '|---|---|---|---|---|---|---|---|'
|
||||
while IFS= read -r name; do
|
||||
[[ -z "$name" ]] && continue
|
||||
desc=""
|
||||
@@ -31,11 +31,40 @@ trap 'rm -f "$table"' EXIT
|
||||
tag_enc="$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name/v$ver")"
|
||||
zip_link="[${name}-v${ver}.zip](${REPO_WEB}/releases/download/${tag_enc}/${name}-v${ver}.zip)"
|
||||
mcpb=""
|
||||
cursor_zip=""
|
||||
deeplink=""
|
||||
if [[ -f "$SKILLS_DIR/$name/extension/manifest.json" ]]; then
|
||||
mcpb="[${name}-v${ver}.mcpb](${REPO_WEB}/releases/download/${tag_enc}/${name}-v${ver}.mcpb)"
|
||||
cursor_zip="[${name}-cursor-v${ver}.zip](${REPO_WEB}/releases/download/${tag_enc}/${name}-cursor-v${ver}.zip)"
|
||||
deeplink="$(python3 - "$SKILLS_DIR/$name/extension/manifest.json" "$name" <<'PY'
|
||||
import json, base64, sys, urllib.parse, re
|
||||
man = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
name = sys.argv[2]
|
||||
sk = {"wordcraft-mcp":"wordcraft","gridcraft-mcp":"gridcraft"}.get(name)
|
||||
if not sk:
|
||||
m = re.match(r"^(.+)-mcp$", name)
|
||||
sk = m.group(1) if m else name
|
||||
cmd = man["server"]["mcp_config"]["command"]
|
||||
args = []
|
||||
for a in man["server"]["mcp_config"].get("args") or []:
|
||||
s = str(a)
|
||||
if s == "${user_config.author}":
|
||||
args.append("Claude")
|
||||
elif s == "${user_config.read_root}":
|
||||
args.append("${userHome}/Documents")
|
||||
elif s == "${user_config.write_root}":
|
||||
args.append("${userHome}/Documents/Claude")
|
||||
else:
|
||||
args.append(s)
|
||||
payload = {sk: {"command": cmd, "args": args}}
|
||||
b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).decode().rstrip("=")
|
||||
link = f"cursor://anysphere.cursor-deeplink/mcp/install?name={urllib.parse.quote(sk)}&config={b64}"
|
||||
print(f"[Add to Cursor]({link})")
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
printf '| [%s](skills/%s/) | %s | %s | [README](skills/%s/README.md) | %s | %s |\n' \
|
||||
"$name" "$name" "$desc" "$ver" "$name" "$zip_link" "$mcpb"
|
||||
printf '| [%s](skills/%s/) | %s | %s | [README](skills/%s/README.md) | %s | %s | %s | %s |\n' \
|
||||
"$name" "$name" "$desc" "$ver" "$name" "$zip_link" "$mcpb" "$cursor_zip" "$deeplink"
|
||||
done < <(skill_names)
|
||||
} > "$table"
|
||||
|
||||
@@ -51,7 +80,6 @@ if [[ $check -eq 1 ]]; then
|
||||
fi
|
||||
echo "README.md ist aktuell."
|
||||
else
|
||||
# Explizit UTF-8 schreiben (Locale kann sonst die README verfaelschen).
|
||||
python3 -c 'import sys; open(sys.argv[1],"w",encoding="utf-8",newline="\n").write(sys.stdin.read())' "$readme" <<< "$new"
|
||||
echo "README.md aktualisiert."
|
||||
fi
|
||||
Reference in new issue
Block a user