Author SHA1 Message Date
jhsandClaude Opus 5 15dedb3d3f humhub: Umfragen anlegen und auflisten (poll-create / poll-list)
Die Polls-REST-API ist auf der Instanz aktiv, der Skill kannte sie nicht.
Zwei Abweichungen vom Muster der Post-API, die beide in
HTTP 500 'Internal error while save a poll!' enden:

* Der Controller macht $poll->load(Yii::$app->request->post()) — die Felder
  gehoeren unter den Yii-Formnamen 'Poll', nicht unter 'data'.
* Die Optionen heissen beim Anlegen 'newAnswers'; 'answers' ist die Leseform
  und wird beim POST ignoriert.

Beim Lesen ist limit Pflicht: ohne Begrenzung antwortet die Instanz ab etwa
25 Umfragen ebenfalls mit 500.

Quelle: humhub/polls, controllers/rest/PollsController.php + models/Poll.php.
Verifiziert an Space inmedias (Umfrage 36).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 07:58:20 +02:00
skoandClaude Opus 4.8 dc3e6a0c68 redmine: Plugin für Redmine via REST-API hinzufügen
Self-contained redmine.py (me/projects/issues/show/create/update/comment/
meta) mit Namensaufloesung (Projekt/Tracker/Status/Prioritaet). SKILL.md,
plugin.json, Beispiel-Konfig; Eintrag in marketplace.json und README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 09:42:25 +02:00
skoandClaude Opus 4.8 28ec699e51 README: klare-sprache-Plugin dokumentieren
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 04:56:06 +02:00
skoandClaude Opus 4.8 c09ea07d3f klare-sprache: Plugin für klare Sprache (ISO 24495-1:2023) hinzufügen
Neues Skill-Plugin, das Texte nach den Leitprinzipien der ISO 24495-1:2023
(Plain Language) schreibt/überarbeitet: zielgruppen- und prozessorientiert
(finden – verstehen – nutzen). Im marketplace.json registriert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 04:54:34 +02:00
sko 36dcb843b9 Merge pull request 'plugins/einfaches-technisches-deutsch: Schreibstil-Skill (ASD-STE100 auf Deutsch)' (#5) from feature/einfaches-technisches-deutsch-plugin into main 2026-08-12 06:46:06 +02:00
skoandClaude Opus 4.8 919cd52bff plugins/einfaches-technisches-deutsch: Schreibstil-Skill (ASD-STE100 auf Deutsch)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 06:45:51 +02:00
sko 27f40ee18a Merge pull request 'README: Einrichtung an oeffentliches Repo angepasst' (#4) from docs/readme-oeffentlicher-marketplace into main
Reviewed-on: #4
2026-08-11 10:23:00 +02:00
14 changed files with 680 additions and 7 deletions
+15
View File
@@ -33,10 +33,25 @@
"source": "./plugins/mediawiki", "source": "./plugins/mediawiki",
"description": "MediaWiki-Seiten/Templates lesen, rendern und schreiben via wiki.py (Instanzen wiki & devwiki; Zugangsdaten lokal)." "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", "name": "grill-me",
"source": "./plugins/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." "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."
} }
] ]
} }
+50 -2
View File
@@ -79,9 +79,10 @@ natürlicher klingen. Basiert auf Wikipedias „Signs of AI writing".
### humhub ### 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 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 - Voraussetzung: `HUMHUB_API_TOKEN` als Umgebungsvariable bzw. dotenv-Datei
(kein Token im Plugin). Token nur als berechtigter Benutzer unter (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 /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 ### grill-me
Löchert dich gnadenlos mit Fragen zu einem Plan, einer Entscheidung oder Idee — 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 /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 ## Updates
```bash ```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.
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "humhub", "name": "humhub",
"version": "0.1.0", "version": "0.2.0",
"description": "HumHub via REST-API: Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren (humhub.py).", "description": "HumHub via REST-API: Wiki-Seiten, Posts und Umfragen auflisten, anzeigen, anlegen und aktualisieren (humhub.py).",
"author": { "author": {
"name": "inmedias.it" "name": "inmedias.it"
} }
+24 -3
View File
@@ -1,13 +1,13 @@
--- ---
name: humhub 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". 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.1.0 version: 0.2.0
--- ---
# HumHub (humhub.py) # HumHub (humhub.py)
Interagiert mit HumHub (https://humhub.inmedias.it) über die REST-API: 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`. mitgelieferte, self-contained `humhub.py`.
## Ausführen ## Ausführen
@@ -59,6 +59,27 @@ werden nicht committet).
./humhub.py post-update <id> [datei|-] ./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.
Inhalte (Markdown) aus Datei oder `-` (stdin) übergeben — nicht als langes Inhalte (Markdown) aus Datei oder `-` (stdin) übergeben — nicht als langes
Shell-Argument (Sonderzeichen). `container_id` akzeptiert eine numerische Shell-Argument (Sonderzeichen). `container_id` akzeptiert eine numerische
Container-ID, einen **Space-Slug** oder eine **Space-URL** (`resolve_container` Container-ID, einen **Space-Slug** oder eine **Space-URL** (`resolve_container`
+76
View File
@@ -212,6 +212,61 @@ def cmd_post_create(args: argparse.Namespace) -> None:
print(f"Topics: {', '.join(saved_topics)}") 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: def cmd_post_update(args: argparse.Namespace) -> None:
session, _ = get_session() session, _ = get_session()
message = read_file(args.file) message = read_file(args.file)
@@ -412,6 +467,27 @@ def main() -> None:
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen (ersetzt bestehende)") p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen (ersetzt bestehende)")
p.set_defaults(func=cmd_post_update) 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 = parser.parse_args()
args.func(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=
+76
View File
@@ -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.
+280
View File
@@ -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()