diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 16b8450..092a60f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -27,6 +27,11 @@ "name": "humhub", "source": "./plugins/humhub", "description": "HumHub via REST-API: Wiki-Seiten und Posts auflisten, anzeigen, anlegen und aktualisieren (Token lokal)." + }, + { + "name": "mediawiki", + "source": "./plugins/mediawiki", + "description": "MediaWiki-Seiten/Templates lesen, rendern und schreiben via wiki.py (Instanzen wiki & devwiki; Zugangsdaten lokal)." } ] } diff --git a/plugins/mediawiki/.claude-plugin/plugin.json b/plugins/mediawiki/.claude-plugin/plugin.json new file mode 100644 index 0000000..2702aa9 --- /dev/null +++ b/plugins/mediawiki/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "mediawiki", + "version": "0.1.0", + "description": "MediaWiki-Seiten/Templates lesen, rendern und schreiben via wiki.py (fetch/update/render/search, Instanzen wiki & devwiki).", + "author": { + "name": "inmedias.it" + } +} diff --git a/plugins/mediawiki/skills/mediawiki/.env.example b/plugins/mediawiki/skills/mediawiki/.env.example new file mode 100644 index 0000000..f4c7ba5 --- /dev/null +++ b/plugins/mediawiki/skills/mediawiki/.env.example @@ -0,0 +1,14 @@ +# Zugangsdaten fuer wiki.py — kopieren nach ~/.config/dokumentierer/.env +# oder .env im Arbeitsverzeichnis, oder als Umgebungsvariablen setzen. +# NICHT committen. Am besten ein eigener Bot-/Service-Account mit Schreibrechten. + +# Prod-Instanz (wiki -> https://wiki.inmedias.it) +MEDIAWIKI_USER=botuser +MEDIAWIKI_PASSWORD=change-me +# MEDIAWIKI_URL=https://wiki.inmedias.it # nur falls abweichend + +# Test-Instanz (devwiki -> https://devwiki.inmedias.it) +# Faellt auf MEDIAWIKI_USER/PASSWORD zurueck, wenn nicht gesetzt. +DEVMEDIAWIKI_USER=botuser +DEVMEDIAWIKI_PASSWORD=change-me +# DEVMEDIAWIKI_URL=https://devwiki.inmedias.it # nur falls abweichend diff --git a/plugins/mediawiki/skills/mediawiki/SKILL.md b/plugins/mediawiki/skills/mediawiki/SKILL.md new file mode 100644 index 0000000..3d25f96 --- /dev/null +++ b/plugins/mediawiki/skills/mediawiki/SKILL.md @@ -0,0 +1,63 @@ +--- +name: mediawiki +description: MediaWiki-Seiten und -Templates lesen, rendern und schreiben. Use when editing/fetching wiki pages (wiki.inmedias.it, devwiki.inmedias.it) or when the user mentions "wiki", "Vorlage/Template", "Kundenseite". +version: 0.1.0 +--- + +# MediaWiki (wiki.py) + +Liest, rendert und schreibt MediaWiki-Seiten/Templates über die mitgelieferte +CLI `wiki.py` (mwclient + Login). Unterstützt mehrere Instanzen. + +## Grundregel + +Wiki-Seiten/Templates werden **ausschließlich** über `wiki.py` (oder ein +MediaWiki-MCP) bearbeitet — **niemals** per `ssh php .../maintenance/edit.php` +oder anderen direkten Server-Eingriffen. Reine Lese-/Render-Checks +(`curl ...?action=render`) sind ok; nur das **Schreiben** läuft über `wiki.py`. + +## Ausführen + +`wiki.py` ist ein eigenständiges `uv run --script` (Inline-Deps: `mwclient`, +`python-dotenv`). Aus dem Skill-Verzeichnis ausführen: + +```bash +./wiki.py -w ... +``` + +`-w` / `--wiki` wählt die Instanz (Default: `wiki`): +- `wiki` → https://wiki.inmedias.it (Prod) +- `devwiki` → https://devwiki.inmedias.it (Test) + +### Befehle + +```bash +./wiki.py -w devwiki fetch "Kunde:Beispiel GmbH" # Wikitext -> stdout +./wiki.py -w devwiki render "Kunde:Beispiel GmbH" # gerendertes HTML -> stdout +./wiki.py -w devwiki search "Suchbegriff" [-n ] # Titelsuche +./wiki.py -w devwiki update "Kunde:Beispiel GmbH" datei.wiki --summary "..." # speichern +./wiki.py -w wiki update "Titel" - --summary "..." # Inhalt von stdin +``` + +Tipp: Beim Schreiben den Inhalt aus einer Datei (`datei.wiki`) oder `-` (stdin) +übergeben — nicht als langes Shell-Argument (Sonderzeichen/Backticks!). + +## Zugangsdaten + +`wiki.py` bringt **keine** Zugangsdaten mit. Jede/r hinterlegt eigene Creds als +Umgebungsvariablen oder in einer `.env` (Reihenfolge: `~/.config/dokumentierer/.env`, +dann `.env` im aktuellen Verzeichnis, dann bereits gesetzte Env-Variablen). Siehe +`.env.example`: + +- `MEDIAWIKI_USER`, `MEDIAWIKI_PASSWORD` — für die Prod-Instanz (`wiki`) +- `DEVMEDIAWIKI_USER`, `DEVMEDIAWIKI_PASSWORD` — für `devwiki` (fällt auf die + `MEDIAWIKI_*`-Werte zurück, wenn nicht gesetzt) +- optional `MEDIAWIKI_URL` / `DEVMEDIAWIKI_URL` — nur nötig, wenn die + Default-URLs abweichen + +Es empfiehlt sich ein eigener Bot-/Service-Account mit den nötigen Schreibrechten. + +## Hinweise +- `search` liefert Titel; `render` ist nützlich, um vor/nach dem Schreiben das + Ergebnis zu prüfen, ohne die Seite im Browser zu öffnen. +- Für neue Instanzen: `WIKI_ALIASES` oben in `wiki.py` erweitern. diff --git a/plugins/mediawiki/skills/mediawiki/wiki.py b/plugins/mediawiki/skills/mediawiki/wiki.py new file mode 100755 index 0000000..e1ec04f --- /dev/null +++ b/plugins/mediawiki/skills/mediawiki/wiki.py @@ -0,0 +1,181 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "mwclient", +# "python-dotenv", +# ] +# /// +"""CLI for fetching and updating MediaWiki pages.""" + +import argparse +import os +import sys +from pathlib import Path +from urllib.parse import urlparse + +from dotenv import load_dotenv +import mwclient + + +WIKI_ALIASES = { + "wiki": ("MEDIAWIKI_URL", "https://wiki.inmedias.it"), + "devwiki": ("DEVMEDIAWIKI_URL", "https://devwiki.inmedias.it"), +} + + +def get_site(wiki: str = "wiki") -> mwclient.Site: + load_dotenv(Path.home() / ".config/dokumentierer/.env") + load_dotenv() # current dir, doesn't override already-set vars + + if wiki not in WIKI_ALIASES: + sys.exit(f"Error: unknown wiki '{wiki}'. Known: {', '.join(WIKI_ALIASES)}") + + url_env, url_default = WIKI_ALIASES[wiki] + url = os.environ.get(url_env, url_default).rstrip("/") + + prefix = "DEVMEDIAWIKI_" if wiki == "devwiki" else "MEDIAWIKI_" + user = os.environ.get(f"{prefix}USER") or os.environ.get("MEDIAWIKI_USER", "") + password = os.environ.get(f"{prefix}PASSWORD") or os.environ.get("MEDIAWIKI_PASSWORD", "") + + if not user: + sys.exit(f"Error: MEDIAWIKI_USER is not set in .env") + if not password: + sys.exit(f"Error: MEDIAWIKI_PASSWORD is not set in .env") + + parsed = urlparse(url) + host = parsed.netloc + # mwclient expects path to end with '/' + path = (parsed.path or "/") + "/" + path = path.replace("//", "/") + scheme = parsed.scheme or "https" + + site = mwclient.Site(host, path=path, scheme=scheme) + site.login(user, password) + return site + + +def cmd_fetch(args: argparse.Namespace) -> None: + site = get_site(args.wiki) + page = site.pages[args.title] + if not page.exists: + sys.exit(f"Error: page '{args.title}' does not exist") + print(page.text(), end="") + + +NAMESPACES = { + "main": 0, "talk": 1, "user": 2, "project": 4, + "file": 6, "template": 10, "category": 14, +} + + +def _strip_html(text: str) -> str: + """Remove HTML tags (used to clean search snippets).""" + import re + return re.sub(r"<[^>]+>", "", text) + + +def cmd_search(args: argparse.Namespace) -> None: + site = get_site(args.wiki) + ns_key = args.namespace + if ns_key is None or ns_key.lower() == "all": + ns = "*" + elif ns_key.isdigit(): + ns = int(ns_key) + else: + ns = NAMESPACES.get(ns_key.lower()) + if ns is None: + sys.exit(f"Error: unknown namespace '{ns_key}'. " + f"Known: all, {', '.join(NAMESPACES)}") + + result = site.api( + "query", list="search", + srsearch=args.query, + srnamespace=ns, + srlimit=50, + srprop="title|snippet", + ) + hits = result["query"]["search"] + if not hits: + print("(keine Ergebnisse)", file=sys.stderr) + for hit in hits: + print(hit["title"]) + if hit.get("snippet"): + print(f" {_strip_html(hit['snippet']).strip()}") + + +def cmd_render(args: argparse.Namespace) -> None: + site = get_site(args.wiki) + result = site.api("parse", page=args.title, prop="text") + print(result["parse"]["text"]["*"], end="") + + +def cmd_upload(args: argparse.Namespace) -> None: + path = Path(args.file_path) + if not path.exists(): + sys.exit(f"Error: file not found: {path}") + site = get_site(args.wiki) + with path.open("rb") as fh: + site.upload(fh, filename=args.wiki_filename, + description=args.description or "", ignore=True) + print(f"Uploaded '{args.wiki_filename}'") + + +def cmd_update(args: argparse.Namespace) -> None: + if args.file == "-": + content = sys.stdin.read() + else: + try: + with open(args.file, "r", encoding="utf-8") as fh: + content = fh.read() + except OSError as exc: + sys.exit(f"Error reading file: {exc}") + + site = get_site(args.wiki) + page = site.pages[args.title] + page.save(content, summary=args.summary or "") + print(f"Saved '{args.title}'") + + +def main() -> None: + parser = argparse.ArgumentParser(description="MediaWiki page fetch/update tool") + parser.add_argument( + "--wiki", "-w", + choices=list(WIKI_ALIASES), + default="wiki", + help="Wiki-Instanz (Standard: wiki = wiki.inmedias.it)", + ) + sub = parser.add_subparsers(dest="command", required=True) + + fetch_p = sub.add_parser("fetch", help="Print page wikitext to stdout") + fetch_p.add_argument("title", help="Page title") + fetch_p.set_defaults(func=cmd_fetch) + + search_p = sub.add_parser("search", help="Search for pages by title") + search_p.add_argument("query", help="Search term") + search_p.add_argument("--namespace", "-n", default=None, + help="Namespace filter: all (default), main, template, category, file, ... or numeric ID") + search_p.set_defaults(func=cmd_search) + + render_p = sub.add_parser("render", help="Print rendered HTML of a page to stdout") + render_p.add_argument("title", help="Page title") + render_p.set_defaults(func=cmd_render) + + upload_p = sub.add_parser("upload", help="Upload a local file to the wiki file store") + upload_p.add_argument("file_path", help="Absolute path to the local file") + upload_p.add_argument("wiki_filename", help="Target filename on the wiki (without 'File:' prefix)") + upload_p.add_argument("--description", default="", help="Wikitext for the file description page") + upload_p.set_defaults(func=cmd_upload) + + update_p = sub.add_parser("update", help="Save wikitext to a page") + update_p.add_argument("title", help="Page title") + update_p.add_argument("file", help="File containing wikitext, or '-' for stdin") + update_p.add_argument("--summary", default="", help="Edit summary") + update_p.set_defaults(func=cmd_update) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main()