Initial commit: Skill-Repo mit wordcraft-mcp 0.1.0
Release / release (push) Canceled after 0s

This commit is contained in:
Dominic Gundel committed 2026-10-19 14:59:33 +02:00
commit 41816fbbae
21 files changed
+1003

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
* text=auto eol=lf
*.ps1 text eol=crlf
*.zip binary
*.png binary
*.jpg binary
*.pdf binary
+43
View File
@@ -0,0 +1,43 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Skills pruefen
run: bash scripts/validate.sh
- name: README-Tabelle aktuell?
run: bash scripts/update-readme.sh --check
- name: PowerShell-Skripte nur ASCII?
run: |
if grep -nP '[^\x00-\x7F]' scripts/*.ps1; then
echo "::error::.ps1-Dateien duerfen nur ASCII enthalten (Windows PowerShell 5.1)."
exit 1
fi
- name: ZIPs bauen
run: bash scripts/package.sh
check-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Skills pruefen und verpacken (Windows PowerShell 5.1)
shell: powershell
run: |
.\scripts\validate.ps1
if ($LASTEXITCODE -ne 0) { exit 1 }
.\scripts\update-readme.ps1 -Check
if ($LASTEXITCODE -ne 0) { exit 1 }
.\scripts\package.ps1
if ($LASTEXITCODE -ne 0) { exit 1 }
+64
View File
@@ -0,0 +1,64 @@
name: Release
# Ausloeser: Tag im Format <skill-name>/v<x.y.z>, z. B. wordcraft-mcp/v0.1.0
on:
push:
tags: ['*/v*']
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Tag zerlegen
id: tag
run: |
TAG="$GITHUB_REF_NAME"
SKILL="${TAG%%/v*}"
VERSION="${TAG#*/v}"
if [ ! -d "skills/$SKILL" ]; then
echo "::error::Skill '$SKILL' aus Tag '$TAG' existiert nicht unter skills/."
exit 1
fi
echo "skill=$SKILL" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Version gegen CHANGELOG pruefen
env:
SKILL: ${{ steps.tag.outputs.skill }}
VERSION: ${{ steps.tag.outputs.version }}
run: |
source scripts/lib.sh
CL="$(skill_version "skills/$SKILL")"
if [ "$CL" != "$VERSION" ]; then
echo "::error::Tag-Version $VERSION passt nicht zur CHANGELOG-Version '$CL' von $SKILL."
exit 1
fi
- name: Verpacken
run: bash scripts/package.sh "${{ steps.tag.outputs.skill }}"
- name: Release-Text aus CHANGELOG
env:
SKILL: ${{ steps.tag.outputs.skill }}
VERSION: ${{ steps.tag.outputs.version }}
run: |
awk -v v="$VERSION" '
index($0, "## " v " ") == 1 { on = 1; next }
/^## / && on { exit }
on { print }' "skills/$SKILL/CHANGELOG.md" > notes.md
cat notes.md
- name: Release anlegen
env:
GH_TOKEN: ${{ github.token }}
SKILL: ${{ steps.tag.outputs.skill }}
VERSION: ${{ steps.tag.outputs.version }}
run: |
gh release create "$GITHUB_REF_NAME" "dist/$SKILL-$VERSION.zip" \
--title "$SKILL $VERSION" \
--notes-file notes.md
+4
View File
@@ -0,0 +1,4 @@
dist/
*.zip
.DS_Store
Thumbs.db
+61
View File
@@ -0,0 +1,61 @@
# Mitarbeiten
## Aufbau eines Skills
```
skills/<name>/
├── SKILL.md Pflicht – Anweisungen für Claude
├── CHANGELOG.md Pflicht – Versionen (wird nicht ins ZIP gepackt)
├── scripts/ optional – Hilfsskripte, die Claude ausführen kann
├── references/ optional – Nachschlagematerial, das Claude bei Bedarf liest
└── assets/ optional – Vorlagen, Bilder usw.
```
## Regeln
- **Name:** kebab-case, nur `a-z`, `0-9` und einzelne Bindestriche, höchstens 64 Zeichen, ohne „claude“ oder „anthropic“. Der Ordnername ist der Skill-Name.
- **Frontmatter:** `SKILL.md` beginnt mit
```yaml
---
name: <name>
description: "Wann dieser Skill verwendet werden soll – ein Satz."
---
```
`name` muss dem Ordnernamen entsprechen. `description` ist Pflicht, höchstens 1024 Zeichen, ohne spitze Klammern. Sie entscheidet, wann Claude den Skill benutzt – also beschreiben, *wann* er gebraucht wird, nicht nur *was* er ist. Enthält sie „: “, in Anführungszeichen setzen.
- **Kodierung:** UTF-8 ohne BOM, Zeilenenden LF (regelt `.gitattributes`).
- **CHANGELOG.md:** Einträge als `## x.y.z (YYYY-MM-DD)`, neuester oben. Der oberste Eintrag ist die aktuelle Version.
## Ablauf
1. Skill anlegen:
`scripts/new-skill.sh <name>` bzw. `scripts\new-skill.ps1 -Name <name>`
2. `SKILL.md` ausfüllen.
3. Prüfen:
`scripts/validate.sh <name>` bzw. `scripts\validate.ps1 -Name <name>`
4. Version im `CHANGELOG.md` des Skills erhöhen und Änderungen beschreiben.
5. Skill-Tabelle in der README aktualisieren:
`scripts/update-readme.sh` bzw. `scripts\update-readme.ps1`
6. Committen.
7. Tag setzen und pushen – das Release entsteht automatisch:
```sh
git tag <name>/v<version>
git push origin main <name>/v<version>
```
Die GitHub Action prüft, dass die Tag-Version zur obersten CHANGELOG-Version passt, baut das ZIP und hängt es an ein Release mit dem CHANGELOG-Abschnitt als Text.
## Versionen
- **Patch** (0.1.0 → 0.1.1): Korrekturen, Tippfehler, präzisere Formulierungen
- **Minor** (0.1.0 → 0.2.0): neue Abschnitte, neue bekannte Fallen, neue Beispiele
- **Major** (0.x → 1.0.0): geänderte Voraussetzungen oder Arbeitsweise, die bestehende Einrichtungen betreffen
## Hinweise zu den Skripten
- Für jedes Skript gibt es eine Bash- (`.sh`) und eine PowerShell-Variante (`.ps1`). Die Bash-Skripte brauchen `zip` und `unzip`, die PowerShell-Skripte laufen ohne Zusatztools unter Windows PowerShell 5.1 und PowerShell 7.
- `.ps1`-Dateien enthalten nur ASCII-Zeichen. Windows PowerShell 5.1 liest UTF-8 ohne BOM als ANSI; Umlaute oder Gedankenstriche können ein Skript dort unbrauchbar machen.
- `package.ps1` baut ZIPs mit `System.IO.Compression` statt `Compress-Archive`, weil `Compress-Archive` unter 5.1 Pfade mit `\` schreibt.
- Unter Windows bei gesperrter Ausführungsrichtlinie: `powershell -ExecutionPolicy Bypass -File scripts\<skript>.ps1`
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dominic Gundel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+67
View File
@@ -0,0 +1,67 @@
# claude-skills
Sammlung von Skills für Claude. Ein Skill ist ein Ordner mit einer `SKILL.md`, die Claude beibringt, eine bestimmte Art von Aufgabe zuverlässig zu erledigen.
## Skills
<!-- skills:start -->
| Skill | Beschreibung | Version |
|---|---|---|
| [wordcraft-mcp](skills/wordcraft-mcp/) | Word-Dokumente (.docx, .odt, .md, PDF) über den WordCraft-MCP-Server erstellen, bearbeiten und prüfen, inkl. Nachverfolgung – mit erprobten Fallen aus WordCraft 0.8.0; statt wordcraft-dokumente verwenden. | 0.1.0 |
<!-- skills:end -->
## Installation
1. Das ZIP des gewünschten Skills unter **Releases** herunterladen – oder selbst bauen (siehe unten).
2. In Claude die Skills öffnen, **„+“ → „Create skill“ → „Upload a skill“** wählen und das ZIP hochladen.
Selbst bauen:
```sh
# Linux / macOS / Git Bash
scripts/package.sh # alle Skills
scripts/package.sh wordcraft-mcp
```
```powershell
# Windows PowerShell
powershell -ExecutionPolicy Bypass -File scripts\package.ps1
powershell -ExecutionPolicy Bypass -File scripts\package.ps1 -Name wordcraft-mcp
```
Die ZIPs landen in `dist/` als `<name>-<version>.zip`.
## wordcraft-mcp
Erstellt, bearbeitet und prüft Word-Dokumente über den MCP-Server von WordCraft – inklusive Nachverfolgung von Änderungen und der Fallen, die beim Praxistest mit WordCraft 0.8.0 aufgefallen sind.
Voraussetzungen:
- WordCraft mit `wordcraft-cli` ist installiert.
- Der MCP-Server ist in der `claude_desktop_config.json` eingetragen, auf Ordner beschränkt:
```json
{
"mcpServers": {
"wordcraft": {
"command": "C:\\Pfad\\zu\\WordCraft\\wordcraft-cli.exe",
"args": [
"mcp",
"--author", "Claude",
"--automation-read-root", "C:\\Pfad\\zu\\Dokumente",
"--automation-write-root", "C:\\Pfad\\zu\\Dokumente\\claude_out"
]
}
}
}
```
- Falls der Plugin-Skill `wordcraft-dokumente` installiert ist: deaktivieren, damit Claude `wordcraft-mcp` verwendet.
## Mitarbeiten
Neue Skills anlegen, prüfen und veröffentlichen: siehe [CONTRIBUTING.md](CONTRIBUTING.md).
## Lizenz
[MIT](LICENSE)
+86
View File
@@ -0,0 +1,86 @@
# Gemeinsame Funktionen fuer die Skill-Skripte. Wird per Dot-Sourcing eingebunden.
# Laeuft unter Windows PowerShell 5.1 und PowerShell 7.
$ErrorActionPreference = 'Stop'
$Root = Split-Path -Parent $PSScriptRoot
$SkillsDir = Join-Path $Root 'skills'
$DistDir = Join-Path $Root 'dist'
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
# Datei als UTF-8 lesen, Zeilenenden auf LF vereinheitlichen.
function Read-Text([string]$Path) {
return ([IO.File]::ReadAllText($Path, [Text.Encoding]::UTF8) -replace "`r", '')
}
# Datei als UTF-8 ohne BOM mit LF schreiben (Set-Content wuerde in 5.1 ein BOM schreiben).
function Write-Text([string]$Path, [string]$Text) {
[IO.File]::WriteAllText($Path, $Text, $Utf8NoBom)
}
function Test-Bom([string]$Path) {
$b = [IO.File]::ReadAllBytes($Path)
return ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF)
}
# Alle Skill-Namen (Ordner unter skills/), ordinal sortiert wie "LC_ALL=C sort".
function Get-SkillNames {
if (-not (Test-Path $SkillsDir)) { return @() }
$names = [string[]]@(Get-ChildItem -Path $SkillsDir -Directory | ForEach-Object { $_.Name })
[Array]::Sort($names, [StringComparer]::Ordinal)
return $names
}
# Gibt pro Verstoss gegen die Namensregeln einen Text zurueck.
function Get-NameErrors([string]$Name) {
$e = @()
if ($Name -cnotmatch '^[a-z0-9]+(-[a-z0-9]+)*$') { $e += "Name '$Name' ist kein kebab-case (nur a-z, 0-9 und einzelne Bindestriche)" }
if ($Name.Length -gt 64) { $e += "Name '$Name' ist laenger als 64 Zeichen" }
if ($Name -match 'claude|anthropic') { $e += "Name '$Name' darf nicht 'claude' oder 'anthropic' enthalten" }
return $e
}
# Frontmatter-Zeilen (ohne die ---) einer SKILL.md oder $null, wenn keins da ist.
function Get-Frontmatter([string]$Path) {
$lines = (Read-Text $Path) -split "`n"
if ($lines.Count -lt 2 -or $lines[0] -ne '---') { return $null }
for ($i = 1; $i -lt $lines.Count; $i++) {
if ($lines[$i] -eq '---') {
if ($i -eq 1) { return , @() }
return , @($lines[1..($i - 1)])
}
}
return $null
}
# Rohwert eines Frontmatter-Schluessels (getrimmt, Anfuehrungszeichen noch enthalten) oder $null.
function Get-FmRaw($Lines, [string]$Key) {
foreach ($l in $Lines) {
if ($l -match "^$Key\s*:(.*)$") { return $Matches[1].Trim() }
}
return $null
}
# Wert eines Frontmatter-Schluessels ohne umschliessende Anfuehrungszeichen oder $null.
function Get-FmValue($Lines, [string]$Key) {
$v = Get-FmRaw $Lines $Key
if ($null -eq $v) { return $null }
if ($v -match '^"(.*)"$' -or $v -match "^'(.*)'$") { $v = $Matches[1] }
return $v
}
# Erste Ueberschrift "## ..." aus CHANGELOG.md oder $null.
function Get-ChangelogHeading([string]$Dir) {
$p = Join-Path $Dir 'CHANGELOG.md'
if (-not (Test-Path $p)) { return $null }
foreach ($l in ((Read-Text $p) -split "`n")) {
if ($l.StartsWith('## ')) { return $l }
}
return $null
}
# Aktuelle Version eines Skills (oberster CHANGELOG-Eintrag) oder ''.
function Get-SkillVersion([string]$Dir) {
$h = Get-ChangelogHeading $Dir
if ($h -and $h -match '^## (\d+\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)\s*$') { return $Matches[1] }
return ''
}
Executable
+68
View File
@@ -0,0 +1,68 @@
# Gemeinsame Funktionen für die Skill-Skripte. Wird per "source" eingebunden.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SKILLS_DIR="$ROOT/skills"
DIST_DIR="$ROOT/dist"
NAME_RE='^[a-z0-9]+(-[a-z0-9]+)*$'
# Alle Skill-Namen (Ordner unter skills/), sortiert.
skill_names() {
local d
for d in "$SKILLS_DIR"/*/; do
[[ -d "$d" ]] && basename "$d"
done | LC_ALL=C sort
}
# Gibt pro Verstoß gegen die Namensregeln eine Zeile aus.
name_errors() {
local n="$1"
[[ "$n" =~ $NAME_RE ]] || echo "Name '$n' ist kein kebab-case (nur a-z, 0-9 und einzelne Bindestriche)"
(( ${#n} <= 64 )) || echo "Name '$n' ist länger als 64 Zeichen"
case "$n" in *claude*|*anthropic*) echo "Name '$n' darf nicht 'claude' oder 'anthropic' enthalten" ;; esac
return 0
}
# Frontmatter-Zeilen (ohne die ---) einer SKILL.md; Exit-Code != 0, wenn keins da ist.
frontmatter() {
tr -d '\r' < "$1" | awk '
NR == 1 { if ($0 != "---") exit 2; next }
$0 == "---" { found = 1; exit }
{ print }
END { if (!found) exit 3 }'
}
# Rohwert eines Frontmatter-Schlüssels (getrimmt, Anführungszeichen noch enthalten).
fm_raw() {
local line v
line="$(grep -m1 -E "^$2[[:space:]]*:" <<< "$1" || true)"
[[ -z "$line" ]] && return 1
v="${line#*:}"
v="${v#"${v%%[![:space:]]*}"}"
v="${v%"${v##*[![:space:]]}"}"
printf '%s' "$v"
}
# Wert eines Frontmatter-Schlüssels ohne umschließende Anführungszeichen.
fm_value() {
local v
v="$(fm_raw "$1" "$2")" || return 1
if [[ "$v" =~ ^\"(.*)\"$ ]] || [[ "$v" =~ ^\'(.*)\'$ ]]; then
v="${BASH_REMATCH[1]}"
fi
printf '%s' "$v"
}
# Erste Überschrift "## ..." aus CHANGELOG.md.
changelog_heading() {
[[ -f "$1/CHANGELOG.md" ]] || return 1
tr -d '\r' < "$1/CHANGELOG.md" | grep -m1 '^## ' || true
}
# Aktuelle Version eines Skills (oberster CHANGELOG-Eintrag) oder leer.
skill_version() {
local h
h="$(changelog_heading "$1")" || return 0
if [[ "$h" =~ ^##\ ([0-9]+\.[0-9]+\.[0-9]+)\ \([0-9]{4}-[0-9]{2}-[0-9]{2}\)[[:space:]]*$ ]]; then
printf '%s' "${BASH_REMATCH[1]}"
fi
}
+30
View File
@@ -0,0 +1,30 @@
# Legt einen neuen Skill aus templates\skill an.
# Aufruf: scripts\new-skill.ps1 -Name <skill-name>
param([Parameter(Mandatory = $true)][string]$Name)
. (Join-Path $PSScriptRoot 'lib.ps1')
$errs = @(Get-NameErrors $Name)
if ($errs.Count -gt 0) {
foreach ($e in $errs) { Write-Host $e }
exit 1
}
$dir = Join-Path $SkillsDir $Name
if (Test-Path $dir) {
Write-Host "skills/$Name existiert bereits."
exit 1
}
New-Item -ItemType Directory -Force -Path $SkillsDir | Out-Null
Copy-Item -Path (Join-Path $Root 'templates/skill') -Destination $dir -Recurse
$today = Get-Date -Format 'yyyy-MM-dd'
foreach ($f in @('SKILL.md', 'CHANGELOG.md')) {
$p = Join-Path $dir $f
$t = (Read-Text $p).Replace('SKILL_NAME', $Name).Replace('DATUM', $today)
Write-Text $p $t
}
& (Join-Path $PSScriptRoot 'update-readme.ps1')
Write-Host "Angelegt: skills/$Name"
Write-Host "Weiter: SKILL.md ausfuellen, dann scripts\validate.ps1 -Name $Name"
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Legt einen neuen Skill aus templates/skill an.
# Aufruf: scripts/new-skill.sh <skill-name>
set -euo pipefail
source "$(dirname "$0")/lib.sh"
name="${1:-}"
if [[ -z "$name" ]]; then
echo "Aufruf: scripts/new-skill.sh <skill-name>" >&2
exit 1
fi
errs="$(name_errors "$name")"
if [[ -n "$errs" ]]; then
echo "$errs" >&2
exit 1
fi
dir="$SKILLS_DIR/$name"
if [[ -e "$dir" ]]; then
echo "skills/$name existiert bereits." >&2
exit 1
fi
mkdir -p "$SKILLS_DIR"
cp -R "$ROOT/templates/skill" "$dir"
today="$(date +%Y-%m-%d)"
for f in "$dir/SKILL.md" "$dir/CHANGELOG.md"; do
sed -e "s/SKILL_NAME/$name/g" -e "s/DATUM/$today/g" "$f" > "$f.tmp"
mv "$f.tmp" "$f"
done
bash "$(dirname "$0")/update-readme.sh"
echo "Angelegt: skills/$name"
echo "Weiter: SKILL.md ausfüllen, dann scripts/validate.sh $name"
+47
View File
@@ -0,0 +1,47 @@
# Prueft und verpackt alle Skills (oder nur den angegebenen) als dist\<name>-<version>.zip.
# Im ZIP liegt der Ordner <name>/ auf oberster Ebene, CHANGELOG.md wird nicht mitverpackt.
# Pfade im ZIP immer mit "/" (Compress-Archive in Windows PowerShell 5.1 schreibt "\",
# das bricht den Upload). Deshalb System.IO.Compression direkt.
# Aufruf: scripts\package.ps1 [-Name <skill-name>]
param([string]$Name)
. (Join-Path $PSScriptRoot 'lib.ps1')
& (Join-Path $PSScriptRoot 'validate.ps1') -Name $Name
if ($LASTEXITCODE -ne 0) { exit 1 }
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
if ($Name) { $names = @($Name) } else { $names = @(Get-SkillNames) }
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
$exclude = @('CHANGELOG.md', '.DS_Store', 'Thumbs.db')
foreach ($n in $names) {
$src = (Get-Item (Join-Path $SkillsDir $n)).FullName
$version = Get-SkillVersion $src
$zipPath = Join-Path $DistDir "$n-$version.zip"
if (Test-Path $zipPath) { Remove-Item $zipPath }
$files = @(Get-ChildItem -Path $src -Recurse -File -Force |
Where-Object { $exclude -notcontains $_.Name } |
ForEach-Object { $_.FullName })
[Array]::Sort($files, [StringComparer]::Ordinal)
$zip = [IO.Compression.ZipFile]::Open($zipPath, [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-$version.zip"
$read = [IO.Compression.ZipFile]::OpenRead($zipPath)
try {
foreach ($e in $read.Entries) { Write-Host ('{0,9} {1}' -f $e.Length, $e.FullName) }
}
finally { $read.Dispose() }
}
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Prüft und verpackt alle Skills (oder nur den angegebenen) als dist/<name>-<version>.zip.
# Im ZIP liegt der Ordner <name>/ auf oberster Ebene, CHANGELOG.md wird nicht mitverpackt.
# Aufruf: scripts/package.sh [<skill-name>]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
bash "$(dirname "$0")/validate.sh" "${1:-}"
if [[ $# -gt 0 && -n "$1" ]]; then
names=("$1")
else
mapfile -t names < <(skill_names)
fi
mkdir -p "$DIST_DIR"
for name in "${names[@]}"; do
version="$(skill_version "$SKILLS_DIR/$name")"
zip_path="$DIST_DIR/$name-$version.zip"
rm -f "$zip_path"
tmp="$(mktemp -d)"
mkdir "$tmp/$name"
(cd "$SKILLS_DIR/$name" && tar cf - --exclude=CHANGELOG.md --exclude=.DS_Store --exclude=Thumbs.db .) \
| (cd "$tmp/$name" && tar xf -)
(cd "$tmp" && zip -qrX "$zip_path" "$name")
rm -rf "$tmp"
echo
echo "dist/$name-$version.zip"
unzip -l "$zip_path"
done
+49
View File
@@ -0,0 +1,49 @@
# Erzeugt die Skill-Tabelle in README.md zwischen <!-- skills:start --> und <!-- skills:end -->
# neu. Mit -Check wird nur geprueft, ob die Tabelle aktuell ist (Exit-Code 1, wenn nicht).
# Aufruf: scripts\update-readme.ps1 [-Check]
param([switch]$Check)
. (Join-Path $PSScriptRoot 'lib.ps1')
$readme = Join-Path $Root 'README.md'
$current = Read-Text $readme
$lines = $current -split "`n"
$start = [Array]::IndexOf($lines, '<!-- skills:start -->')
$end = [Array]::IndexOf($lines, '<!-- skills:end -->')
if ($start -lt 0 -or $end -lt $start) {
Write-Error 'README.md enthaelt die Marker <!-- skills:start --> / <!-- skills:end --> nicht.'
exit 1
}
$table = @('| Skill | Beschreibung | Version |', '|---|---|---|')
foreach ($n in (Get-SkillNames)) {
$dir = Join-Path $SkillsDir $n
$desc = ''
$skill = Join-Path $dir 'SKILL.md'
if (Test-Path $skill) {
$fm = Get-Frontmatter $skill
if ($null -ne $fm) {
$d = Get-FmValue $fm 'description'
if ($d) { $desc = $d }
}
}
$desc = $desc.Replace('|', '\|')
$table += ('| [{0}](skills/{0}/) | {1} | {2} |' -f $n, $desc, (Get-SkillVersion $dir))
}
$newLines = @()
if ($start -ge 0) { $newLines += $lines[0..$start] }
$newLines += $table
$newLines += $lines[$end..($lines.Count - 1)]
$new = $newLines -join "`n"
if ($Check) {
if ($new -cne $current) {
Write-Host 'README.md ist nicht aktuell - scripts\update-readme.ps1 ausfuehren und committen.'
exit 1
}
Write-Host 'README.md ist aktuell.'
}
else {
Write-Text $readme $new
Write-Host 'README.md aktualisiert.'
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Erzeugt die Skill-Tabelle in README.md zwischen <!-- skills:start --> und <!-- skills:end -->
# neu. Mit --check wird nur geprüft, ob die Tabelle aktuell ist (Exit-Code 1, wenn nicht).
# Aufruf: scripts/update-readme.sh [--check]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
readme="$ROOT/README.md"
check=0
[[ "${1:-}" == "--check" ]] && check=1
if ! grep -q '<!-- skills:start -->' "$readme" || ! grep -q '<!-- skills:end -->' "$readme"; then
echo "README.md enthält die Marker <!-- skills:start --> / <!-- skills:end --> nicht." >&2
exit 1
fi
table="$(mktemp)"
trap 'rm -f "$table"' EXIT
{
echo "| Skill | Beschreibung | Version |"
echo "|---|---|---|"
while IFS= read -r name; do
[[ -z "$name" ]] && continue
desc=""
if fm="$(frontmatter "$SKILLS_DIR/$name/SKILL.md" 2>/dev/null)"; then
desc="$(fm_value "$fm" description || true)"
fi
desc="${desc//|/\\|}"
printf '| [%s](skills/%s/) | %s | %s |\n' "$name" "$name" "$desc" "$(skill_version "$SKILLS_DIR/$name")"
done < <(skill_names)
} > "$table"
new="$(tr -d '\r' < "$readme" | awk -v t="$table" '
/<!-- skills:start -->/ { print; while ((getline l < t) > 0) print l; skip = 1; next }
/<!-- skills:end -->/ { skip = 0 }
!skip { print }')"
if [[ $check -eq 1 ]]; then
if [[ "$new" != "$(tr -d '\r' < "$readme")" ]]; then
echo "README.md ist nicht aktuell – scripts/update-readme.sh ausführen und committen." >&2
exit 1
fi
echo "README.md ist aktuell."
else
printf '%s\n' "$new" > "$readme"
echo "README.md aktualisiert."
fi
+77
View File
@@ -0,0 +1,77 @@
# Prueft alle Skills (oder nur den angegebenen) auf die Regeln aus CONTRIBUTING.md.
# Aufruf: scripts\validate.ps1 [-Name <skill-name>]
param([string]$Name)
. (Join-Path $PSScriptRoot 'lib.ps1')
function Test-Skill([string]$N) {
$dir = Join-Path $SkillsDir $N
if (-not (Test-Path $dir -PathType Container)) { return @("Ordner skills/$N existiert nicht") }
$errs = @(Get-NameErrors $N)
$skill = Join-Path $dir 'SKILL.md'
if (-not (Test-Path $skill)) {
$errs += 'SKILL.md fehlt'
}
elseif (Test-Bom $skill) {
$errs += 'SKILL.md beginnt mit einem BOM - als UTF-8 ohne BOM speichern'
}
else {
$fm = Get-Frontmatter $skill
if ($null -eq $fm) {
$errs += "SKILL.md hat kein Frontmatter (erste Zeile '---', danach schliessendes '---')"
}
else {
$fmName = Get-FmValue $fm 'name'
if (-not $fmName) { $errs += "Frontmatter: 'name' fehlt" }
elseif ($fmName -cne $N) { $errs += "Frontmatter: name '$fmName' passt nicht zum Ordnernamen '$N'" }
$desc = Get-FmValue $fm 'description'
if (-not $desc) {
$errs += "Frontmatter: 'description' fehlt oder ist leer"
}
else {
if ($desc.Length -gt 1024) { $errs += "Frontmatter: description ist laenger als 1024 Zeichen ($($desc.Length))" }
if ($desc -match '[<>]') { $errs += 'Frontmatter: description darf keine spitzen Klammern < > enthalten' }
$raw = Get-FmRaw $fm 'description'
if (-not ($raw.StartsWith('"') -or $raw.StartsWith("'")) -and $raw.Contains(': ')) {
$errs += "Frontmatter: description enthaelt ': ' - in Anfuehrungszeichen setzen, sonst ist das YAML ungueltig"
}
}
}
}
if (-not (Test-Path (Join-Path $dir 'CHANGELOG.md'))) {
$errs += 'CHANGELOG.md fehlt'
}
else {
$h = Get-ChangelogHeading $dir
if (-not $h) { $errs += "CHANGELOG.md hat keinen Eintrag '## x.y.z (YYYY-MM-DD)'" }
elseif (-not (Get-SkillVersion $dir)) { $errs += "CHANGELOG.md: oberster Eintrag '$h' hat nicht das Format '## x.y.z (YYYY-MM-DD)'" }
}
return $errs
}
if ($Name) { $names = @($Name) } else { $names = @(Get-SkillNames) }
if ($names.Count -eq 0) {
Write-Host 'Keine Skills unter skills/ gefunden.'
exit 0
}
$failed = $false
foreach ($n in $names) {
$errs = @(Test-Skill $n)
if ($errs.Count -eq 0) {
Write-Host ("OK {0} ({1})" -f $n, (Get-SkillVersion (Join-Path $SkillsDir $n)))
}
else {
Write-Host "FEHLER $n"
foreach ($e in $errs) { Write-Host " - $e" }
$failed = $true
}
}
if ($failed) { exit 1 }
exit 0
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Prüft alle Skills (oder nur den angegebenen) auf die Regeln aus CONTRIBUTING.md.
# Aufruf: scripts/validate.sh [<skill-name>]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
validate_skill() {
local name="$1" dir="$SKILLS_DIR/$1" fm fm_name desc raw heading
local -a errs=()
if [[ ! -d "$dir" ]]; then
echo " - Ordner skills/$name existiert nicht"
return
fi
mapfile -t errs < <(name_errors "$name")
if [[ ! -f "$dir/SKILL.md" ]]; then
errs+=("SKILL.md fehlt")
else
if [[ "$(head -c3 "$dir/SKILL.md" | od -An -tx1 | tr -d ' \n')" == "efbbbf" ]]; then
errs+=("SKILL.md beginnt mit einem BOM – als UTF-8 ohne BOM speichern")
elif ! fm="$(frontmatter "$dir/SKILL.md")"; then
errs+=("SKILL.md hat kein Frontmatter (erste Zeile '---', danach schließendes '---')")
else
if ! fm_name="$(fm_value "$fm" name)" || [[ -z "$fm_name" ]]; then
errs+=("Frontmatter: 'name' fehlt")
elif [[ "$fm_name" != "$name" ]]; then
errs+=("Frontmatter: name '$fm_name' passt nicht zum Ordnernamen '$name'")
fi
if ! desc="$(fm_value "$fm" description)" || [[ -z "$desc" ]]; then
errs+=("Frontmatter: 'description' fehlt oder ist leer")
else
(( ${#desc} <= 1024 )) || errs+=("Frontmatter: description ist länger als 1024 Zeichen (${#desc})")
[[ "$desc" == *"<"* || "$desc" == *">"* ]] && errs+=("Frontmatter: description darf keine spitzen Klammern < > enthalten")
raw="$(fm_raw "$fm" description)"
if [[ "$raw" != \"* && "$raw" != \'* && "$raw" == *": "* ]]; then
errs+=("Frontmatter: description enthält ': ' – in Anführungszeichen setzen, sonst ist das YAML ungültig")
fi
fi
fi
fi
if [[ ! -f "$dir/CHANGELOG.md" ]]; then
errs+=("CHANGELOG.md fehlt")
else
heading="$(changelog_heading "$dir")"
if [[ -z "$heading" ]]; then
errs+=("CHANGELOG.md hat keinen Eintrag '## x.y.z (YYYY-MM-DD)'")
elif [[ -z "$(skill_version "$dir")" ]]; then
errs+=("CHANGELOG.md: oberster Eintrag '$heading' hat nicht das Format '## x.y.z (YYYY-MM-DD)'")
fi
fi
local e
for e in "${errs[@]+"${errs[@]}"}"; do echo " - $e"; done
}
if [[ $# -gt 0 && -n "$1" ]]; then
names=("$1")
else
mapfile -t names < <(skill_names)
fi
if [[ ${#names[@]} -eq 0 ]]; then
echo "Keine Skills unter skills/ gefunden."
exit 0
fi
failed=0
for name in "${names[@]}"; do
out="$(validate_skill "$name")"
if [[ -z "$out" ]]; then
echo "OK $name ($(skill_version "$SKILLS_DIR/$name"))"
else
echo "FEHLER $name"
echo "$out"
failed=1
fi
done
exit "$failed"
+7
View File
@@ -0,0 +1,7 @@
# Changelog – wordcraft-mcp
## 0.1.0 (2026-10-19)
- Erste Version, getestet mit WordCraft 0.8.0 (headless, `--automation-read-root` / `--automation-write-root`).
- Erprobte Fallen: Kopf-/Fußzeile, Tabellenzellen per `caret.set`, `review.trackChanges` mit `value`, UTF-8-Offsets, `select_text` auf gelöschtem Text, Spaltenbreiten, A4/de-DE bei neuen Dokumenten.
- Abschnitt „Freigaben“ mit empfohlenen Tool-Berechtigungen.
+138
View File
@@ -0,0 +1,138 @@
---
name: wordcraft-mcp
description: Word-Dokumente (.docx, .odt, .md, PDF) über den WordCraft-MCP-Server erstellen, bearbeiten und prüfen, inkl. Nachverfolgung – mit erprobten Fallen aus WordCraft 0.8.0; statt wordcraft-dokumente verwenden.
---
# Dokumente mit WordCraft (MCP)
WordCraft ist ein Open-Source-Textverarbeitungsprogramm (Rust), das Word-Parität anstrebt. Alles ist ein Befehl mit stabiler ID; Ribbon, CLI und MCP rufen dieselben Befehle auf.
## Voraussetzung
Der MCP-Server muss verbunden sein (Tools wie `execute`, `list_commands`, `render_page`). In claude.ai heißen sie `mcp__claude-device__lcl-wordcraft-*` und sind oft **deferred**: zuerst per ToolSearch laden (`select:mcp__claude-device__lcl-wordcraft-list_commands,...`, alle benötigten in einem Aufruf). Fehlen sie auch dann, dem Nutzer den Grund nennen statt auszuweichen.
Einrichtung (Windows, Store-App): Eintrag "wordcraft" in `%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json`:
`wordcraft-cli.exe mcp --author Claude --automation-read-root <Ordner> --automation-write-root <Ordner>\claude_out`
**Achtung:** Die Claude-App überschreibt diese Datei, wenn sie im laufenden Betrieb Einstellungen speichert. Fehlt der Eintrag, neu eintragen und die App sofort neu starten.
Alternativ laufende App steuern (nicht auf Ordner beschränkt): `wordcraft --control 7981` und `wordcraft-cli mcp --connect 127.0.0.1:7981`.
## Freigaben
Der eigentliche Schutz sind `--automation-read-root` und `--automation-write-root`, nicht die Freigabe-Dialoge. Empfohlene Tool-Berechtigungen im Connector:
- Immer erlauben: `list_commands`, `get_text`, `inspect_document`, `render_page`, `open_document`, `select_text`, `new_document`, `parity`, `execute`, `batch`, `type_text`
- Optional nachfragen: `save_document` (nur bedingt wirksam, da `execute` mit `file.saveAs`/`file.exportPdf` ebenfalls speichern kann)
- Nie: `screenshot`, `click`, `key`, `ui_inspect` (nur für `--connect`, dort ohne Ordnergrenzen)
„Immer erlauben“ ist nur sicher, solange die Config die Root-Argumente enthält und kein `--connect` verwendet. Nach jedem Neueintrag prüfen.
Aufrufe sparen: genaue Dateipfade vom Nutzer erfragen statt zu raten, zusammenhängende Schritte immer als `batch`.
## Tools
| Tool | Zweck |
|---|---|
| `list_commands` | alle Befehle, mit `query` filtern |
| `execute` | einen Befehl ausführen: `{command, params}` |
| `batch` | mehrere Befehle in Reihenfolge (stoppt beim ersten Fehler) |
| `new_document` | blank, sample, letter, resume, report |
| `open_document` / `save_document` | docx, doc (nur öffnen), odt, rtf, html, md, tex, txt, json; speichern auch pdf, png |
| `type_text` | an der Schreibmarke tippen (ersetzt Markierung) |
| `select_text` | n-tes Vorkommen eines Textes markieren |
| `get_text` / `inspect_document` | Dokument zurücklesen (Struktur als JSON, inkl. Kopf-/Fußzeilen unter `parts`) |
| `render_page` | Seite als PNG |
| `parity` | Funktionsabdeckung gegenüber Word |
| `screenshot`, `click`, `key`, `ui_inspect` | nur mit `--connect` |
## Arbeitsweise
1. **Befehls-IDs nie raten.** Vor jedem unbekannten Befehl `list_commands` mit `query` aufrufen (z. B. `table.`, `insert.`, `layout.`, `styles.`, `review`, `caret.`). Präfix mit Punkt filtert gezielter.
2. **Formatvorlagen statt Direktformatierung.** `styles.modify` an Normal/Heading1/Heading2/Title; Absätze nur per `para.set {"props":{"style":"Heading1"}}` zuweisen.
3. **Gebündelt arbeiten.** Zusammenhängende Schritte als `batch`.
4. **Prüfen, bevor gespeichert wird.** `inspect_document` für Inhalt/Struktur/Kopf-Fußzeilen, `render_page` für Layout.
5. **Speichern** mit `save_document` in den Schreibordner; PDF per Endung `.pdf`.
6. **Rundreise prüfen:** gespeicherte Datei neu öffnen und `inspect_document`/`render_page` bzw. `review.changes` erneut aufrufen.
## Dateien finden
Es gibt **keinen Befehl zum Auflisten von Ordnern**. `open_document` auf einen Pfad:
- `os error 5 (Zugriff verweigert)` → Pfad ist ein existierender Ordner
- `os error 2 (nicht gefunden)` → existiert nicht
Damit lassen sich Ordner bestätigen, aber keine Dateinamen ermitteln. Bei Unklarheit den Nutzer nach dem genauen Dateinamen fragen statt viele Namen zu raten.
## Neues Dokument – Grundeinrichtung (DE)
`new_document blank` startet in **US-Letter** und englischer Sprache. Immer zuerst:
```json
[{"command":"layout.size","params":{"name":"A4"}},
{"command":"layout.margins","params":{"top":70.87,"bottom":56.69,"left":70.87,"right":56.69}},
{"command":"review.language","params":{"lang":"de-DE","default":true}},
{"command":"styles.modify","params":{"style":"Normal","chr":{"font":"Arial","size":10.5,"lang":"de-DE"}}}]
```
Ohne de-DE zeigt `render_page` deutsche Wörter mit roten Wellenlinien.
Text aufbauen: `para.set {style}` → `text.insert` → `text.newParagraph`, Stil für jeden Absatz explizit setzen. Aufzählung: `para.bullets {"kind":"bullet"}` vor dem ersten Punkt, `para.bullets {"off":true}` danach (hinterlässt `numbering num 0` am nächsten Absatz – harmlos).
## Kopf- und Fußzeile, Seitenzahl
- Nach `insert.header` / `insert.footer` **sofort** `insert.closeHeader`, sonst landet folgender Text in der Kopf-/Fußzeile.
- `insert.footer {"text": ...}` setzt die Schreibmarke **an den Anfang** des Textes – nachfolgende Einfügungen landen davor. `preset: "blank"` legt einen Platzhalter „[Type here]“ an. Deshalb **ohne Parameter** öffnen und dann tippen:
```json
[{"command":"insert.footer"},
{"command":"text.insert","params":{"text":"Seite "}},
{"command":"insert.pageField"},
{"command":"text.insert","params":{"text":" von "}},
{"command":"insert.field","params":{"instr":"NUMPAGES","result":"1"}},
{"command":"para.set","params":{"props":{"align":"center"}}},
{"command":"insert.closeHeader"}]
```
- Für reinen Text in der Kopfzeile ist `insert.header {"text": ...}` + `insert.closeHeader` in Ordnung.
- Kontrolle: `inspect_document` → `parts[]` mit `kind: header/footer`. Fehler korrigieren mit `insert.removeFooter` und neu aufbauen.
## Tabellen
- `insert.table {"rows":n,"cols":n,"style":"TableGrid"}` liefert `{"table":[index]}` = Blockindex.
- Zellen **nicht per Tab** füllen, sondern anspringen: `caret.set {"pos":{"path":[tabelle, zeile, spalte, 0],"off":0,"story":"body"}}` dann `text.insert`.
- Kopfzeile: Caret in Zeile 0 → `table.properties {"headerRow":true}`, `table.selectRow`, `table.shading {"color":"D9E2F3"}`, `format.set {"props":{"bold":true}}`.
- `table.columnWidth` ändert **nur die Spalte der Schreibmarke**; die Tabelle wird dadurch schmaler/breiter. Alle Spalten so setzen, dass die Summe = Satzspiegel (pageW − marginLeft − marginRight; A4 mit 2,5/2 cm ≈ 467,7 pt).
## Positionen
- `off` in Positionen zählt **UTF-8-Bytes**, nicht Zeichen (ä/ö/ü/ß = 2, ▬ oder – = 3). Für Textstellen mit Umlauten lieber `select_text` als selbst berechnete Offsets.
- Kommentaranker und andere Objekte belegen Positionen im Absatz; nach `review.newComment` Offsets neu ermitteln.
## Überarbeitung fremder Dokumente (Nachverfolgung)
1. Original nur öffnen, nie darauf speichern; erste Speicherung sofort mit neuem Namen in den Schreibordner.
2. `file.setAuthor {"name":"Claude"}` und `review.trackChanges {"value": true}` – **immer mit value**; ohne schaltet der Befehl um und kann die Nachverfolgung ausschalten.
3. Ersetzen: `select_text` → `type_text`. Ergebnis ist Einfügung + Löschung (ganzes Wort).
4. **`select_text` findet auch bereits nachverfolgt gelöschten Text.** Nach einer Ersetzung nicht erneut nach dem alten Wortlaut suchen; zum Markieren das neue Wort verwenden.
5. Begründungen als Kommentar: Text markieren, `review.newComment {"text": ...}`.
6. Prüfen: `review.changes` (Liste mit author, kind, text) und `review.comments` – auch nach dem Neuöffnen der gespeicherten Datei.
## Eigenschaften (Kurzreferenz)
Längen in Punkt, Farben als `"#RRGGBB"`, `[r,g,b]` oder `{"Rgb":[r,g,b]}`; bei Tabellenbefehlen `"RRGGBB"` ohne #. Unbekannte Feldnamen werden **still ignoriert**.
- Zeichen (`format.set {"props": …}`): `font`, `size`, `bold`, `italic`, `underline`, `color`, `highlight`, `vertAlign`, `lang`, `link`
- Absatz (`para.set {"props": …}`): `style`, `align`, `indentLeft`, `indentFirst`, `spaceBefore`, `spaceAfter`, `lineSpacing` (`{"rule":"multiple","value":1.15}`), `keepNext`, `keepLines`, `pageBreakBefore`, `outlineLevel`, `numbering`, `tabs`, `borders`
## Renderer-Grenzen (0.8.0)
Bekannte Darstellungsfehler von `render_page`, die **nicht** von eigenen Änderungen kommen:
- mehrzeilige Zellen in dunkel hinterlegten Tabellenköpfen zeigen nur die letzte Zeile
- Seitenzahlfelder in Fußzeilen importierter python-docx-Dokumente bleiben leer
Bei Auffälligkeiten erst das Original rendern und vergleichen, bevor man die eigene Änderung verdächtigt.
## Word-Kompatibilität
- Bei unbekannten Funktionen `parity` abfragen.
- Original nie überschreiben.
- Nutzer bitten, das Ergebnis in Word zu öffnen und gegen das Original zu prüfen (ggf. `review.compare`).
## Grenzen
- Die Ordnerbeschränkung gilt nur headless; mit `--connect` ist die App nicht eingeschränkt.
- Befehle ohne Pfad wirken immer auf das zuletzt geöffnete Dokument. Vor dem Öffnen eines anderen Dokuments speichern.
+5
View File
@@ -0,0 +1,5 @@
# Changelog – SKILL_NAME
## 0.1.0 (DATUM)
- Erste Version.
+31
View File
@@ -0,0 +1,31 @@
---
name: SKILL_NAME
description: "Wann dieser Skill verwendet werden soll – ein Satz."
---
# SKILL_NAME
Ein, zwei Sätze: Wofür ist dieser Skill da, und was kann Claude damit, was es ohne ihn nicht zuverlässig kann?
## Voraussetzung
Was muss eingerichtet oder verbunden sein (MCP-Server, Programme, Zugänge)? Was soll Claude tun, wenn es fehlt?
## Arbeitsweise
Die Schritte in der Reihenfolge, in der Claude sie ausführen soll. Konkrete Befehle und Beispiele statt allgemeiner Ratschläge.
1. …
2. …
## Bekannte Fallen
Was in der Praxis schiefgegangen ist und wie man es vermeidet. Jede Falle mit Symptom und Gegenmittel.
- …
## Grenzen
Was der Skill nicht abdeckt oder wo Claude den Nutzer fragen bzw. warnen soll.
- …