Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a20da2ed2 | ||
|
|
2a02dac74e | ||
|
|
75e99dd0b8 | ||
|
|
dc3e6a0c68 | ||
|
|
28ec699e51 | ||
|
|
c09ea07d3f | ||
|
|
36dcb843b9 | ||
|
|
919cd52bff | ||
|
|
27f40ee18a |
@@ -33,10 +33,25 @@
|
||||
"source": "./plugins/mediawiki",
|
||||
"description": "MediaWiki-Seiten/Templates lesen, rendern und schreiben via wiki.py (Instanzen wiki & devwiki; Zugangsdaten lokal)."
|
||||
},
|
||||
{
|
||||
"name": "redmine",
|
||||
"source": "./plugins/redmine",
|
||||
"description": "Redmine via REST-API: Projekte/Tickets auflisten, anzeigen, anlegen, aktualisieren und kommentieren via redmine.py (API-Key lokal)."
|
||||
},
|
||||
{
|
||||
"name": "grill-me",
|
||||
"source": "./plugins/grill-me",
|
||||
"description": "Löchert dich gnadenlos mit Fragen zu einem Plan, einer Entscheidung oder Idee, bis ein gemeinsames Verständnis steht."
|
||||
},
|
||||
{
|
||||
"name": "einfaches-technisches-deutsch",
|
||||
"source": "./plugins/einfaches-technisches-deutsch",
|
||||
"description": "Antworten in einfachem technischem Deutsch (nach ASD-STE100, ins Deutsche übertragen): Aktiv, kurze Sätze, ein Gedanke pro Satz — Technik bleibt exakt."
|
||||
},
|
||||
{
|
||||
"name": "klare-sprache",
|
||||
"source": "./plugins/klare-sprache",
|
||||
"description": "Texte in klarer, verständlicher Sprache nach ISO 24495-1:2023 (Plain Language): zielgruppenorientiert — finden, verstehen, nutzen."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# lokale Umgebungs-/Secret-Dateien
|
||||
.env
|
||||
@@ -79,9 +79,10 @@ natürlicher klingen. Basiert auf Wikipedias „Signs of AI writing".
|
||||
|
||||
### humhub
|
||||
|
||||
HumHub (humhub.inmedias.it) über die REST-API: Wiki-Seiten und Posts auflisten,
|
||||
HumHub (humhub.inmedias.it) über die REST-API: Wiki-Seiten, Posts und Umfragen auflisten,
|
||||
anzeigen, anlegen und aktualisieren — via mitgelieferter, self-contained
|
||||
`humhub.py` (`wiki-list/get/update/create`, `post-list/create/update`).
|
||||
`humhub.py` (`wiki-list/get/update/create`, `post-list/create/update`,
|
||||
`poll-list/create`).
|
||||
|
||||
- Voraussetzung: `HUMHUB_API_TOKEN` als Umgebungsvariable bzw. dotenv-Datei
|
||||
(kein Token im Plugin). Token nur als berechtigter Benutzer unter
|
||||
@@ -107,6 +108,22 @@ self-contained `wiki.py` (`fetch`/`render`/`search`/`update`). Instanzen `wiki`
|
||||
/plugin install mediawiki@inmedias-claude-plugins
|
||||
```
|
||||
|
||||
### redmine
|
||||
|
||||
Redmine (redmine.inmedias.it) über die REST-API: Projekte und Tickets auflisten,
|
||||
anzeigen, anlegen, aktualisieren und kommentieren — via mitgelieferter,
|
||||
self-contained `redmine.py` (`me`/`projects`/`issues`/`show`/`create`/`update`/
|
||||
`comment`/`meta`). Namen (Projekt/Tracker/Status/Priorität) werden auf IDs
|
||||
aufgelöst.
|
||||
|
||||
- Voraussetzung: `REDMINE_URL` und persönlicher `REDMINE_API_KEY` als
|
||||
Umgebungsvariable bzw. dotenv-Datei (kein Key im Plugin). API-Key unter
|
||||
https://redmine.inmedias.it/my/account.
|
||||
|
||||
```bash
|
||||
/plugin install redmine@inmedias-claude-plugins
|
||||
```
|
||||
|
||||
### grill-me
|
||||
|
||||
Löchert dich gnadenlos mit Fragen zu einem Plan, einer Entscheidung oder Idee —
|
||||
@@ -123,6 +140,37 @@ nach deiner Bestätigung.
|
||||
/plugin install grill-me@inmedias-claude-plugins
|
||||
```
|
||||
|
||||
### einfaches-technisches-deutsch
|
||||
|
||||
Setzt einen festen Schreibstil für Antworten: nach ASD-STE100 (Simplified
|
||||
Technical English), ins Deutsche übertragen. Aktiv, Sätze mit höchstens 20
|
||||
Wörtern, ein Gedanke pro Satz, gleiche Wörter für gleiche Dinge. Technische
|
||||
Angaben (Pfade, Befehle, Zahlen, Preise) bleiben exakt.
|
||||
|
||||
- Kein Setup, keine Zugangsdaten. Auslösen mit „schreib einfacher", „einfaches
|
||||
Deutsch", „STE" o. ä.; gilt dann für die weitere Sitzung.
|
||||
|
||||
```bash
|
||||
/plugin install einfaches-technisches-deutsch@inmedias-claude-plugins
|
||||
```
|
||||
|
||||
### klare-sprache
|
||||
|
||||
Schreibt oder überarbeitet Texte in klarer, verständlicher Sprache nach
|
||||
ISO 24495-1:2023 (Plain Language). Anders als `einfaches-technisches-deutsch`
|
||||
kein starres Regelwerk, sondern zielgruppen- und prozessorientiert: Die Leser
|
||||
sollen leicht **finden**, **verstehen** und **nutzen** können. Vier Prinzipien
|
||||
(relevant, auffindbar, verständlich, nutzbar) mit Checkliste; technische Angaben
|
||||
bleiben exakt.
|
||||
|
||||
- Kein Setup, keine Zugangsdaten. Auslösen mit „klare Sprache", „verständlich
|
||||
schreiben", „ISO 24495" o. ä. Ohne bekannte Zielgruppe fragt der Skill zuerst
|
||||
nach.
|
||||
|
||||
```bash
|
||||
/plugin install klare-sprache@inmedias-claude-plugins
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "einfaches-technisches-deutsch",
|
||||
"version": "0.1.0",
|
||||
"description": "Antworten in einfachem technischem Deutsch schreiben (nach ASD-STE100, ins Deutsche übertragen): Aktiv, kurze Sätze, ein Gedanke pro Satz — Technik bleibt exakt.",
|
||||
"author": {
|
||||
"name": "inmedias.it"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: einfaches-technisches-deutsch
|
||||
description: Antworten in einfachem technischem Deutsch schreiben (nach ASD-STE100 Simplified Technical English, ins Deutsche übertragen). Use when the user asks to write clearly/simply, mentions "einfache Sprache", "einfaches Deutsch", "vereinfachtes technisches Deutsch", "verständlich schreiben", "schreib einfacher", "STE", "Simplified Technical English", or wants replies that stay short and precise.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Einfaches technisches Deutsch
|
||||
|
||||
Dieser Skill setzt einen festen Schreibstil. Der Stil folgt ASD-STE100
|
||||
(Simplified Technical English) und ist ins Deutsche übertragen. Ziel: Antworten
|
||||
bleiben kurz, klar und eindeutig.
|
||||
|
||||
## Wann anwenden
|
||||
|
||||
Wende den Stil an, sobald der Nutzer ihn verlangt. Er gilt dann für alle
|
||||
weiteren Antworten in der Sitzung, bis der Nutzer ihn wieder abschaltet.
|
||||
|
||||
Der Nutzer kann den Stil auch dauerhaft wollen. In dem Fall speichere die Regel
|
||||
im Memory und wende sie in jeder Sitzung an.
|
||||
|
||||
## Die Regeln
|
||||
|
||||
Halte dich an diese Regeln:
|
||||
|
||||
- Ich schreibe im Aktiv.
|
||||
- Ich schreibe Sätze mit höchstens 20 Wörtern.
|
||||
- Ich schreibe einen Gedanken pro Satz.
|
||||
- Ich nutze einfache Zeitformen: Präsens, Präteritum und Futur.
|
||||
- Ich nutze dasselbe Wort für dieselbe Sache.
|
||||
- Ich nutze keine Redewendungen, keinen Slang und keinen unnötigen Jargon.
|
||||
- Ich schreibe Absätze mit höchstens 6 Sätzen.
|
||||
|
||||
## Technische Angaben bleiben exakt
|
||||
|
||||
Der Stil vereinfacht die Sprache, nicht die Technik. Technische Angaben bleiben
|
||||
unverändert. Das gilt für Dateipfade, Funktionsnamen, Spaltennamen, Befehle,
|
||||
Preise und Zahlen.
|
||||
|
||||
Beispiel: Ich schreibe `workers/updatePricing.php` und `4.855 €` vollständig aus.
|
||||
|
||||
## Vorgehen
|
||||
|
||||
1. Bestätige kurz, dass du den Stil ab jetzt nutzt.
|
||||
2. Schreibe die Antwort direkt im Stil. Die Antwort ist zugleich das Beispiel.
|
||||
3. Prüfe vor dem Senden: Aktiv, kurze Sätze, ein Gedanke pro Satz, gleiche
|
||||
Wörter, exakte Technik.
|
||||
4. Will der Nutzer den Stil dauerhaft, lege ein Memory an.
|
||||
|
||||
## Grenzen
|
||||
|
||||
Der Stil gilt für Fließtext. Er gilt nicht für Code, Log-Ausgaben,
|
||||
Fehlermeldungen oder Zitate. Diese gibst du unverändert wieder.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "humhub",
|
||||
"version": "0.1.0",
|
||||
"description": "HumHub via REST-API: Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren (humhub.py).",
|
||||
"version": "0.3.0",
|
||||
"description": "HumHub via REST-API: Wiki-Seiten, Posts, Umfragen und Kommentare auflisten, anzeigen, anlegen und aktualisieren (humhub.py).",
|
||||
"author": {
|
||||
"name": "inmedias.it"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
name: humhub
|
||||
description: HumHub Wiki-Seiten und Posts lesen/erstellen/aktualisieren via REST-API. Use when the user mentions "HumHub", "Space", "Wiki-Seite in HumHub" or "Post/Beitrag".
|
||||
version: 0.1.0
|
||||
description: HumHub Wiki-Seiten, Posts und Umfragen lesen/erstellen/aktualisieren via REST-API. Use when the user mentions "HumHub", "Space", "Wiki-Seite in HumHub", "Post/Beitrag" or "Umfrage/Poll".
|
||||
version: 0.2.0
|
||||
---
|
||||
|
||||
# HumHub (humhub.py)
|
||||
|
||||
Interagiert mit HumHub (https://humhub.inmedias.it) über die REST-API:
|
||||
Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren — über die
|
||||
Wiki-Seiten, Posts und Umfragen auflisten, anzeigen, anlegen und aktualisieren — über die
|
||||
mitgelieferte, self-contained `humhub.py`.
|
||||
|
||||
## Ausführen
|
||||
@@ -59,6 +59,38 @@ werden nicht committet).
|
||||
./humhub.py post-update <id> [datei|-]
|
||||
```
|
||||
|
||||
### Umfragen
|
||||
|
||||
```bash
|
||||
./humhub.py poll-list <container_id> [--limit N]
|
||||
./humhub.py poll-create <container_id> "<frage>" --answers "A|B|C" \
|
||||
[--file beschreibung.md] [--anonymous] [--multiple] [--hide-results]
|
||||
```
|
||||
|
||||
Antwortoptionen mit `|` trennen, mindestens zwei. Der Beschreibungstext kommt
|
||||
wie überall aus einer Datei oder `-`; Markdown ist erlaubt. Ohne `--anonymous`
|
||||
ist sichtbar, wer wie gestimmt hat — bei Anmeldungen mit begrenzten Plätzen ist
|
||||
das der Sinn der Sache.
|
||||
|
||||
Zwei Eigenheiten der Polls-API, die zu `HTTP 500 "Internal error while save a
|
||||
poll!"` führen, wenn man dem Muster der Post-API folgt: die Felder gehören
|
||||
unter den Yii-Formnamen **`Poll`** (nicht `data`), und die Optionen heißen beim
|
||||
Anlegen **`newAnswers`** (beim Lesen `answers`). Beim Lesen ist `limit` Pflicht
|
||||
und nicht Komfort — ohne Begrenzung antwortet die Instanz ab ~25 Umfragen
|
||||
ebenfalls mit 500. Ein fehlgeschlagener POST legt nichts an; vor einem zweiten
|
||||
Versuch trotzdem mit `poll-list` prüfen.
|
||||
|
||||
### Kommentare
|
||||
|
||||
```bash
|
||||
./humhub.py comment-list <content-id|perma-url> # Kommentare eines Inhalts
|
||||
./humhub.py comment-create <content-id|perma-url> [datei|-] # Kommentar schreiben
|
||||
```
|
||||
|
||||
Beide nehmen wie `fetch` eine **Content-ID** oder Perma-URL (nicht die Post-/
|
||||
Wiki-Seiten-ID) und lösen das Zielobjekt selbst auf — funktioniert für Posts
|
||||
und Wiki-Seiten.
|
||||
|
||||
Inhalte (Markdown) aus Datei oder `-` (stdin) übergeben — nicht als langes
|
||||
Shell-Argument (Sonderzeichen). `container_id` akzeptiert eine numerische
|
||||
Container-ID, einen **Space-Slug** oder eine **Space-URL** (`resolve_container`
|
||||
|
||||
@@ -212,6 +212,61 @@ def cmd_post_create(args: argparse.Namespace) -> None:
|
||||
print(f"Topics: {', '.join(saved_topics)}")
|
||||
|
||||
|
||||
def cmd_poll_list(args: argparse.Namespace) -> None:
|
||||
session, _ = get_session()
|
||||
container_id = resolve_container(session, args.container_id)
|
||||
# limit ist Pflicht, nicht Komfort: ohne Begrenzung antwortet die Instanz
|
||||
# bei vielen Umfragen mit HTTP 500 statt mit einer Liste.
|
||||
data = api_get(session, f"/polls/container/{container_id}?limit={args.limit}")
|
||||
results = data.get("results", [])
|
||||
if not results:
|
||||
print("(keine Umfragen)")
|
||||
return
|
||||
for poll in results:
|
||||
antworten = [a.get("answer", "") for a in poll.get("answers", [])]
|
||||
zu = " [geschlossen]" if poll.get("closed") else ""
|
||||
print(f"ID {poll.get('id')}{zu}: {poll.get('question', '')}")
|
||||
print(f" {' | '.join(antworten)}")
|
||||
|
||||
|
||||
def cmd_poll_create(args: argparse.Namespace) -> None:
|
||||
"""Umfrage anlegen.
|
||||
|
||||
Zwei Abweichungen von der Post-API, die beide mit HTTP 500
|
||||
("Internal error while save a poll!") enden, wenn man sie übersieht:
|
||||
|
||||
* Der Controller macht `$poll->load(Yii::$app->request->post())` — Yii
|
||||
erwartet die Felder deshalb unter dem Formnamen **`Poll`**, nicht unter
|
||||
`data` wie bei Posts und Wiki-Seiten.
|
||||
* Die Optionen heißen beim Anlegen **`newAnswers`**; `answers` ist die
|
||||
Leseform und wird beim POST ignoriert.
|
||||
|
||||
Quelle: humhub/polls, controllers/rest/PollsController.php + models/Poll.php.
|
||||
Pflicht sind `question` und mindestens zwei Antworten.
|
||||
"""
|
||||
session, _ = get_session()
|
||||
container_id = resolve_container(session, args.container_id)
|
||||
antworten = [a.strip() for a in args.answers.split("|") if a.strip()]
|
||||
if len(antworten) < 2:
|
||||
sys.exit("Error: mindestens zwei Antwortoptionen nötig "
|
||||
"(--answers 'Ja|Nein').")
|
||||
beschreibung = read_file(args.file) if args.file else ""
|
||||
|
||||
payload = {"Poll": {
|
||||
"question": args.question,
|
||||
"description": beschreibung,
|
||||
"newAnswers": antworten,
|
||||
"anonymous": 1 if args.anonymous else 0,
|
||||
"allow_multiple": 1 if args.multiple else 0,
|
||||
"show_result_after_close": 1 if args.hide_results else 0,
|
||||
"is_random": 0,
|
||||
}}
|
||||
data = api_post(session, f"/polls/container/{container_id}", payload)
|
||||
print(f"Umfrage erstellt (ID {data.get('id')}): {data.get('question')}")
|
||||
print("Optionen: " + " | ".join(a.get("answer", "")
|
||||
for a in data.get("answers", [])))
|
||||
|
||||
|
||||
def cmd_post_update(args: argparse.Namespace) -> None:
|
||||
session, _ = get_session()
|
||||
message = read_file(args.file)
|
||||
@@ -251,6 +306,48 @@ def cmd_space_list(args: argparse.Namespace) -> None:
|
||||
print("(keine Spaces gefunden)")
|
||||
|
||||
|
||||
# ── Comment commands ─────────────────────────────────────────────────────────
|
||||
|
||||
def cmd_comment_list(args: argparse.Namespace) -> None:
|
||||
session, _ = get_session()
|
||||
content_id = parse_content_ref(args.ref)
|
||||
data = api_get(session, f"/comment/content/{content_id}")
|
||||
results = data.get("results", data if isinstance(data, list) else [])
|
||||
if not results:
|
||||
print("(keine Kommentare)")
|
||||
return
|
||||
for c in results:
|
||||
cid = c.get("id")
|
||||
author = (c.get("createdBy") or {}).get("display_name", "?")
|
||||
created = c.get("createdAt", "")
|
||||
msg = (c.get("message") or "").replace("\n", " ")[:100]
|
||||
print(f"ID {cid:>5} [{created}] {author}: {msg}")
|
||||
|
||||
|
||||
def cmd_comment_create(args: argparse.Namespace) -> None:
|
||||
session, _ = get_session()
|
||||
content_id = parse_content_ref(args.ref)
|
||||
message = read_file(args.file)
|
||||
|
||||
envelope = api_get(session, f"/content/{content_id}")
|
||||
meta = envelope.get("metadata", {})
|
||||
model = meta.get("object_model", "")
|
||||
object_id = meta.get("object_id")
|
||||
if not model or not object_id:
|
||||
sys.exit(f"Error: Content {content_id} nicht auflösbar (object_model/object_id fehlen)")
|
||||
|
||||
payload = {
|
||||
"objectModel": model,
|
||||
"objectId": object_id,
|
||||
"Comment": {"message": message},
|
||||
}
|
||||
data = api_post(session, "/comment", payload)
|
||||
cid = data.get("id")
|
||||
short_model = model.split("\\")[-1]
|
||||
print(f"Kommentar erstellt (ID {cid}) auf {short_model} {object_id}")
|
||||
print(f"Perma: https://humhub.inmedias.it/content/perma?id={content_id}")
|
||||
|
||||
|
||||
# ── Fetch (perma-/content-id-agnostisch) ─────────────────────────────────────
|
||||
|
||||
def parse_content_ref(ref: str) -> int:
|
||||
@@ -405,6 +502,17 @@ def main() -> None:
|
||||
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen")
|
||||
p.set_defaults(func=cmd_post_create)
|
||||
|
||||
# comment-list
|
||||
p = sub.add_parser("comment-list", help="Kommentare eines Inhalts auflisten")
|
||||
p.add_argument("ref", help="Content-ID oder Perma-Link (…/content/perma?id=NNNN)")
|
||||
p.set_defaults(func=cmd_comment_list)
|
||||
|
||||
# comment-create
|
||||
p = sub.add_parser("comment-create", help="Kommentar auf einen Inhalt (Post/Wiki-Seite) schreiben")
|
||||
p.add_argument("ref", help="Content-ID oder Perma-Link (…/content/perma?id=NNNN)")
|
||||
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
|
||||
p.set_defaults(func=cmd_comment_create)
|
||||
|
||||
# post-update
|
||||
p = sub.add_parser("post-update", help="Post aktualisieren")
|
||||
p.add_argument("id", type=int, help="Post-ID")
|
||||
@@ -412,6 +520,27 @@ def main() -> None:
|
||||
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen (ersetzt bestehende)")
|
||||
p.set_defaults(func=cmd_post_update)
|
||||
|
||||
# poll-list
|
||||
p = sub.add_parser("poll-list", help="Umfragen eines Containers auflisten")
|
||||
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
|
||||
p.add_argument("--limit", type=int, default=10, help="Max. Anzahl (Standard: 10)")
|
||||
p.set_defaults(func=cmd_poll_list)
|
||||
|
||||
# poll-create
|
||||
p = sub.add_parser("poll-create", help="Neue Umfrage anlegen")
|
||||
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
|
||||
p.add_argument("question", help="Die Frage (Pflicht)")
|
||||
p.add_argument("--answers", required=True,
|
||||
help="Antwortoptionen, mit | getrennt: 'Ja|Nein|Vielleicht' "
|
||||
"(mindestens zwei)")
|
||||
p.add_argument("--file", default=None,
|
||||
help="Markdown-Datei mit dem Beschreibungstext, '-' für stdin")
|
||||
p.add_argument("--anonymous", action="store_true", help="anonyme Abstimmung")
|
||||
p.add_argument("--multiple", action="store_true", help="Mehrfachauswahl erlauben")
|
||||
p.add_argument("--hide-results", action="store_true",
|
||||
help="Ergebnis erst nach Ende der Umfrage zeigen")
|
||||
p.set_defaults(func=cmd_poll_create)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "klare-sprache",
|
||||
"version": "0.1.0",
|
||||
"description": "Texte in klarer, verständlicher Sprache nach ISO 24495-1:2023 (Plain Language) schreiben oder überarbeiten: zielgruppen- und prozessorientiert — finden, verstehen, nutzen.",
|
||||
"author": {
|
||||
"name": "inmedias.it"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: klare-sprache
|
||||
description: Texte in klarer, verständlicher Sprache nach ISO 24495-1:2023 (Plain Language) schreiben oder überarbeiten. Use when the user asks for clear/plain/accessible language, mentions "klare Sprache", "verständlich schreiben", "plain language", "ISO 24495", "bürgernah", "leicht verständlich", "an die Zielgruppe anpassen", or wants a text a specific audience can easily find, understand, and use.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Klare Sprache (ISO 24495-1:2023)
|
||||
|
||||
Dieser Skill schreibt und überarbeitet Texte nach den Leitprinzipien der
|
||||
ISO 24495-1:2023 („Plain language — Part 1"). Ziel: Die vorgesehenen Leser
|
||||
finden leicht, was sie brauchen, verstehen es und können danach handeln.
|
||||
|
||||
Anders als starre Regelwerke (Wortlisten, feste Satzlängen) ist ISO 24495-1
|
||||
**prinzipien- und prozessorientiert**. Der Maßstab ist nicht „einfach an sich",
|
||||
sondern **passend für die konkrete Zielgruppe und ihren Zweck**.
|
||||
|
||||
## Wann anwenden
|
||||
|
||||
Wende den Skill an, wenn der Nutzer einen Text verständlich für eine bestimmte
|
||||
Zielgruppe braucht (Kunden-Doku, Anleitungen, Formulare, Briefe, Web-Texte,
|
||||
Bescheide). Für einen festen, normierten Schreibstil in der internen Technik-
|
||||
Doku passt eher [[einfaches-technisches-deutsch]].
|
||||
|
||||
## Kerndefinition
|
||||
|
||||
Ein Text ist in klarer Sprache, wenn Wortwahl, Struktur und Gestaltung so klar
|
||||
sind, dass die vorgesehenen Leser leicht
|
||||
|
||||
1. **finden**, was sie brauchen,
|
||||
2. **verstehen**, was sie finden, und
|
||||
3. **nutzen** können, was sie verstanden haben.
|
||||
|
||||
## Ablauf
|
||||
|
||||
1. **Zielgruppe und Zweck klären.** Wer liest? Welches Vorwissen? Was soll die
|
||||
Person danach wissen oder tun? Ist das unklar, frage kurz nach — ohne
|
||||
Zielgruppe lässt sich „klar" nicht beurteilen.
|
||||
2. **Schreiben/Überarbeiten** nach den vier Prinzipien (unten).
|
||||
3. **Prüfen** gegen die Checkliste; wenn möglich, aus Lesersicht gegenlesen.
|
||||
4. **Kurz zurückmelden**, welche Zielgruppe angenommen wurde und was geändert
|
||||
wurde.
|
||||
|
||||
## Die vier Prinzipien mit Checkliste
|
||||
|
||||
### 1. Relevant — Leser bekommen, was sie brauchen
|
||||
- Inhalt an Zielgruppe und Zweck ausrichten.
|
||||
- Vorwissen und Bedarf der Leser berücksichtigen.
|
||||
- Unnötiges weglassen; das Wichtigste zuerst.
|
||||
|
||||
### 2. Auffindbar — Leser finden es leicht
|
||||
- Logische, erwartbare Reihenfolge.
|
||||
- Aussagekräftige Überschriften und Zwischenüberschriften.
|
||||
- Gliederung, Listen, Hervorhebungen; klares Layout und Navigation.
|
||||
|
||||
### 3. Verständlich — Leser verstehen es beim ersten Lesen
|
||||
- Gebräuchliche Wörter; Fachbegriffe und Abkürzungen erklären oder ersetzen.
|
||||
- Kurze, klar gebaute Sätze; ein Gedanke pro Satz.
|
||||
- Aktiv statt Passiv; die Leser direkt ansprechen.
|
||||
- Beispiele, wo sie helfen.
|
||||
|
||||
### 4. Nutzbar — Leser können danach handeln
|
||||
- Handlungsschritte klar und in der richtigen Reihenfolge.
|
||||
- Sagen, wer was tun muss.
|
||||
- Den Text an einer echten Aufgabe prüfen und überarbeiten.
|
||||
|
||||
## Technische Angaben bleiben exakt
|
||||
|
||||
Klarheit betrifft die Sprache, nicht die Fakten. Dateipfade, Befehle,
|
||||
Funktions- und Feldnamen, Preise, Zahlen und Fristen bleiben unverändert.
|
||||
|
||||
## Grenzen
|
||||
|
||||
- Der Skill setzt die **Prinzipien** der Norm um; er gibt nicht den geschützten
|
||||
Normtext wieder und bescheinigt keine formale Konformität.
|
||||
- Gilt für Fließtext und Struktur, nicht für Code, Log-Ausgaben oder Zitate.
|
||||
- Ohne bekannte Zielgruppe zuerst nachfragen.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "redmine",
|
||||
"version": "0.1.0",
|
||||
"description": "Redmine via REST-API: Projekte/Tickets auflisten, anlegen, aktualisieren, kommentieren (redmine.py).",
|
||||
"author": {
|
||||
"name": "inmedias.it"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Redmine REST-API — Zugangsdaten (Kopie nach .env, Werte eintragen)
|
||||
# API-Key: https://redmine.inmedias.it/my/account → rechts "API-Zugriffsschlüssel"
|
||||
|
||||
REDMINE_URL=https://redmine.inmedias.it
|
||||
REDMINE_API_KEY=
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: redmine
|
||||
description: Redmine-Projekte und -Tickets lesen/anlegen/aktualisieren/kommentieren via REST-API. Use when the user mentions "Redmine", "Ticket", "Issue in Redmine", "Meilenstein/Milestone" or das Projekt "pg-wissen".
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Redmine (redmine.py)
|
||||
|
||||
Interagiert mit Redmine (https://redmine.inmedias.it) über die REST-API:
|
||||
Projekte und Tickets auflisten, anzeigen, anlegen, aktualisieren und
|
||||
kommentieren — über die mitgelieferte, self-contained `redmine.py`. Namen
|
||||
werden auf IDs aufgelöst (Projekt, Tracker, Status, Priorität).
|
||||
|
||||
## Ausführen
|
||||
|
||||
`redmine.py` ist ein eigenständiges `uv run --script` (Inline-Deps: `requests`,
|
||||
`python-dotenv`). Aus dem Skill-Verzeichnis:
|
||||
|
||||
```bash
|
||||
./redmine.py <befehl> ...
|
||||
```
|
||||
|
||||
`--json` (vor dem Befehl) gibt jeweils die Rohdaten aus.
|
||||
|
||||
### Kontext & Stammdaten
|
||||
|
||||
```bash
|
||||
./redmine.py me # aktueller Benutzer (id, admin?, mail)
|
||||
./redmine.py projects # sichtbare Projekte (id, identifier, name)
|
||||
./redmine.py meta trackers # Stammdaten: trackers | statuses | priorities
|
||||
```
|
||||
|
||||
### Tickets auflisten / anzeigen
|
||||
|
||||
```bash
|
||||
./redmine.py issues --project pg-wissen # offene Tickets (Default)
|
||||
./redmine.py issues --project pg-wissen --status '*' # alle (open|closed|*|Status-ID)
|
||||
./redmine.py issues --assigned me --sort priority:desc
|
||||
./redmine.py issues --query "Wiki" --limit 20 # Volltext im Betreff (~-Suche)
|
||||
./redmine.py show 290 [--notes] # Ticket (optional mit Journalen)
|
||||
```
|
||||
|
||||
### Anlegen / Aktualisieren / Kommentieren
|
||||
|
||||
`--project` und `--subject` sind bei `create` Pflicht. Beschreibung inline
|
||||
(`--description`) oder aus Datei/stdin (`--description-file -`) übergeben — nicht
|
||||
als langes Shell-Argument (Sonderzeichen). Weitere Felder: `--tracker`,
|
||||
`--status`, `--priority`, `--assigned me`, `--parent <id>`, `--done <0-100>`.
|
||||
|
||||
```bash
|
||||
./redmine.py create --project pg-wissen --subject "Titel" \
|
||||
--tracker Feature --description-file - < body.md
|
||||
./redmine.py update 290 --status Erledigt --done 100 --notes "erledigt"
|
||||
./redmine.py comment 290 "kurzer Kommentar"
|
||||
```
|
||||
|
||||
## Zugangsdaten
|
||||
|
||||
`redmine.py` bringt **keinen** Schlüssel mit. Jede/r hinterlegt einen eigenen
|
||||
API-Key als Umgebungsvariable oder in einer dotenv-Datei (Reihenfolge:
|
||||
`~/.config/dokumentierer/.env`, dann Datei im Arbeitsverzeichnis, dann bereits
|
||||
gesetzte Env-Variablen). Siehe `.env.example`:
|
||||
|
||||
- `REDMINE_URL` — Basis-URL (z.B. `https://redmine.inmedias.it`)
|
||||
- `REDMINE_API_KEY` — persönlicher API-Zugriffsschlüssel unter
|
||||
https://redmine.inmedias.it/my/account (rechts „API-Zugriffsschlüssel"). Fehlt
|
||||
der Schlüssel dort, ist der REST-Webservice noch nicht aktiviert
|
||||
(Administration → Konfiguration → API).
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Schreibzugriff nur in Projekten mit „Add/Edit issues"-Recht; ein Nicht-Admin
|
||||
kann keine Tickets in fremden Projekten ändern.
|
||||
- Ein neues Milestone (Version) anzulegen erfordert das Recht *manage_versions*
|
||||
und ist über `redmine.py` nicht abgedeckt — dafür die Weboberfläche oder die
|
||||
REST-API (`POST /projects/<id>/versions.json`) nutzen.
|
||||
Executable
+280
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# "python-dotenv",
|
||||
# ]
|
||||
# ///
|
||||
"""CLI für Redmine via REST-API: Projekte/Tickets auflisten, anlegen, aktualisieren, kommentieren.
|
||||
|
||||
Zugangsdaten aus .env: REDMINE_URL, REDMINE_API_KEY.
|
||||
|
||||
Beispiele:
|
||||
uv run redmine.py me
|
||||
uv run redmine.py projects
|
||||
uv run redmine.py issues --project pg-wissen --status open
|
||||
uv run redmine.py show 42 --notes
|
||||
uv run redmine.py create --project pg-wissen --subject "Titel" --description - < body.md
|
||||
uv run redmine.py update 42 --status Erledigt --notes "gefixt"
|
||||
uv run redmine.py comment 42 "kurzer Kommentar"
|
||||
uv run redmine.py meta trackers
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def session() -> tuple[requests.Session, str]:
|
||||
load_dotenv(Path.home() / ".config/dokumentierer/.env")
|
||||
load_dotenv() # CWD/Projektverzeichnis
|
||||
url = os.environ.get("REDMINE_URL", "").rstrip("/")
|
||||
key = os.environ.get("REDMINE_API_KEY", "")
|
||||
if not url or not key:
|
||||
sys.exit("Fehler: REDMINE_URL und/oder REDMINE_API_KEY fehlen in der .env")
|
||||
s = requests.Session()
|
||||
s.headers.update({"X-Redmine-API-Key": key, "Content-Type": "application/json"})
|
||||
s.base = url
|
||||
return s, url
|
||||
|
||||
|
||||
def api(s, method: str, path: str, **kw) -> dict:
|
||||
r = s.request(method, f"{s.base}{path}", **kw)
|
||||
if not r.ok:
|
||||
sys.exit(f"Fehler {r.status_code} bei {method} {path}: {r.text[:400]}")
|
||||
if r.status_code == 204 or not r.content:
|
||||
return {}
|
||||
return r.json()
|
||||
|
||||
|
||||
def paged(s, path: str, root: str, params: dict | None = None, cap: int = 1000) -> list:
|
||||
"""Alle Seiten einer Redmine-Collection einsammeln."""
|
||||
params = dict(params or {})
|
||||
out, offset = [], 0
|
||||
while True:
|
||||
params.update({"limit": 100, "offset": offset})
|
||||
d = api(s, "GET", path, params=params)
|
||||
items = d.get(root, [])
|
||||
out += items
|
||||
total = d.get("total_count", len(out))
|
||||
offset += len(items)
|
||||
if not items or offset >= total or len(out) >= cap:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
# ---- Auflösen von Namen/Identifiern auf IDs -----------------------------------
|
||||
|
||||
def resolve_project(s, value: str) -> int:
|
||||
if value.isdigit():
|
||||
return int(value)
|
||||
for p in paged(s, "/projects.json", "projects"):
|
||||
if value in (p.get("identifier"), p.get("name")):
|
||||
return p["id"]
|
||||
sys.exit(f"Projekt nicht gefunden: {value}")
|
||||
|
||||
|
||||
def resolve_enum(s, path: str, root: str, value: str, label: str) -> int:
|
||||
if value.isdigit():
|
||||
return int(value)
|
||||
items = api(s, "GET", path).get(root, [])
|
||||
for it in items:
|
||||
if it.get("name", "").lower() == value.lower():
|
||||
return it["id"]
|
||||
names = ", ".join(i.get("name", "") for i in items)
|
||||
sys.exit(f"{label} nicht gefunden: {value} (verfügbar: {names})")
|
||||
|
||||
|
||||
def resolve_assigned(s, value: str) -> int:
|
||||
if value in ("me", "@me"):
|
||||
return api(s, "GET", "/users/current.json")["user"]["id"]
|
||||
if value.isdigit():
|
||||
return int(value)
|
||||
sys.exit("--assigned erwartet 'me' oder eine numerische User-ID")
|
||||
|
||||
|
||||
def read_text(value: str | None, file: str | None) -> str | None:
|
||||
if file is not None:
|
||||
return sys.stdin.read() if file == "-" else Path(file).read_text(encoding="utf-8")
|
||||
return value
|
||||
|
||||
|
||||
# ---- Befehle ------------------------------------------------------------------
|
||||
|
||||
def cmd_me(s, a):
|
||||
u = api(s, "GET", "/users/current.json")["user"]
|
||||
if a.json:
|
||||
return print(json.dumps(u, ensure_ascii=False, indent=2))
|
||||
print(f"{u['login']} (id={u['id']}, {u.get('firstname','')} {u.get('lastname','')}, "
|
||||
f"admin={u.get('admin', False)}, mail={u.get('mail','?')})")
|
||||
|
||||
|
||||
def cmd_projects(s, a):
|
||||
ps = paged(s, "/projects.json", "projects")
|
||||
if a.json:
|
||||
return print(json.dumps(ps, ensure_ascii=False, indent=2))
|
||||
for p in sorted(ps, key=lambda x: x["id"]):
|
||||
print(f"#{p['id']:>3} {p['identifier']:<32} {p['name']}")
|
||||
print(f"— {len(ps)} Projekte")
|
||||
|
||||
|
||||
def cmd_issues(s, a):
|
||||
params = {"status_id": a.status, "sort": a.sort}
|
||||
if a.project:
|
||||
params["project_id"] = resolve_project(s, a.project)
|
||||
if a.assigned:
|
||||
params["assigned_to_id"] = resolve_assigned(s, a.assigned)
|
||||
if a.query:
|
||||
params["subject"] = "~" + a.query
|
||||
issues = paged(s, "/issues.json", "issues", params, cap=a.limit)[: a.limit]
|
||||
if a.json:
|
||||
return print(json.dumps(issues, ensure_ascii=False, indent=2))
|
||||
for i in issues:
|
||||
who = (i.get("assigned_to") or {}).get("name", "—")
|
||||
print(f"#{i['id']:>5} [{i['status']['name']:<12}] {i['tracker']['name']:<8} "
|
||||
f"{i['subject'][:70]:<70} ({who})")
|
||||
print(f"— {len(issues)} Tickets")
|
||||
|
||||
|
||||
def cmd_show(s, a):
|
||||
inc = "journals,attachments,relations,children" if a.notes else "attachments,relations"
|
||||
i = api(s, "GET", f"/issues/{a.id}.json", params={"include": inc})["issue"]
|
||||
if a.json:
|
||||
return print(json.dumps(i, ensure_ascii=False, indent=2))
|
||||
print(f"#{i['id']} {i['subject']}")
|
||||
print(f" Projekt : {i['project']['name']}")
|
||||
print(f" Status : {i['status']['name']} | Tracker: {i['tracker']['name']} | "
|
||||
f"Priorität: {i['priority']['name']}")
|
||||
print(f" Autor : {i['author']['name']} | Zugewiesen: {(i.get('assigned_to') or {}).get('name','—')}")
|
||||
print(f" Erstellt: {i.get('created_on','')} | Aktualisiert: {i.get('updated_on','')}")
|
||||
if i.get("description"):
|
||||
print(" ---\n" + "\n".join(" " + ln for ln in i["description"].splitlines()))
|
||||
if a.notes:
|
||||
for j in i.get("journals", []):
|
||||
if j.get("notes"):
|
||||
print(f" --- {j['user']['name']} @ {j.get('created_on','')}:\n"
|
||||
+ "\n".join(" " + ln for ln in j["notes"].splitlines()))
|
||||
|
||||
|
||||
def build_issue_fields(s, a) -> dict:
|
||||
f = {}
|
||||
if getattr(a, "project", None):
|
||||
f["project_id"] = resolve_project(s, a.project)
|
||||
if getattr(a, "subject", None) is not None:
|
||||
f["subject"] = a.subject
|
||||
desc = read_text(getattr(a, "description", None), getattr(a, "description_file", None))
|
||||
if desc is not None:
|
||||
f["description"] = desc
|
||||
if getattr(a, "tracker", None):
|
||||
f["tracker_id"] = resolve_enum(s, "/trackers.json", "trackers", a.tracker, "Tracker")
|
||||
if getattr(a, "status", None):
|
||||
f["status_id"] = resolve_enum(s, "/issue_statuses.json", "issue_statuses", a.status, "Status")
|
||||
if getattr(a, "priority", None):
|
||||
f["priority_id"] = resolve_enum(s, "/enumerations/issue_priorities.json",
|
||||
"issue_priorities", a.priority, "Priorität")
|
||||
if getattr(a, "assigned", None):
|
||||
f["assigned_to_id"] = resolve_assigned(s, a.assigned)
|
||||
if getattr(a, "parent", None):
|
||||
f["parent_issue_id"] = int(a.parent)
|
||||
if getattr(a, "done", None) is not None:
|
||||
f["done_ratio"] = int(a.done)
|
||||
if getattr(a, "notes", None):
|
||||
f["notes"] = a.notes
|
||||
return f
|
||||
|
||||
|
||||
def cmd_create(s, a):
|
||||
fields = build_issue_fields(s, a)
|
||||
if "project_id" not in fields or "subject" not in fields:
|
||||
sys.exit("create: --project und --subject sind Pflicht")
|
||||
res = api(s, "POST", "/issues.json", data=json.dumps({"issue": fields}))
|
||||
i = res["issue"]
|
||||
print(f"Angelegt: #{i['id']} {i['subject']} -> {s.base}/issues/{i['id']}")
|
||||
|
||||
|
||||
def cmd_update(s, a):
|
||||
fields = build_issue_fields(s, a)
|
||||
if not fields:
|
||||
sys.exit("update: nichts zu ändern angegeben")
|
||||
api(s, "PUT", f"/issues/{a.id}.json", data=json.dumps({"issue": fields}))
|
||||
print(f"Aktualisiert: #{a.id} ({', '.join(fields)})")
|
||||
|
||||
|
||||
def cmd_comment(s, a):
|
||||
api(s, "PUT", f"/issues/{a.id}.json", data=json.dumps({"issue": {"notes": a.text}}))
|
||||
print(f"Kommentar an #{a.id} hinzugefügt.")
|
||||
|
||||
|
||||
def cmd_meta(s, a):
|
||||
spec = {
|
||||
"trackers": ("/trackers.json", "trackers"),
|
||||
"statuses": ("/issue_statuses.json", "issue_statuses"),
|
||||
"priorities": ("/enumerations/issue_priorities.json", "issue_priorities"),
|
||||
}[a.kind]
|
||||
items = api(s, "GET", spec[0]).get(spec[1], [])
|
||||
if a.json:
|
||||
return print(json.dumps(items, ensure_ascii=False, indent=2))
|
||||
for it in items:
|
||||
extra = " (default)" if it.get("is_default") else ""
|
||||
print(f"#{it['id']:>3} {it['name']}{extra}")
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Redmine REST-API CLI")
|
||||
p.add_argument("--json", action="store_true", help="Rohdaten als JSON ausgeben")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
sub.add_parser("me", help="Aktuellen Benutzer anzeigen")
|
||||
sub.add_parser("projects", help="Projekte auflisten")
|
||||
|
||||
pi = sub.add_parser("issues", help="Tickets auflisten")
|
||||
pi.add_argument("--project", help="Projekt (ID, Identifier oder Name)")
|
||||
pi.add_argument("--status", default="open", help="open (Default) | closed | * | Status-ID")
|
||||
pi.add_argument("--assigned", help="me | User-ID")
|
||||
pi.add_argument("--query", help="Volltext im Betreff (~-Suche)")
|
||||
pi.add_argument("--sort", default="updated_on:desc", help="z.B. updated_on:desc, priority:desc")
|
||||
pi.add_argument("--limit", type=int, default=50)
|
||||
|
||||
ps = sub.add_parser("show", help="Ticket anzeigen")
|
||||
ps.add_argument("id")
|
||||
ps.add_argument("--notes", action="store_true", help="Journale/Kommentare mit anzeigen")
|
||||
|
||||
for name, help_ in (("create", "Ticket anlegen"), ("update", "Ticket aktualisieren")):
|
||||
c = sub.add_parser(name, help=help_)
|
||||
if name == "update":
|
||||
c.add_argument("id")
|
||||
c.add_argument("--project", help="Projekt (ID/Identifier/Name)" + (" [Pflicht]" if name == "create" else ""))
|
||||
c.add_argument("--subject")
|
||||
c.add_argument("--description", help="Beschreibungstext")
|
||||
c.add_argument("--description-file", help="Datei oder '-' für stdin")
|
||||
c.add_argument("--tracker", help="Tracker (Name oder ID)")
|
||||
c.add_argument("--status", help="Status (Name oder ID)")
|
||||
c.add_argument("--priority", help="Priorität (Name oder ID)")
|
||||
c.add_argument("--assigned", help="me | User-ID")
|
||||
c.add_argument("--parent", help="übergeordnetes Ticket (ID)")
|
||||
c.add_argument("--done", type=int, help="Fortschritt in %% (0-100)")
|
||||
if name == "update":
|
||||
c.add_argument("--notes", help="Kommentar zur Änderung")
|
||||
|
||||
pc = sub.add_parser("comment", help="Kommentar an ein Ticket hängen")
|
||||
pc.add_argument("id"); pc.add_argument("text")
|
||||
|
||||
pm = sub.add_parser("meta", help="Stammdaten auflisten")
|
||||
pm.add_argument("kind", choices=["trackers", "statuses", "priorities"])
|
||||
|
||||
a = p.parse_args()
|
||||
s, _ = session()
|
||||
{
|
||||
"me": cmd_me, "projects": cmd_projects, "issues": cmd_issues, "show": cmd_show,
|
||||
"create": cmd_create, "update": cmd_update, "comment": cmd_comment, "meta": cmd_meta,
|
||||
}[a.cmd](s, a)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user