Author SHA1 Message Date
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
skoandClaude Opus 5 64744a213d README: Einrichtung an oeffentliches Repo angepasst
Das Repo ist inzwischen oeffentlich lesbar; `/plugin marketplace add` klont
direkt ueber HTTPS. Die insteadOf-Umleitung auf SSH ist dafuer nicht mehr
noetig und steht jetzt nur noch als Rueckfallweg fuer den Fall, dass das Repo
wieder privat gestellt wird.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:11:03 +02:00
skoandClaude Opus 4.8 04886d73f5 humhub: fetch-Befehl + space-list/resolve_container ins Plugin
- fetch <content-id|perma-url> [--save]: löst Perma-Links selbst auf
- space-list + resolve_container: container_id akzeptiert Slug/URL/ID
- SKILL.md entsprechend ergänzt

Gleicht das Plugin an die kanonische humhub.py (dokumentierer) an.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-05 06:03:41 +02:00
sko aa9888700d Merge pull request 'plugins/grill-me: grill-me als Skill (Plan/Idee gnadenlos durchfragen)' (#3) from feature/grill-me-plugin into main 2026-07-29 08:44:33 +02:00
skoandClaude Opus 4.8 437bd1c1ee plugins/grill-me: grill-me als Skill (Plan/Idee gnadenlos durchfragen)
Deutsche Adaption des englischen Originals von Matt Pocock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 08:43:33 +02:00
skoandClaude Opus 4.8 0fd228b962 README: mediawiki- und humhub-Plugin dokumentiert
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:17:04 +02:00
sko ad893e20d5 Merge pull request 'plugins/mediawiki: wiki.py als Skill (Wiki lesen/rendern/schreiben)' (#1) from feature/mediawiki-plugin into main
Reviewed-on: #1
2026-07-16 10:10:23 +02:00
8 changed files with 331 additions and 22 deletions
+10
View File
@@ -32,6 +32,16 @@
"name": "mediawiki",
"source": "./plugins/mediawiki",
"description": "MediaWiki-Seiten/Templates lesen, rendern und schreiben via wiki.py (Instanzen wiki & devwiki; Zugangsdaten 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."
}
]
}
+45 -13
View File
@@ -5,26 +5,28 @@ und kann einzeln installiert werden — niemand bekommt alles auf einmal.
## Einrichtung (einmalig)
`/plugin marketplace add` akzeptiert nur `owner/repo` (immer GitHub!), `https://…`
oder einen lokalen `./path`**kein** `ssh://`. Da dieses Repo privat ist und der
Plugin-Clone nicht interaktiv nach Zugangsdaten fragen kann, muss git HTTPS-Zugriffe
auf das Gitea transparent auf SSH umleiten. Einmalig pro Rechner:
Dieses Repo ist öffentlich lesbar. Der Marketplace lässt sich deshalb direkt über
seine HTTPS-URL hinzufügen — ohne Zugangsdaten und ohne git-Konfiguration:
```bash
# 1. git so konfigurieren, dass https auf dieses Gitea über SSH (Key) läuft
git config --global url."ssh://gitea@gitea.inmedias.it:2222/".insteadOf "https://gitea.inmedias.it/"
# 2. Marketplace mit der HTTPS-URL hinzufügen (klont real über SSH)
/plugin marketplace add https://gitea.inmedias.it/inmedias.it/claude-plugins.git
```
Voraussetzung: dein Gitea-SSH-Key ist auf Port 2222 eingerichtet.
Zum **Pushen** ist weiterhin ein Gitea-SSH-Key auf Port 2222 nötig.
> Hinweis: Die `insteadOf`-Regel gilt für **alle** git-Operationen auf
> `gitea.inmedias.it`, nicht nur für Plugins.
> Hinweis: `/plugin marketplace add` akzeptiert nur `owner/repo` (immer GitHub!),
> `https://…` oder einen lokalen `./path` — **kein** `ssh://`.
>
> Alternative ohne git-Config: Repo `inmedias.it/claude-plugins` intern/öffentlich
> lesbar machen — dann reicht die HTTPS-URL direkt (SSH-Key trotzdem für Push nötig).
> Falls das Repo wieder auf privat gestellt wird, kann der Plugin-Clone nicht
> interaktiv nach Zugangsdaten fragen. Dann muss git HTTPS-Zugriffe auf das Gitea
> transparent auf SSH umleiten:
>
> ```bash
> git config --global url."ssh://gitea@gitea.inmedias.it:2222/".insteadOf "https://gitea.inmedias.it/"
> ```
>
> Diese Regel gilt für **alle** git-Operationen auf `gitea.inmedias.it`, nicht nur
> für Plugins.
## Plugin installieren
@@ -105,6 +107,36 @@ self-contained `wiki.py` (`fetch`/`render`/`search`/`update`). Instanzen `wiki`
/plugin install mediawiki@inmedias-claude-plugins
```
### grill-me
Löchert dich gnadenlos mit Fragen zu einem Plan, einer Entscheidung oder Idee —
geht den Entscheidungsbaum Zweig für Zweig durch, eine Frage nach der anderen (je
mit Empfehlung), bis ein gemeinsames Verständnis steht. Startet die Umsetzung erst
nach deiner Bestätigung.
- Kein Setup, keine Zugangsdaten. Auslösen mit „grill mich" bzw. wenn du deinen
Plan stresstesten willst.
- Deutsche Adaption des englischen Originals von Matt Pocock:
[mattpocock/skills · grill-me](https://github.com/mattpocock/skills/blob/main/skills/productivity/grill-me/SKILL.md).
```bash
/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
```
## 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.
@@ -0,0 +1,8 @@
{
"name": "grill-me",
"version": "0.1.0",
"description": "Löchert dich gnadenlos mit Fragen zu einem Plan, einer Entscheidung oder Idee, bis ein gemeinsames Verständnis steht.",
"author": {
"name": "inmedias.it"
}
}
+16
View File
@@ -0,0 +1,16 @@
---
name: grill-me
description: Löchert dich gnadenlos mit Fragen zu einem Plan, einer Entscheidung oder Idee, bis ein gemeinsames Verständnis steht. Nutzen, wenn der User seinen Plan stresstesten will oder "grill mich" sagt.
---
Interviewe mich gnadenlos zu jedem Aspekt dieses Vorhabens, bis wir ein gemeinsames Verständnis erreicht haben. Gehe jeden Zweig des Entscheidungsbaums durch und löse die Abhängigkeiten zwischen den Entscheidungen nacheinander auf. Gib mir zu jeder Frage deine empfohlene Antwort dazu.
Stelle die Fragen einzeln und warte auf meine Antwort, bevor du weitermachst. Mehrere Fragen auf einmal sind verwirrend.
Wenn sich ein Fakt durch Erkunden der Umgebung (Dateien, Tools usw.) herausfinden lässt, schlag ihn selbst nach, statt mich zu fragen. Die Entscheidungen aber gehören mir: Leg mir jede einzeln vor und warte auf meine Antwort.
Fang nicht mit der Umsetzung an, bevor ich bestätigt habe, dass wir ein gemeinsames Verständnis erreicht haben.
<!-- Deutsche Adaption des englischen Originals von Matt Pocock:
https://github.com/mattpocock/skills/blob/main/skills/productivity/grill-me/SKILL.md -->
+27 -3
View File
@@ -19,6 +19,29 @@ mitgelieferte, self-contained `humhub.py`.
./humhub.py <befehl> ...
```
### Perma-Link / Content-ID holen (empfohlener Einstieg)
HumHub-Links der Form `…/content/perma?id=NNNN` tragen eine **Content-ID** — nicht
die Wiki-Seiten- oder Post-ID, die die anderen Befehle erwarten. `fetch` löst das
selbst auf (erkennt WikiPage vs. Post) und braucht keine Vorab-Recherche:
```bash
./humhub.py fetch <content-id|perma-url> # Inhalt + Herkunfts-Kopf auf stdout
./humhub.py fetch <content-id|perma-url> --save # ablegen unter resources/humhub-<id>-<slug>.md
./humhub.py fetch <content-id|perma-url> --save --out <dir>
```
`--save` schreibt Markdown mit einem Herkunfts-Kopf (Quelle, Titel, Typ, Topics,
Stand, Abrufdatum). Zielverzeichnis ist standardmäßig `resources/` im aktuellen
Projekt — dieses gehört üblicherweise in `.gitignore` (abgerufene Fremdinhalte
werden nicht committet).
### Spaces
```bash
./humhub.py space-list # Spaces auflisten mit contentcontainer_id
```
### Wiki
```bash
@@ -37,9 +60,10 @@ mitgelieferte, self-contained `humhub.py`.
```
Inhalte (Markdown) aus Datei oder `-` (stdin) übergeben — nicht als langes
Shell-Argument (Sonderzeichen). `container_id` ist die Space-/Profil-Container-ID
in HumHub (z.&nbsp;B. ein User- oder Space-Container). Mit `wiki-list` /
`post-list` die passende ID/Seite ermitteln.
Shell-Argument (Sonderzeichen). `container_id` akzeptiert eine numerische
Container-ID, einen **Space-Slug** oder eine **Space-URL** (`resolve_container`
löst Slug/URL selbst auf). Passende Container per `space-list` ermitteln, Seiten
per `wiki-list` / `post-list`.
## Zugangsdaten
+165 -6
View File
@@ -11,8 +11,11 @@
import argparse
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from urllib.parse import parse_qs, urlparse
import requests
from dotenv import load_dotenv, find_dotenv
@@ -52,6 +55,30 @@ def api_post(session: requests.Session, path: str, payload: dict) -> dict:
return r.json()
def resolve_container(session: requests.Session, value: str) -> int:
"""Resolve a container ID, space slug, or space URL to a contentcontainer_id."""
try:
return int(value)
except ValueError:
pass
# Strip full URL down to slug
slug = value.rstrip("/").split("/s/")[-1].rstrip("/") if "/s/" in value else value.strip("/")
page = 1
while True:
data = api_get(session, f"/space?limit=50&page={page}")
results = data.get("results", [])
if not results:
break
for space in results:
space_slug = space.get("url", "").rstrip("/").split("/s/")[-1].rstrip("/")
if space_slug == slug or space.get("name", "").lower() == slug.lower():
return space["contentcontainer_id"]
if len(results) < 50:
break
page += 1
sys.exit(f"Error: Space '{value}' nicht gefunden. Verfügbare Spaces: uv run humhub.py space-list")
def parse_topics(topics_str: str | None) -> list[dict]:
if not topics_str:
return []
@@ -126,6 +153,7 @@ def cmd_wiki_update(args: argparse.Namespace) -> None:
def cmd_wiki_create(args: argparse.Namespace) -> None:
session, _ = get_session()
container_id = resolve_container(session, args.container_id)
content = read_file(args.file) if args.file else ""
wiki_page: dict = {"title": args.title}
@@ -140,7 +168,7 @@ def cmd_wiki_create(args: argparse.Namespace) -> None:
if topics:
payload["content"] = {"topics": topics}
data = api_post(session, f"/wiki/container/{args.container_id}", payload)
data = api_post(session, f"/wiki/container/{container_id}", payload)
page_id = data.get("id")
url = data.get("content", {}).get("metadata", {}).get("url", "")
print(f"Erstellt: '{data.get('title')}' (ID {page_id})")
@@ -152,7 +180,8 @@ def cmd_wiki_create(args: argparse.Namespace) -> None:
def cmd_post_list(args: argparse.Namespace) -> None:
session, _ = get_session()
data = api_get(session, f"/post/container/{args.container_id}?limit={args.limit}")
container_id = resolve_container(session, args.container_id)
data = api_get(session, f"/post/container/{container_id}?limit={args.limit}")
results = data.get("results", [])
if not results:
print("(keine Posts)")
@@ -167,6 +196,7 @@ def cmd_post_list(args: argparse.Namespace) -> None:
def cmd_post_create(args: argparse.Namespace) -> None:
session, _ = get_session()
container_id = resolve_container(session, args.container_id)
message = read_file(args.file)
topics = parse_topics(args.topics)
@@ -174,7 +204,7 @@ def cmd_post_create(args: argparse.Namespace) -> None:
if topics:
payload["data"]["content"] = {"topics": topics}
data = api_post(session, f"/post/container/{args.container_id}", payload)
data = api_post(session, f"/post/container/{container_id}", payload)
pid = data.get("id")
saved_topics = [t["name"] for t in data.get("content", {}).get("topics", [])]
print(f"Post erstellt (ID {pid})")
@@ -199,12 +229,137 @@ def cmd_post_update(args: argparse.Namespace) -> None:
print(f"Topics: {', '.join(saved_topics)}")
def cmd_space_list(args: argparse.Namespace) -> None:
session, _ = get_session()
page = 1
found = False
while True:
data = api_get(session, f"/space?limit=50&page={page}")
results = data.get("results", [])
if not results:
break
for space in results:
slug = space.get("url", "").rstrip("/").split("/s/")[-1].rstrip("/")
ccid = space.get("contentcontainer_id", "?")
name = space.get("name", "")
print(f"ccid={ccid:>4} slug={slug:<30} {name}")
found = True
if len(results) < 50:
break
page += 1
if not found:
print("(keine Spaces gefunden)")
# ── Fetch (perma-/content-id-agnostisch) ─────────────────────────────────────
def parse_content_ref(ref: str) -> int:
"""Akzeptiert eine blanke Content-ID oder einen Perma-Link
(…/content/perma?id=NNNN) und liefert die Content-ID."""
ref = ref.strip()
if ref.isdigit():
return int(ref)
qs = parse_qs(urlparse(ref).query)
if "id" in qs and qs["id"][0].isdigit():
return int(qs["id"][0])
sys.exit(f"Error: keine Content-ID erkennbar in '{ref}'")
def slugify(text: str) -> str:
text = text.lower()
for a, b in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
text = text.replace(a, b)
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
return text[:60] or "inhalt"
def resolve_content(session: requests.Session, content_id: int) -> dict:
"""Löst eine Content-ID auf den zugrundeliegenden Datensatz auf
(WikiPage oder Post) und liefert Titel, Inhalt und Metadaten."""
envelope = api_get(session, f"/content/{content_id}")
meta = envelope.get("metadata", {})
model = meta.get("object_model", "")
object_id = meta.get("object_id")
page_url = meta.get("url", "")
topics = [t["name"] for t in envelope.get("topics", [])]
updated_at = meta.get("updated_at", "")
if model.endswith("WikiPage"):
data = api_get(session, f"/wiki/page/{object_id}")
title = data.get("title", "") or f"Wiki-Seite {object_id}"
content = data.get("latest_revision", {}).get("content", "")
kind = f"WikiPage (page {object_id})"
seg = page_url.rstrip("/").split("/")[-1]
slug = seg if seg and not seg.isdigit() else slugify(title)
elif model.endswith("Post"):
data = api_get(session, f"/post/{object_id}")
content = data.get("data", {}).get("message") or data.get("message", "")
first = content.splitlines()[0].lstrip("# ").strip() if content else ""
title = first or f"Post {object_id}"
kind = f"Post (id {object_id})"
slug = slugify(title)
else:
sys.exit(f"Error: Objekttyp nicht unterstützt: {model or '(unbekannt)'}")
return {
"content_id": content_id,
"title": title,
"content": content,
"kind": kind,
"topics": topics,
"updated_at": updated_at,
"page_url": f"https://humhub.inmedias.it{page_url}" if page_url else "",
"slug": slug,
}
def render_resource(info: dict) -> str:
"""Inhalt mit Herkunfts-Kopf als Markdown-Kommentar."""
perma = f"https://humhub.inmedias.it/content/perma?id={info['content_id']}"
header = (
"<!--\n"
f"Quelle: {perma}\n"
f"Seite: {info['page_url']}\n"
f"Titel: {info['title']}\n"
f"Typ: {info['kind']}\n"
f"Topics: {', '.join(info['topics']) or '(keine)'}\n"
f"Stand: {info['updated_at']}\n"
f"Abgerufen: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
"-->\n\n"
)
return header + info["content"].rstrip() + "\n"
def cmd_fetch(args: argparse.Namespace) -> None:
session, _ = get_session()
content_id = parse_content_ref(args.ref)
info = resolve_content(session, content_id)
body = render_resource(info)
if args.save:
outdir = Path(args.out)
outdir.mkdir(parents=True, exist_ok=True)
path = outdir / f"humhub-{content_id}-{info['slug']}.md"
path.write_text(body, encoding="utf-8")
print(f"Gespeichert: {path}")
print(f" {info['kind']}'{info['title']}' ({len(info['content'])} Zeichen)")
else:
sys.stdout.write(body)
# ── Main ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(description="HumHub REST API CLI")
sub = parser.add_subparsers(dest="command", required=True)
# fetch
p = sub.add_parser("fetch", help="Content-ID/Perma-Link auflösen und Inhalt holen (WikiPage oder Post)")
p.add_argument("ref", help="Content-ID oder Perma-Link (…/content/perma?id=NNNN)")
p.add_argument("--save", action="store_true", help="In Datei ablegen statt auf stdout")
p.add_argument("--out", default="resources", help="Zielverzeichnis für --save (Standard: resources/)")
p.set_defaults(func=cmd_fetch)
# wiki-list
p = sub.add_parser("wiki-list", help="Alle Wiki-Seiten auflisten")
p.add_argument("filter", nargs="?", help="Filter nach Titel (case-insensitiv)")
@@ -224,9 +379,13 @@ def main() -> None:
p.add_argument("--parent-id", type=int, default=None, dest="parent_id", help="ID der Oberseite")
p.set_defaults(func=cmd_wiki_update)
# space-list
p = sub.add_parser("space-list", help="Spaces auflisten mit contentcontainer_id")
p.set_defaults(func=cmd_space_list)
# wiki-create
p = sub.add_parser("wiki-create", help="Neue Wiki-Seite anlegen")
p.add_argument("container_id", type=int, help="Container-ID (z.B. 37 für user serge)")
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
p.add_argument("title", help="Seitentitel")
p.add_argument("file", nargs="?", default=None, help="Markdown-Datei oder '-' für stdin (optional)")
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen")
@@ -235,13 +394,13 @@ def main() -> None:
# post-list
p = sub.add_parser("post-list", help="Posts eines Containers auflisten")
p.add_argument("container_id", type=int, help="Container-ID")
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
p.add_argument("--limit", type=int, default=10, help="Max. Anzahl Posts (Standard: 10)")
p.set_defaults(func=cmd_post_list)
# post-create
p = sub.add_parser("post-create", help="Neuen Post erstellen")
p.add_argument("container_id", type=int, help="Container-ID")
p.add_argument("container_id", help="Space-Slug, URL oder numerische Container-ID")
p.add_argument("file", help="Markdown-Datei oder '-' für stdin")
p.add_argument("--topics", default=None, help="Komma-getrennte Topic-Namen")
p.set_defaults(func=cmd_post_create)