Author SHA1 Message Date
m3ta-chiron bdeb27b775 feat: explain-permissions v1.3.0 — Erklärungen als Zitat-Block mit Zeilenstruktur
- SYSTEM_PROMPT_RULE: Erklärung als Markdown-Zitat-Block (>) mit fetten
  Labels, jeder Punkt (WAS passiert/WELCHE FOLGEN/WIE RISKANT) auf eigener
  Zeile — bessere Lesbarkeit + visuelle Hervorhebung in TUI und Desktop
  (beide rendern Blockquotes mit farbigem Balken; opencode 1.18.30 bietet
  keinen Anzeige-Transform-Hook, experimental.chat.messages.transform
  wirkt nur modellseitig — verifiziert)
- Desktop-Notifications: neuer plainText()-Filter streicht Markdown-Marker
  (>, **) aus dem Toast-Text
- EXPLAIN_FIRST_ERROR verweist auf das neue Block-Format
- READMEs: Zweck + Versionierung ergänzt, Root-Tabelle auf v1.3.0
2026-09-18 10:06:33 +02:00
m3ta-chiron 53b3eb651b feat: explain-permissions v1.2.0 — Cross-Platform-Notifications (az-fleet-wla)
- notify() mit Plattform-Zweigen: Linux notify-send (unverändert),
  macOS osascript, Windows PowerShell-Toast via WinRT + PowerShell-AUMID
  (keine App-Registrierung nötig); Titel/Text über env des Child-Prozesses
  (injektionssicher), XML-Escape via SecurityElement
- fail-soft Spawn in Node UND Bun (try/catch + error-Listener decken beide
  ENOENT-Semantiken ab) — fehlendes Binary/headless = stiller No-Op
- Log-Pfad-Entscheidung: Windows %LOCALAPPDATA%\opencode\ (idiomatisch,
  roamt nicht, Fallback ~\AppData\Local), sonst XDG-State unverändert;
  dokumentiert im README
- Live verifiziert: Linux permission.asked -> ask/ask.explained/replied im
  Audit-Log + Toast; Fail-Soft win32/darwin-Zweige in node+bun ohne Crash
