commit 41816fbbae56695c9adb0dcd4f858edfdf6fa299 Author: Dominic Gundel Date: Mon Oct 19 14:59:33 2026 +0200 Initial commit: Skill-Repo mit wordcraft-mcp 0.1.0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9c4e318 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +* text=auto eol=lf +*.ps1 text eol=crlf +*.zip binary +*.png binary +*.jpg binary +*.pdf binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8a10a82 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0b56646 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release + +# Ausloeser: Tag im Format /v, 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89eda0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +*.zip +.DS_Store +Thumbs.db diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..55f2a7a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Mitarbeiten + +## Aufbau eines Skills + +``` +skills// +├── 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: + 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 ` bzw. `scripts\new-skill.ps1 -Name ` +2. `SKILL.md` ausfüllen. +3. Prüfen: + `scripts/validate.sh ` bzw. `scripts\validate.ps1 -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 /v + git push origin main /v + ``` + +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\.ps1` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d997f6a --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..87906e4 --- /dev/null +++ b/README.md @@ -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 + + +| 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 | + + +## 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 `-.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) diff --git a/scripts/lib.ps1 b/scripts/lib.ps1 new file mode 100644 index 0000000..b5f4c30 --- /dev/null +++ b/scripts/lib.ps1 @@ -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 '' +} diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100755 index 0000000..78664b7 --- /dev/null +++ b/scripts/lib.sh @@ -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 +} diff --git a/scripts/new-skill.ps1 b/scripts/new-skill.ps1 new file mode 100644 index 0000000..8da1a2a --- /dev/null +++ b/scripts/new-skill.ps1 @@ -0,0 +1,30 @@ +# Legt einen neuen Skill aus templates\skill an. +# Aufruf: scripts\new-skill.ps1 -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" diff --git a/scripts/new-skill.sh b/scripts/new-skill.sh new file mode 100755 index 0000000..3e51394 --- /dev/null +++ b/scripts/new-skill.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Legt einen neuen Skill aus templates/skill an. +# Aufruf: scripts/new-skill.sh +set -euo pipefail +source "$(dirname "$0")/lib.sh" + +name="${1:-}" +if [[ -z "$name" ]]; then + echo "Aufruf: scripts/new-skill.sh " >&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" diff --git a/scripts/package.ps1 b/scripts/package.ps1 new file mode 100644 index 0000000..213567f --- /dev/null +++ b/scripts/package.ps1 @@ -0,0 +1,47 @@ +# Prueft und verpackt alle Skills (oder nur den angegebenen) als dist\-.zip. +# Im ZIP liegt der Ordner / 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 ] +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() } +} diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 0000000..29bd164 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Prüft und verpackt alle Skills (oder nur den angegebenen) als dist/-.zip. +# Im ZIP liegt der Ordner / auf oberster Ebene, CHANGELOG.md wird nicht mitverpackt. +# Aufruf: scripts/package.sh [] +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 diff --git a/scripts/update-readme.ps1 b/scripts/update-readme.ps1 new file mode 100644 index 0000000..f128d58 --- /dev/null +++ b/scripts/update-readme.ps1 @@ -0,0 +1,49 @@ +# Erzeugt die Skill-Tabelle in README.md zwischen und +# 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, '') +$end = [Array]::IndexOf($lines, '') +if ($start -lt 0 -or $end -lt $start) { + Write-Error 'README.md enthaelt die Marker / 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.' +} diff --git a/scripts/update-readme.sh b/scripts/update-readme.sh new file mode 100755 index 0000000..83d406a --- /dev/null +++ b/scripts/update-readme.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Erzeugt die Skill-Tabelle in README.md zwischen und +# 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 '' "$readme" || ! grep -q '' "$readme"; then + echo "README.md enthält die Marker / 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" ' + // { print; while ((getline l < t) > 0) print l; skip = 1; next } + // { 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 diff --git a/scripts/validate.ps1 b/scripts/validate.ps1 new file mode 100644 index 0000000..a798b67 --- /dev/null +++ b/scripts/validate.ps1 @@ -0,0 +1,77 @@ +# Prueft alle Skills (oder nur den angegebenen) auf die Regeln aus CONTRIBUTING.md. +# Aufruf: scripts\validate.ps1 [-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 diff --git a/scripts/validate.sh b/scripts/validate.sh new file mode 100755 index 0000000..8e619d6 --- /dev/null +++ b/scripts/validate.sh @@ -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 [] +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" diff --git a/skills/wordcraft-mcp/CHANGELOG.md b/skills/wordcraft-mcp/CHANGELOG.md new file mode 100644 index 0000000..6e25d32 --- /dev/null +++ b/skills/wordcraft-mcp/CHANGELOG.md @@ -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. diff --git a/skills/wordcraft-mcp/SKILL.md b/skills/wordcraft-mcp/SKILL.md new file mode 100644 index 0000000..95d6d28 --- /dev/null +++ b/skills/wordcraft-mcp/SKILL.md @@ -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 --automation-write-root \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. diff --git a/templates/skill/CHANGELOG.md b/templates/skill/CHANGELOG.md new file mode 100644 index 0000000..f55bcd4 --- /dev/null +++ b/templates/skill/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog – SKILL_NAME + +## 0.1.0 (DATUM) + +- Erste Version. diff --git a/templates/skill/SKILL.md b/templates/skill/SKILL.md new file mode 100644 index 0000000..6afd0aa --- /dev/null +++ b/templates/skill/SKILL.md @@ -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. + +- …