#!/bin/sh # SeibertGPT – Einrichtung für opencode (macOS). Quelle: https://gpt.seibert.ai/clients # Der Key wird nur lokal abgefragt und landet im Schlüsselbund, nie in einer Datei; gesendet wird er nur an das Gateway. set -eu say() { printf '%s\n' "$*"; } our_config() { cat <<'SGPT_CONFIG_EOF' { "$schema": "https://opencode.ai/config.json", "model": "seibertgpt/deepseek-v41-flash-fast", "small_model": "seibertgpt/deepseek-v41-flash-fast", "provider": { "seibertgpt": { "npm": "@ai-sdk/openai-compatible", "name": "SeibertGPT", "options": { "baseURL": "https://api.gpt.seibert.ai/v1", "apiKey": "{env:SEIBERTGPT_API_KEY}" }, "models": { "deepseek-v41-flash": { "name": "Standard mit Denkmodus", "tool_call": true, "attachment": true, "limit": { "context": 1000000, "output": 32000 } }, "deepseek-v41-flash-fast": { "name": "ohne Denkmodus – schnell", "tool_call": true, "attachment": true, "limit": { "context": 1000000, "output": 32000 } }, "deepseek-v41-flash-low": { "name": "Denkaufwand niedrig", "tool_call": true, "attachment": true, "limit": { "context": 1000000, "output": 32000 } }, "deepseek-v41-flash-high": { "name": "Denkaufwand hoch", "tool_call": true, "attachment": true, "limit": { "context": 1000000, "output": 32000 } }, "deepseek-v41-flash-max": { "name": "Denkaufwand maximal", "tool_call": true, "attachment": true, "limit": { "context": 1000000, "output": 32000 } } } } } } SGPT_CONFIG_EOF } merge_jxa() { cat <<'SGPT_JXA_EOF' ObjC.import("Foundation"); function run(argv) { var target = argv[0], ours = JSON.parse(argv[1]), merge = JSON.parse(argv[2]), absent = JSON.parse(argv[3]); var fm = $.NSFileManager.defaultManager; function write(path, obj) { var ok = $(JSON.stringify(obj, null, 2) + "\n").writeToFileAtomicallyEncodingError(path, true, $.NSUTF8StringEncoding, null); if (!ok) throw new Error("Konnte " + path + " nicht schreiben."); } function get(o, p) { for (var i = 0; i < p.length; i++) { if (o == null || typeof o !== "object") return undefined; o = o[p[i]]; } return o; } function set(o, p, v) { for (var i = 0; i < p.length - 1; i++) { if (typeof o[p[i]] !== "object" || o[p[i]] === null) o[p[i]] = {}; o = o[p[i]]; } o[p[p.length - 1]] = v; } if (!fm.fileExistsAtPath(target)) { write(target, ours); return "created"; } var text = null; try { text = $.NSString.stringWithContentsOfFileEncodingError(target, $.NSUTF8StringEncoding, null).js; } catch (e) { text = null; } if (typeof text === "string" && text.trim() === "") { write(target, ours); return "created"; } var existing; try { existing = JSON.parse(text); } catch (e) { existing = null; } if (existing === null || typeof existing !== "object" || Array.isArray(existing)) { write(target.replace(/\.jsonc?$/, "") + ".seibertgpt.json", ours); return "sidecar"; } var stamp = new Date().toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15); var backup = target + ".bak-" + stamp; if (!fm.copyItemAtPathToPathError(target, backup, null) && !fm.fileExistsAtPath(backup)) { throw new Error("Konnte keine Sicherung von " + target + " anlegen."); } merge.forEach(function (p) { var v = get(ours, p); if (v !== undefined) set(existing, p, v); }); absent.forEach(function (p) { var v = get(ours, p); if (v !== undefined && get(existing, p) === undefined) set(existing, p, v); }); write(target, existing); return "merged"; } SGPT_JXA_EOF } main() { say 'SeibertGPT: Einrichtung für opencode' if ! { command -v opencode >/dev/null 2>&1 || ls -d /Applications/*[Oo]pen[Cc]ode*.app >/dev/null 2>&1; }; then say 'opencode scheint nicht installiert zu sein. Download: https://opencode.ai/download' printf '%s' 'Trotzdem fortfahren? (j/N) '; read -r ans /dev/null || true' EXIT trap 'exit 130' INT TERM while :; do # Echo vor dem Prompt abschalten, sonst erscheint ein sofort eingefügter Key doch im Terminal. stty -echo /dev/null; then [ -s "$HOME/.zshrc" ] && [ -n "$(tail -c1 "$HOME/.zshrc")" ] && printf '\n' >> "$HOME/.zshrc" printf '%s\n' 'export SEIBERTGPT_API_KEY="$(security find-generic-password -ws seibertgpt 2>/dev/null)" # SeibertGPT' >> "$HOME/.zshrc" fi launchctl setenv SEIBERTGPT_API_KEY "$SGPT_KEY" # Nach einem Neustart setzt dieser LaunchAgent die Variable für Desktop-Apps erneut (aus dem Schlüsselbund). mkdir -p "$HOME/Library/LaunchAgents" cat > "$HOME/Library/LaunchAgents/tools.seibert.gpt-env.plist" <<'SGPT_PLIST_EOF' Labeltools.seibert.gpt-env ProgramArguments/bin/sh-claunchctl setenv SEIBERTGPT_API_KEY "$(security find-generic-password -ws seibertgpt)" RunAtLoad SGPT_PLIST_EOF launchctl unload "$HOME/Library/LaunchAgents/tools.seibert.gpt-env.plist" 2>/dev/null || true; launchctl load "$HOME/Library/LaunchAgents/tools.seibert.gpt-env.plist" 2>/dev/null || true say 'Umgebungsvariable SEIBERTGPT_API_KEY für Terminal (~/.zshrc) und Desktop-Apps eingerichtet.' SGPT_OC_MAJOR=$(opencode --version 2>/dev/null | sed -n 's/^[^0-9]*\([0-9][0-9]*\).*/\1/p' || true); if [ "${SGPT_OC_MAJOR:-0}" -ge 2 ] 2>/dev/null; then opencode service set env SEIBERTGPT_API_KEY "$SGPT_KEY" >/dev/null 2>&1 || true; fi TARGET="$TARGET_DIR/"'opencode.json' RESULT=$(osascript -l JavaScript -e "$(merge_jxa)" "$TARGET" "$(our_config)" '[["provider","seibertgpt"]]' '[["$schema"],["model"],["small_model"]]') || RESULT=error case "$RESULT" in created) say "Config angelegt: $TARGET" ;; merged) say "Config ergänzt: $TARGET (Sicherung daneben: *.bak-…)" ;; sidecar) say "Deine Config enthält Kommentare und wurde nicht verändert. Unsere liegt daneben als *.seibertgpt.json – übernimm den Block „seibertgpt“ von Hand." ;; *) say "Config konnte nicht geschrieben werden: $TARGET"; exit 1 ;; esac # Header über stdin (printf ist eingebaut): der Key erscheint nicht in der Prozessliste. CODE=$(printf 'Authorization: Bearer %s\n' "$SGPT_KEY" | curl -s --max-time 15 -o /dev/null -w '%{http_code}' -H @- 'https://api.gpt.seibert.ai/v1/models' || true) case "$CODE" in 200) say 'Verbindung ok.' ;; 401) say 'Der Key wurde abgelehnt – ist er gesperrt? Siehe https://gpt.seibert.ai/konto/keys' ;; 403) say 'Zugriff verweigert – IP-Bindung oder kein aktiver Plan. Siehe https://gpt.seibert.ai/konto/keys' ;; *) say "Gateway nicht erreichbar (HTTP $CODE). Einrichtung ist trotzdem fertig." ;; esac unset SGPT_KEY say 'Öffne ein neues Terminal-Fenster (oder starte die App neu) und wähl mit /models „SeibertGPT“.' say 'Fertig.' } main "$@"