2026-09-18 09:12:17 +02:00
3 changed files with 156 additions and 26 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ Das Repos ist **anonym lesbar** (kein Token für Pulls nötig), Schreiben läuft
| Plugin | Zweck | Version | | Plugin | Zweck | Version |
|---|---|---| |---|---|---|
| [`explain-permissions/`](explain-permissions/) | Erklärt jeden freigabepflichtigen Tool-Call laienverständlich (Was/Folgen/Risiko), Desktop-Notification + Audit-Log | v1.1.0 | | [`explain-permissions/`](explain-permissions/) | Erklärt jeden freigabepflichtigen Tool-Call laienverständlich (Was/Folgen/Risiko) als hervorgehobener Zitat-Block, Desktop-Notification + Audit-Log | v1.3.0 |
## Struktur & Konventionen ## Struktur & Konventionen
+46 -2
View File
@@ -20,7 +20,12 @@ Menschen ohne Computer-Kenntnisse. Das Plugin:
1. **Injiziert die Freigabe-Regel in den System-Prompt** (Was / Welche Folgen / 1. **Injiziert die Freigabe-Regel in den System-Prompt** (Was / Welche Folgen /
Wie riskant, auf Deutsch, vor jedem freigabepflichtigen Call) — inklusive Wie riskant, auf Deutsch, vor jedem freigabepflichtigen Call) — inklusive
der konkreten ask-/allow-Muster aus der opencode-Konfiguration. der konkreten ask-/allow-Muster aus der opencode-Konfiguration. Seit v1.3.0
mit festem Ausgabeformat: Zitat-Block (`>`) mit fetten Labels, jeder Punkt
auf eigener Zeile. TUI und OpenCode Desktop rendern das als abgesetzten
Kasten mit farbigem Balken — die Erklärung hebt sich damit klar vom restlichen
Chatverlauf ab, und die drei Punkte bleiben sauber untereinander lesbar.
Desktop-Notifications bekommen reinen Text (Markdown-Marker werden entfernt).
2. **Schickt eine Desktop-Notification**, wenn der Freigabe-Dialog erscheint — 2. **Schickt eine Desktop-Notification**, wenn der Freigabe-Dialog erscheint —
mit der deutschen Erklärung des Modells, Fallback: der Roh-Befehl. mit der deutschen Erklärung des Modells, Fallback: der Roh-Befehl.
3. **Schreibt ein JSONL-Audit-Log**: jeden Freigabe-Request (`ask`), die 3. **Schreibt ein JSONL-Audit-Log**: jeden Freigabe-Request (`ask`), die
@@ -54,12 +59,40 @@ Pin auf ein Tag (z. B. `explain-permissions/v1.1.0`) oder `main`; anonymes
| `OPENCODE_EXPLAIN_INJECT=0` | System-Prompt-Regel abschalten | an | | `OPENCODE_EXPLAIN_INJECT=0` | System-Prompt-Regel abschalten | an |
| `OPENCODE_EXPLAIN_ENFORCE=1` | Hartes Erzwingen der Erklärung aktivieren (Call ohne vorherige Erklärung → Fehler) | aus | | `OPENCODE_EXPLAIN_ENFORCE=1` | Hartes Erzwingen der Erklärung aktivieren (Call ohne vorherige Erklärung → Fehler) | aus |
| `OPENCODE_EXPLAIN_DEBUG=1` | Permission-Events ins Log schreiben | aus | | `OPENCODE_EXPLAIN_DEBUG=1` | Permission-Events ins Log schreiben | aus |
| `OPENCODE_EXPLAIN_LOG_PATH=…` | Anderer Log-Pfad | `~/.local/state/opencode/explain-permissions.jsonl` | | `OPENCODE_EXPLAIN_LOG_PATH=…` | Anderer Log-Pfad | plattformabhängig (siehe unten) |
## Notifications je Plattform (v1.2.0)
| Plattform | Mechanismus | Anmerkung |
|---|---|---|
| Linux | `notify-send` (libnotify) | wie bisher |
| macOS | `osascript display notification` | Boardmittel, keine Abhängigkeit |
| Windows | PowerShell-Toast (WinRT) via `-EncodedCommand` | AppUserModelID = PowerShell-AUMID — **keine App-Registrierung nötig**; Titel/Text laufen über env-Variablen des Child-Prozesses (injektionssicher), XML-Escaping in PowerShell |
Alle Zweige sind **fail-soft**: fehlendes Binary, headless Session oder
Spawn-Fehler sind stille No-Ops — kein Crash, kein Log-Müll. Node und Bun
behandeln ENOENT beim Spawn unterschiedlich (asynchrones `error`-Event vs.
synchroner Wurf); das Plugin deckt beide Semantiken ab (try/catch plus
error-Listener) und läuft damit in beiden Runtime-Welten.
## Log-Pfad-Entscheidung (v1.2.0)
| Plattform | Default-Pfad | Begründung |
|---|---|---|
| Windows | `%LOCALAPPDATA%\opencode\explain-permissions.jsonl` | idiomatisch, roamt nicht, liegt neben anderen App-States; Fallback `~\AppData\Local\…`, falls `LOCALAPPDATA` nicht gesetzt |
| Linux/macOS | `~/.local/state/opencode/explain-permissions.jsonl` | XDG-State-Konvention (unverändert seit v1.1.0) |
`~/.local/state` würde zwar überall funktionieren (auch auf Windows), aber
`%LOCALAPPDATA%` ist auf Windows der etablierte Ort für pro-Nutzer-State —
Pilotnutzer-Support findet Dateien dort, wo Windows sie erwartet.
## Audit-Log ansehen ## Audit-Log ansehen
```bash ```bash
# Linux/macOS
tail -f ~/.local/state/opencode/explain-permissions.jsonl | jq . tail -f ~/.local/state/opencode/explain-permissions.jsonl | jq .
# Windows (PowerShell)
Get-Content "$env:LOCALAPPDATA\opencode\explain-permissions.jsonl" -Wait
``` ```
Relevante Events: Relevante Events:
@@ -77,6 +110,17 @@ Chat-Session im laufenden Prozess reicht nicht. Der Audit-Log-Eintrag
## Versionierung ## Versionierung
- `explain-permissions/v1.3.0` — Erklärungs-Format überarbeitet: Zitat-Block
(`>`) mit fetten Labels (`**WAS passiert:**` usw.), jeder Punkt auf eigener
Zeile — bessere Lesbarkeit und visuelle Hervorhebung in TUI und Desktop
(beide rendern Blockquotes mit farbigem Balken; verifiziert gegen opencode
1.18.30, das keinen Anzeige-Transform-Hook bietet — `experimental.chat.messages.transform`
wirkt nur modellseitig). Desktop-Notifications erhalten reinen Text ohne
Markdown-Marker; `EXPLAIN_FIRST_ERROR` verweist auf das neue Format.
- `explain-permissions/v1.2.0` — Cross-Platform-Notifications (Linux
notify-send / macOS osascript / Windows PowerShell-Toast mit PowerShell-AUMID,
keine App-Registrierung), fail-soft Spawn in Node- und Bun-Runtimes,
Windows-Log-Pfad `%LOCALAPPDATA%\opencode\` (Entscheidung siehe oben).
- `explain-permissions/v1.1.0` — Baseline, exakt der Stand der lokalen - `explain-permissions/v1.1.0` — Baseline, exakt der Stand der lokalen
Solo-Datei vom 17.09.2026 (Inhaltsgleichheit per SHA-256 verifiziert). Solo-Datei vom 17.09.2026 (Inhaltsgleichheit per SHA-256 verifiziert).
- Fortlaufende Versionsnummer in `PLUGIN_VERSION` im Dateikopf; Änderungen - Fortlaufende Versionsnummer in `PLUGIN_VERSION` im Dateikopf; Änderungen
+109 -23
View File
@@ -1,5 +1,10 @@
// explain-permissions.js — opencode plugin // explain-permissions.js — opencode plugin
// //
// Kanonische Quelle: git.az-gruppe.com/AZ-Intec-GmbH/opencode-plugins
// (Tag explain-permissions/v1.3.0). Fleet-Bezug: az-fleet vendort diese
// Datei nach config/managed/plugins/ (Pin mit sha256 dort in
// roles/managed_config — Byte-Gleichheit ist Vertragsgrundlage).
//
// Zweck: Jeden berechtigungspflichtigen Tool-Call laienverständlich erklären — // Zweck: Jeden berechtigungspflichtigen Tool-Call laienverständlich erklären —
// WAS passiert, WELCHE FOLGEN, WIE RISKANT (Zielgruppe: nicht-technische Nutzer). // WAS passiert, WELCHE FOLGEN, WIE RISKANT (Zielgruppe: nicht-technische Nutzer).
// //
@@ -18,11 +23,23 @@
// 3. experimental.chat.system.transform: // 3. experimental.chat.system.transform:
// → injiziert die Regel "Erst laienverständlich erklären (Was/Folgen/Risiko), // → injiziert die Regel "Erst laienverständlich erklären (Was/Folgen/Risiko),
// dann der Call" in den System-Prompt — in jeder Session, auch in Worktrees // dann der Call" in den System-Prompt — in jeder Session, auch in Worktrees
// ohne eigenes AGENTS.md // ohne eigenes AGENTS.md. Seit v1.3.0 mit festem Ausgabeformat: Zitat-Block
// (>) mit fetten Labels, jeder Punkt auf eigener Zeile — TUI und Desktop
// rendern das als klar abgesetzten Kasten mit farbigem Balken.
// 4. Optional (env OPENCODE_EXPLAIN_ENFORCE=1): tool.execute.before wirft einen // 4. Optional (env OPENCODE_EXPLAIN_ENFORCE=1): tool.execute.before wirft einen
// Fehler, wenn das Modell einen berechtigungspflichtigen Call OHNE vorherige // Fehler, wenn das Modell einen berechtigungspflichtigen Call OHNE vorherige
// Text-Erklärung im selben Message-Turn absetzt. Default: AUS (Dialog-Schleifen). // Text-Erklärung im selben Message-Turn absetzt. Default: AUS (Dialog-Schleifen).
// //
// Notifications je Plattform (v1.2.0):
// Linux notify-send (libnotify)
// macOS osascript "display notification" (Boardmittel)
// Windows PowerShell-Toast via WinRT; AppUserModelID = PowerShell-AUMID
// (keine App-Registrierung nötig); Titel/Text über env-Variablen des
// Child-Prozesses (injektionssicher), XML-Escape in PowerShell
// Alle Zweige fail-soft: fehlendes Binary, headless Session oder Spawn-Fehler
// sind stille No-Ops (Node und Bun behandeln ENOENT verschieden — try/catch
// plus error-Listener decken beide Semantiken ab).
//
// Wichtig: Plugin-Änderungen greifen erst nach NEUSTART von opencode — eine neue // Wichtig: Plugin-Änderungen greifen erst nach NEUSTART von opencode — eine neue
// Chat-Session im laufenden Prozess reicht nicht. Der Audit-Log-Eintrag // Chat-Session im laufenden Prozess reicht nicht. Der Audit-Log-Eintrag
// "plugin-loaded" (mit Version) zeigt an, welche Version wann geladen wurde. // "plugin-loaded" (mit Version) zeigt an, welche Version wann geladen wurde.
@@ -34,7 +51,10 @@
// OPENCODE_EXPLAIN_ENFORCE=1 Hartes Erzwingen aktivieren (Default: aus) // OPENCODE_EXPLAIN_ENFORCE=1 Hartes Erzwingen aktivieren (Default: aus)
// OPENCODE_EXPLAIN_DEBUG=1 Permission-Events ins Log (Default: aus) // OPENCODE_EXPLAIN_DEBUG=1 Permission-Events ins Log (Default: aus)
// OPENCODE_EXPLAIN_LOG_PATH=… Anderer Log-Pfad // OPENCODE_EXPLAIN_LOG_PATH=… Anderer Log-Pfad
// (Default: ~/.local/state/opencode/explain-permissions.jsonl) // (Default Windows: %LOCALAPPDATA%\opencode\explain-permissions.jsonl —
// idiomatisch, roamt nicht; Fallback ~\AppData\Local, wenn LOCALAPPDATA
// nicht gesetzt. Entscheidung dokumentiert im README.)
// (Default sonst: ~/.local/state/opencode/explain-permissions.jsonl)
// //
// Log ansehen: // Log ansehen:
// tail -f ~/.local/state/opencode/explain-permissions.jsonl | jq . // tail -f ~/.local/state/opencode/explain-permissions.jsonl | jq .
@@ -45,33 +65,51 @@ import os from "node:os";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
const ENV = process.env; const ENV = process.env;
const PLUGIN_VERSION = "1.1.0"; const PLUGIN_VERSION = "1.3.0";
const NOTIFY = ENV.OPENCODE_EXPLAIN_NOTIFY !== "0"; const NOTIFY = ENV.OPENCODE_EXPLAIN_NOTIFY !== "0";
const LOG = ENV.OPENCODE_EXPLAIN_LOG !== "0"; const LOG = ENV.OPENCODE_EXPLAIN_LOG !== "0";
const INJECT = ENV.OPENCODE_EXPLAIN_INJECT !== "0"; const INJECT = ENV.OPENCODE_EXPLAIN_INJECT !== "0";
const ENFORCE = ENV.OPENCODE_EXPLAIN_ENFORCE === "1"; const ENFORCE = ENV.OPENCODE_EXPLAIN_ENFORCE === "1";
const DEBUG = ENV.OPENCODE_EXPLAIN_DEBUG === "1"; const DEBUG = ENV.OPENCODE_EXPLAIN_DEBUG === "1";
const LOG_PATH =
ENV.OPENCODE_EXPLAIN_LOG_PATH || // Windows: %LOCALAPPDATA% (idiomatisch, roamt nicht); sonst XDG-State im Profil.
path.join(os.homedir(), ".local", "state", "opencode", "explain-permissions.jsonl"); function defaultLogPath() {
if (process.platform === "win32") {
const base = ENV.LOCALAPPDATA || (os.homedir() + "\\AppData\\Local");
return base + "\\opencode\\explain-permissions.jsonl";
}
return path.join(os.homedir(), ".local", "state", "opencode", "explain-permissions.jsonl");
}
const LOG_PATH = ENV.OPENCODE_EXPLAIN_LOG_PATH || defaultLogPath();
const SYSTEM_PROMPT_RULE = `## Tool-Call-Erklärungen (verpflichtend, laienverständlich) const SYSTEM_PROMPT_RULE = `## Tool-Call-Erklärungen (verpflichtend, laienverständlich)
Vor JEDEM Tool-Call, der eine Freigabe erfordert, schreibe unmittelbar davor eine Erklärung auf Deutsch, die ein Mensch ohne Computer-Kenntnisse versteht. Struktur: Vor JEDEM Tool-Call, der eine Freigabe erfordert, schreibe unmittelbar davor eine Erklärung auf Deutsch, die ein Mensch ohne Computer-Kenntnisse versteht.
- WAS passiert: in Alltagssprache — nicht wie der Befehl heißt, sondern was er bewirkt
- WELCHE FOLGEN: Was ist danach anders? Ist es rückgängig machbar? Sind eigene Dateien gefährdet?
- WIE RISKANT: harmlos / wiederherstellbar / dauerhaft
Beispiel (gut): „Ich lösche jetzt den Ordner „foo" im temporären Bereich samt allem, was darin liegt. Das lässt sich nicht rückgängig machen — es betrifft aber nur diesen einen Ordner, deine anderen Dateien bleiben unberührt." Format — GENAU SO einhalten (dient der Lesbarkeit und Hebung im Chat):
Beispiel (schlecht, zu technisch): „Ich führe rm -rf /tmp/foo aus." - Die gesamte Erklärung als Zitat-Block: JEDE Zeile beginnt mit „> “
- Jeder der drei Punkte auf EIGENER Zeile — nie alle hintereinander in einer Zeile
- Die Labels fettgedruckt
> **WAS passiert:** in Alltagssprache — nicht wie der Befehl heißt, sondern was er bewirkt
> **WELCHE FOLGEN:** Was ist danach anders? Ist es rückgängig machbar? Sind eigene Dateien gefährdet?
> **WIE RISKANT:** harmlos / wiederherstellbar / dauerhaft
Beispiel (gut):
> **WAS passiert:** Ich lege eine neue, leere Datei namens „Neue Textdatei.txt“ im Ordner „Documents\\Default Project“ an.
> **WELCHE FOLGEN:** Es gibt danach eine zusätzliche Datei im Arbeitsordner, die du jederzeit löschen kannst. Nichts Bestehendes wird verändert.
> **WIE RISKANT:** harmlos.
Beispiel (schlecht, zu technisch): „Ich führe rm -rf /tmp/foo aus.“
Regeln: Regeln:
- Der genaue Befehl darf NACH der Erklärung zusätzlich genannt werden, die Erklärung muss aber auch ohne ihn verständlich sein - Der genaue Befehl darf NACH dem Erklärungs-Block zusätzlich genannt werden, die Erklärung muss aber auch ohne ihn verständlich sein
- Kein Fachjargon (rm, rekursiv, force, Pipe, Exit-Code) ohne Umschreibung - Kein Fachjargon (rm, rekursiv, force, Pipe, Exit-Code) ohne Umschreibung
- Erst die Erklärung, dann der Call — niemals umgekehrt - Erst die Erklärung, dann der Call — niemals umgekehrt
- Rein lesende, automatisch erlaubte Standard-Calls brauchen keine Extra-Erklärung - Rein lesende, automatisch erlaubte Standard-Calls brauchen keine Extra-Erklärung
- Im Zweifel, ob ein Call eine Freigabe braucht: immer erklären`; - Im Zweifel, ob ein Call eine Freigabe braucht: immer erklären`;
const EXPLAIN_FIRST_ERROR = `EXPLAIN_FIRST: Du hast den Tool-Call ohne vorherige Erklärung abgesetzt. Schreibe zuerst 1-2 Sätze auf Deutsch, laienverständlich (kein Fachjargon): Was macht der Call, welche Folgen hat er, wie riskant ist er? Führe den Call danach erneut aus.`; const EXPLAIN_FIRST_ERROR = `EXPLAIN_FIRST: Du hast den Tool-Call ohne vorherige Erklärung abgesetzt. Schreibe zuerst eine Erklärung auf Deutsch, laienverständlich (kein Fachjargon) — als Zitat-Block („> “), jeder Punkt auf eigener Zeile: **WAS passiert:** / **WELCHE FOLGEN:** / **WIE RISKANT:**. Führe den Call danach erneut aus.`;
const PERMISSION_KEYS = [ const PERMISSION_KEYS = [
"bash", "bash",
@@ -118,20 +156,68 @@ function appendLog(entry) {
} }
} }
function notify(title, body) { // Detached Spawn, fail-soft in Node UND Bun: ENOENT wirft/emittiert in den
if (!NOTIFY) return; // beiden Runtimes unterschiedlich (Node: asynchrones 'error'-Event, Bun kann
// synchron werfen) — try/catch plus error-Listener decken beide Semantiken ab.
// stdio 'ignore' + unref: der Agent-Prozess wartet nie auf das Notification-
// Binary; fehlendes Binary/headless = stiller No-Op.
function runDetached(cmd, args, env) {
try { try {
const child = spawn( const child = spawn(cmd, args, { detached: true, stdio: "ignore", env: { ...ENV, ...env } });
"notify-send", child.on("error", () => {});
["-a", "opencode", "-i", "utilities-terminal", "-u", "normal", title, body.slice(0, 400)],
{ detached: true, stdio: "ignore" },
);
child.unref(); child.unref();
} catch { } catch {
// keine Desktop-Session (headless) → egal // fehlendes Binary oder Spawn-Verweigerung → stiller No-Op
} }
} }
// PowerShell-AppUserModelID (Shell-Folder-GUID von Windows PowerShell): Toasts
// erscheinen unter dem Namen "Windows PowerShell", ohne dass eine eigene App
// registriert werden muss (AUMID-Standardtrick, kein Shortcut, keine Registry).
const TOAST_AUMID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe";
// Titel/Text erreichen PowerShell über env-Variablen des Child-Prozesses —
// kein String-Interpolieren in den Skript-Body (injektionssicher); XML-Escape
// macht [SecurityElement]::Escape auf der Windows-Seite.
const TOAST_PS = [
"$ErrorActionPreference = 'SilentlyContinue'",
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null",
"$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
"$t = [System.Security.SecurityElement]::Escape($env:OC_TOAST_TITLE)",
"$b = [System.Security.SecurityElement]::Escape($env:OC_TOAST_BODY)",
"$xml.LoadXml('<toast><visual><binding template=\"ToastGeneric\"><text>' + $t + '</text><text>' + $b + '</text></binding></visual></toast>')",
"$toast = New-Object Windows.UI.Notifications.ToastNotification $xml",
"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('" + TOAST_AUMID + "').Show($toast)",
].join("\n");
function windowsToast(title, body) {
const encoded = Buffer.from(TOAST_PS, "utf16le").toString("base64");
runDetached(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-EncodedCommand", encoded],
{ OC_TOAST_TITLE: title, OC_TOAST_BODY: body },
);
}
function macosNotify(title, body) {
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
runDetached("osascript", ["-e", `display notification "${esc(body)}" with title "${esc(title)}"`]);
}
function notify(title, body) {
if (!NOTIFY) return;
const b = body.slice(0, 400);
if (process.platform === "win32") windowsToast(title, b);
else if (process.platform === "darwin") macosNotify(title, b);
else runDetached("notify-send", ["-a", "opencode", "-i", "utilities-terminal", "-u", "normal", title, b]);
}
// Desktop-Notifications render kein Markdown: Zitat-Marker (>) und Fett-
// Marker (**) aus der Erklärung entfernen, bevor sie in den Toast geht.
function plainText(s) {
return s.replace(/^[ \t]*>[ \t]?/gm, "").replace(/\*\*/g, "");
}
function debugLog(source, payload) { function debugLog(source, payload) {
if (!DEBUG) return; if (!DEBUG) return;
let serialized; let serialized;
@@ -308,7 +394,7 @@ async function explainAndNotify(client, p) {
} }
const body = explanation const body = explanation
? `${explanation}\n—\n${type}: ${rawCall}\n→ im opencode-UI antworten` ? `${plainText(explanation)}\n—\n${type}: ${rawCall}\n→ im opencode-UI antworten`
: `${rawCall}\n→ im opencode-UI antworten`; : `${rawCall}\n→ im opencode-UI antworten`;
notify(`opencode · Freigabe nötig (${type})`, body); notify(`opencode · Freigabe nötig (${type})`, body);
} }