This commit is contained in:
commit
41816fbbae
21 files changed
+1003
No files matched your search
@@ -0,0 +1,6 @@
|
||||
* text=auto eol=lf
|
||||
*.ps1 text eol=crlf
|
||||
*.zip binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.pdf binary
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
dist/
|
||||
*.zip
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
Executable
+36
@@ -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"
|
||||
@@ -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() }
|
||||
}
|
||||
Executable
+33
@@ -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
|
||||
@@ -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.'
|
||||
}
|
||||
Executable
+47
@@ -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
|
||||
@@ -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
|
||||
Executable
+83
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog – SKILL_NAME
|
||||
|
||||
## 0.1.0 (DATUM)
|
||||
|
||||
- Erste Version.
|
||||
@@ -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.
|
||||
|
||||
- …
|
||||
Reference in new issue
Block a user