#!/bin/bash # Abbrechen bei Fehlern set -e echo "====================================================" echo "🚀 Starte J.A.R.V.I.S. Desktop OS - Pure Local Setup" echo "====================================================" # Benutzererkennung REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || whoami)}" REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6) if [ -z "$REAL_USER" ] || [ "$REAL_USER" = "root" ]; then REAL_USER=$(id -nu 1000 2>/dev/null || echo "meik") REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6) fi JARVIS_DIR="$REAL_HOME/jarvis-ai" # 1. System aktualisieren & Basispakete installieren echo "📦 Aktualisiere Paketquellen und installiere Systemkomponenten..." sudo apt update sudo apt install -y labwc firefox-esr curl wget git sudo python3 python3-pip python3-venv original-awk tilix geany waybar wlr-randr fonts-noto-color-emoji wofi pipewire pipewire-audio-client-libraries pipewire-pulse wireplumber alsa-utils # 1.1 Gruppenrechte für Grafik und Eingabe echo "👥 Füge Benutzer '$REAL_USER' zu den Grafik- und Input-Gruppen hinzu..." sudo usermod -aG video,render,input "$REAL_USER" systemctl --user --now enable pipewire.service pipewire-pulse.service wireplumber.service # 2. Architektur erkennen und wdotool installieren ARCH=$(uname -m) echo "🔍 Erkannte Systemarchitektur: $ARCH" if [ "$ARCH" = "x86_64" ]; then wget https://github.com/cushycush/wdotool/releases/download/v0.5.3/wdotool_0.5.3-1_amd64.deb -O /tmp/wdotool.deb sudo apt install -y /tmp/wdotool.deb rm /tmp/wdotool.deb elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cushycush/wdotool/releases/download/v0.5.3/wdotool-installer.sh | sh else echo "❌ Unbekannte Architektur: $ARCH. Installation abgebrochen." exit 1 fi # 3. Den realen Installationspfad von wdotool ermitteln echo "🛠️ Konfiguriere globalen wdotool-Wrapper..." if [ -f "/usr/bin/wdotool" ]; then REAL_WDOTOOL="/usr/bin/wdotool" elif [ -f "$REAL_HOME/.cargo/bin/wdotool" ]; then REAL_WDOTOOL="$REAL_HOME/.cargo/bin/wdotool" elif [ -f "$REAL_HOME/.local/bin/wdotool" ]; then REAL_WDOTOOL="$REAL_HOME/.local/bin/wdotool" else REAL_WDOTOOL=$(which wdotool || true); fi if [ -z "$REAL_WDOTOOL" ]; then echo "❌ Fehler: wdotool nicht gefunden!"; exit 1; fi sudo mv "$REAL_WDOTOOL" /usr/local/bin/wdotool.real # 4.1 wdotool Wrapper erstellen sudo cat << 'EOF' | sudo tee /usr/local/bin/wdotool > /dev/null #!/bin/bash export XDG_RUNTIME_DIR=/run/user/$(id -u) export WAYLAND_DISPLAY=wayland-0 exec /usr/local/bin/wdotool.real "$@" EOF sudo chmod +x /usr/local/bin/wdotool # 4.2 wdotool Wrapper erstellen sudo cat << 'EOF' | sudo tee /usr/local/bin/jwin > /dev/null #!/bin/bash ACTION=$1 APP_NAME=$2 if [ -z "$ACTION" ] || [ -z "$APP_NAME" ]; then echo "❌ Fehler: Falsche Syntax." echo "Nutzung: jwin [Parameter]" exit 1 fi # ========================================================= # 1. SONDERFALL: PROGRAMM STARTEN (Mit Display-Erkennung) # ========================================================= if [ "$ACTION" == "start" ]; then shift # Falls WAYLAND_DISPLAY nicht gesetzt ist (z.B. im JARVIS-Dienst), # versuchen wir es automatisch zu erraten (meistens wayland-0) if [ -z "$WAYLAND_DISPLAY" ]; then export WAYLAND_DISPLAY=$(ls /run/user/$(id -u)/wayland-* 2>/dev/null | head -n 1 | xargs basename) # Fallback auf Standard, falls obiges leer ist [ -z "$WAYLAND_DISPLAY" ] && export WAYLAND_DISPLAY="wayland-0" fi # Sicherheits-Fallback für ältere X11/XWayland Apps wie Geany if [ -z "$DISPLAY" ]; then export DISPLAY=":0" fi # Das Programm mit den exportierten Variablen starten nohup "$@" >/dev/null 2>&1 & echo "✅ Programm '$APP_NAME' wurde entkoppelt im Hintergrund gestartet (Display: $WAYLAND_DISPLAY)." exit 0 fi # ========================================================= # 2. WAYLAND AKTIONEN (Fokus-optimiert) # ========================================================= PARAM1=$3 WD="wdotool --backend wlr-protocols" if [ "$APP_NAME" == "active" ]; then # Absolut kugelsicher: Nimm einfach das Fenster, das GERADE aktiv ist WINDOW_ID=$($WD getactivewindow 2>/dev/null | awk '{print $1}') else # Normale Suche für gezielte Befehle (wie "schließe geany") WINDOW_LINE=$($WD search --ignore-case --any --name "$APP_NAME" --class "$APP_NAME" 2>/dev/null | head -n 1) if [ -z "$WINDOW_LINE" ]; then SAFE_APP_NAME=$(echo "$APP_NAME" | sed 's/[.[\*^$]/\\&/g') WINDOW_LINE=$($WD search --ignore-case --regex --any --name "$SAFE_APP_NAME" --class "$SAFE_APP_NAME" 2>/dev/null | head -n 1) fi WINDOW_ID=$(echo "$WINDOW_LINE" | awk '{print $1}') fi if [ -z "$WINDOW_ID" ]; then echo "❌ Kein aktives oder passendes Fenster gefunden." exit 1 fi case "$ACTION" in activate) $WD windowactivate "$WINDOW_ID" echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) ist jetzt im Fokus." ;; close) $WD windowclose "$WINDOW_ID" echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) wurde geschlossen." ;; maximize) # 1. Fenster aktivieren $WD windowactivate "$WINDOW_ID" sleep 0.2 # 2. Labwc Vollbild-Shortcut senden (oft Super+Up oder Alt+F11, passe das an deine rc.xml an!) $WD key super+up echo "✅ Fenster '$APP_NAME' maximiert." ;; snap) if [ -z "$PARAM1" ]; then echo "❌ Fehler: Für 'snap' wird eine Richtung (left, right, up, down) benötigt." exit 1 fi # 1. Fenster MUSS zuerst fokussiert werden (Wayland-Sicherheitsregel für Input) $WD windowactivate "$WINDOW_ID" sleep 0.2 # Kurze Pause, damit der Fokus greift # 2. Sende die Tastenkombination an labwc zum Andocken case "$PARAM1" in left) $WD key super+Left ;; right) $WD key super+Right ;; up) $WD key super+Up ;; down) $WD key super+Down ;; # --- ECKEN (Super + Shift + Pfeiltasten für maximale Kompatibilität) --- top-left) $WD key super+shift+Left ;; top-right) $WD key super+shift+Up ;; bottom-right) $WD key super+shift+Down ;; bottom-left) $WD key super+shift+Right ;; *) echo "❌ Unbekannte Richtung. Nutze left, right, up, down." ; exit 1 ;; esac if [ $? -eq 0 ]; then echo "✅ Fenster '$APP_NAME' nach $PARAM1 angedockt." else echo "❌ Fehler beim Senden der Tastenkombination an wdotool." exit 1 fi ;; *) echo "❌ Unbekannte Aktion: $ACTION. Erlaubt sind: start, activate, close, snap, maximize." exit 1 ;; esac EOF sudo chmod +x /usr/local/bin/jwin # 5. Desktop-Konfiguration (labwc & environment) echo "📂 Konfiguriere labwcAutostart und Tastaturlayout..." mkdir -p "$REAL_HOME/.config/labwc" cat << 'EOF' > "$REAL_HOME/.config/labwc/environment" dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=labwc XKB_DEFAULT_LAYOUT=de EOF # 6. Auto-Start in .profile eintragen echo "⚙️ Richte .profile für Wayland Auto-Start ein..." if ! grep -q "labwc" "$REAL_HOME/.profile"; then cat << 'EOF' >> "$REAL_HOME/.profile" if [ "$(tty)" = "/dev/tty1" ]; then export WLR_RENDERER=pixman export WLR_NO_HARDWARE_CURSORS=1 export LIBGL_ALWAYS_SOFTWARE=1 dbus-run-session labwc > ~/labwc.log 2>&1 logout fi EOF fi # 7. Waybar & Wofi Powermenu echo "📊 Konfiguriere Waybar und Wofi Power-Menü..." mkdir -p "$REAL_HOME/.config/waybar" cat << 'EOF' > "$REAL_HOME/.config/waybar/config" { "layer": "top", "position": "bottom", "height": 34, "modules-left": ["custom/terminal", "custom/browser", "wlr/taskbar"], "modules-right": ["clock", "custom/logout"], "custom/terminal": { "format": "📁 Term", "on-click": "tilix", "tooltip": false }, "custom/browser": { "format": "🌐 Web", "on-click": "MOZ_WEBRENDER=software MOZ_ENABLE_WAYLAND=1 firefox-esr", "tooltip": false }, "wlr/taskbar": { "format": "{icon}", "icon-size": 16, "on-click": "activate", "on-click-right": "minimize" }, "clock": { "format": "🕒 {:%H:%M:%S}", "interval": 1 }, "custom/logout": { "format": "⚙️ System ", "on-click": "~/.config/labwc/powermenu.sh", "tooltip": false } } EOF cat << 'EOF' > "$REAL_HOME/.config/waybar/style.css" * { font-family: sans-serif; font-size: 12px; } window#waybar { background-color: #1e293b; color: white; border-top: 1px solid #334155; } #custom-terminal, #custom-browser { padding: 0 8px; background: #334155; margin: 3px 2px; border-radius: 3px; } #custom-terminal:hover, #custom-browser:hover { background: #475569; } #taskbar button { padding: 0 10px; color: #94a3b8; } #taskbar button.active { background-color: #0f172a; color: #38bdf8; } #clock { padding: 0 15px; background-color: #0f172a; } #custom-logout { padding: 0 12px; background-color: #ef4444; color: white; } #custom-logout:hover { background-color: #dc2626; } EOF cat << 'EOF' > "$REAL_HOME/.config/labwc/powermenu.sh" #!/bin/bash OPTIONS="🚪 Abmelden\n🔄 Neu starten\n🛑 Herunterfahren" CHOICE=$(echo -e "$OPTIONS" | wofi --dmenu --prompt "Systemaktion wählen:" --width 280 --height 180 --style "$HOME/.config/labwc/wofi-power.css") case "$CHOICE" in *"Abmelden") labwc --exit ;; *"Neu starten") sudo systemctl reboot ;; *"Herunterfahren") sudo systemctl poweroff ;; esac EOF chmod +x "$REAL_HOME/.config/labwc/powermenu.sh" cat << 'EOF' > "$REAL_HOME/.config/labwc/wofi-power.css" window { background-color: #1e293b; color: white; border: 2px solid #334155; border-radius: 8px; font-family: sans-serif; } #entry { padding: 8px; color: white; } #entry:selected { background-color: #334155; color: #38bdf8; } #input { background-color: #0f172a; color: white; border: 1px solid #334155; margin: 5px; } EOF echo "%sudo ALL=(ALL) NOPASSWD: /usr/bin/systemctl poweroff, /usr/bin/systemctl reboot" | sudo tee /etc/sudoers.d/powermenu > /dev/null sudo chmod 0440 /etc/sudoers.d/powermenu # =================================================================== # 8. J.A.R.V.I.S. REIN LOKALE INSTALLATION # =================================================================== echo "🧠 Installiere J.A.R.V.I.S. AI Desktop Backend..." mkdir -p "$JARVIS_DIR"/{config,workspace} # J.A.R.V.I.S. Requirements cat << 'EOF' > "$JARVIS_DIR/requirements.txt" openai google-genai python-dotenv EOF # J.A.R.V.I.S. .env Template cat << 'EOF' > "$JARVIS_DIR/config/.env" WEB_USER_NAME=$REAL_USER AI_PROVIDER=nvdia OPENAI_API_KEY=dein-openai-key OPENAI_MODEL= GOOGLE_API_KEY=dein-google-key NVIDIA_API_KEY=dein-nvidia-key NVIDIA_MODEL=moonshotai/kimi-k2.6 GOOGLE_MODEL=gemini-2.5-flash OLLAMA_BASE_URL=http://127.0.0.1:11434/v1 OLLAMA_MODEL=llama3 GROQ_API_KEY=dein-groq-key GROQ_MODEL=groq/compound EOF # J.A.R.V.I.S. Optimierter System-Prompt inkl. wdotool-Handbuch cat << 'EOF' > "$JARVIS_DIR/config/system_prompt.txt" Du bist J.A.R.V.I.S., ein KI-Systemassistent, der direkt auf einem Debian Wayland-Desktop läuft. Du hast vollen lokalen Zugriff auf das System. UMGEBUNG & GEDÄCHTNIS: - Arbeitsverzeichnis: {workspace_dir} - Notizen: {notes_file} - Todos: {todo_file} DESKTOP STEUERUNG & FENSTER-MANAGEMENT: Du steuerst die grafische Oberfläche (Wayland/labwc) über Befehlszeilen-Tools. 1. Programme & Fenster verwalten (Tool: jwin) Du kannst Fenster steuern, indem du das jwin-Skript aufrufst. Folgende Aktionen sind erlaubt: - start (Startet ein Programm) - activate (Holt ein Fenster in den Vordergrund) - close (Schließt das Fenster) - snap (Dockt das Fenster an. Parameter: left, right, up, down, top-left, bottom-left, top-right, bottom-right) - maximize (Maximiert das Fenster) Beispiele: jwin snap firefox right (Schiebt Firefox auf die rechte Bildschirmhälfte) jwin snap firefox bottom-right (Schiebt Firefox auf die rechte untere Bildschirmviertel) jwin activate terminal (Holt das Terminal in den Vordergrund) Regel für Multitasking-Fenster: Wenn der User mehrere Instanzen derselben App starten und verteilen möchte, führe die Befehle immer abwechselnd aus und nutze das Schlüsselwort active für das Snapping: Beispiel-Verkettung für das System: jwin start firefox && sleep 1.5 && jwin snap active top-left && jwin start firefox && sleep 1.5 && jwin snap active top-right 2. Tastatur & Maus (Tool: wdotool) - Tastatur: wdotool key ctrl+l, wdotool key alt+Tab, wdotool type "Hallo" - Maus: wdotool mousemove 500 400 (absolut), wdotool click 1 (1=links, 3=rechts) 3. System- & Display-Infos - Bildschirmauflösung ermitteln: wlr-randr | grep current | awk '{print $1}' WICHTIGE REGELN FÜR DIE AUSFÜHRUNG: 1. Wenn du eine Aktion ausführst, MUSST du den Linux-Befehl EXAKT in und Tags setzen. 2. VERBOTEN: Verwende NIEMALS Markdown-Codeblöcke (```) um oder in den -Tags! Schreibe die Tags als simplen, rohen Text. 3. ERZWUNGEN: Sag nicht nur, dass du etwas tust – du MUSST den Tag in deiner Antwort mitsenden, sonst passiert nichts! Beispiel für einen perfekten Workflow: Das mache ich sofort für dich! jwin activate Firefox && sleep 1 && jwin move Firefox 0 0 Schreibe immer eine kurze Textantwort dazu, was du gerade tust. Du duzt {user_name} konsequent, dein Tonfall ist locker und technisch versiert. EOF # J.A.R.V.I.S. Python Backend (Kein SQLite, schlanker Regex) cat << 'EOF' > "$JARVIS_DIR/jarvis.py" import os import re import sqlite3 import asyncio import openai from google import genai from google.genai import types from datetime import datetime from pathlib import Path from dotenv import load_dotenv from colorama import init, Fore, Style # ==================================================== # INITIALISIERUNG # ==================================================== init(autoreset=True) # ==================================================== # PFADE & SETUP # ==================================================== BASE_DIR = Path(__file__).resolve().parent CONFIG_DIR = BASE_DIR / "config" DATA_DIR = BASE_DIR / "data" WORKSPACE_DIR = BASE_DIR / "workspace" ENV_FILE = CONFIG_DIR / ".env" load_dotenv(ENV_FILE) DB_PATH = DATA_DIR / "cluster.db" NOTES_FILE = WORKSPACE_DIR / "NOTIZEN.md" TODO_FILE = WORKSPACE_DIR / "TODO.md" CHAT_LOG_FILE = WORKSPACE_DIR / "chat_history.log" WEB_USER_NAME = os.getenv("WEB_USER_NAME", "Meik") # ==================================================== # TERMINAL FARBEN # ==================================================== USER_COLOR = Fore.CYAN JARVIS_COLOR = Fore.GREEN SYSTEM_COLOR = Fore.YELLOW ERROR_COLOR = Fore.RED OUTPUT_COLOR = Fore.MAGENTA INFO_COLOR = Fore.BLUE RESET = Style.RESET_ALL # ==================================================== # ORDNER & DATEIEN # ==================================================== for d in [WORKSPACE_DIR, DATA_DIR, CONFIG_DIR]: d.mkdir(parents=True, exist_ok=True) for f in [NOTES_FILE, TODO_FILE]: if not f.exists(): f.write_text( f"# {f.name}\nHier fängt dein Gedächtnis an, J.A.R.V.I.S.\n", encoding="utf-8" ) # ==================================================== # KI KONFIGURATION # ==================================================== AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower() OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "") GROQ_API_KEY = os.getenv("GROQ_API_KEY", "") OLLAMA_BASE_URL = os.getenv( "OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1" ) GOOGLE_MODEL = os.getenv( "GOOGLE_MODEL", "gemini-2.5-flash" ) OPENAI_MODEL = os.getenv( "OPENAI_MODEL", "gpt-4o" ) OLLAMA_MODEL = os.getenv( "OLLAMA_MODEL", "llama3" ) NVIDIA_MODEL = os.getenv( "NVIDIA_MODEL", "moonshotai/kimi-k2.6" ) GROQ_MODEL = os.getenv( "GROQ_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct" ) # ==================================================== # DATENBANK # ==================================================== def init_db(): conn = sqlite3.connect(DB_PATH) conn.execute(''' CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, ip TEXT UNIQUE, user TEXT, sudo_password TEXT, os TEXT DEFAULT 'Unbekannt', arch TEXT DEFAULT 'Unbekannt', docker_installed INTEGER DEFAULT 0, status TEXT ) ''') conn.commit() conn.close() init_db() def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn # ==================================================== # SYSTEM PROMPT # ==================================================== def get_system_prompt(): prompt_path = CONFIG_DIR / "system_prompt.txt" prompt = ( prompt_path.read_text(encoding="utf-8") if prompt_path.exists() else f"Hallo {WEB_USER_NAME}, ich bin J.A.R.V.I.S." ) prompt = prompt.replace("{user_name}", WEB_USER_NAME) prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR)) prompt = prompt.replace("{notes_file}", str(NOTES_FILE)) prompt = prompt.replace("{todo_file}", str(TODO_FILE)) conn = get_db() nodes = conn.execute( 'SELECT * FROM nodes' ).fetchall() conn.close() node_info = "" for n in nodes: node_info += ( f"- Name: {n['name']}, " f"IP: {n['ip']}, " f"User: {n['user']}\n" ) return prompt.replace("{node_info}", node_info) # ==================================================== # KI KOMMUNIKATION # ==================================================== async def get_ai_response(user_msg, system_prompt, history_list): try: if AI_PROVIDER in ["openai", "ollama", "nvidia", "groq"]: # ======================================== # PAYLOAD WASCHEN (Für strikte APIs wie Groq) # ======================================== clean_history = [ { "role": msg["role"], "content": msg["content"] } for msg in history_list ] messages = [ { "role": "system", "content": system_prompt } ] + clean_history if AI_PROVIDER == "ollama": url = ( OLLAMA_BASE_URL if OLLAMA_BASE_URL.endswith('/v1') else OLLAMA_BASE_URL.rstrip('/') + '/v1' ) key = "ollama" model_to_use = OLLAMA_MODEL elif AI_PROVIDER == "nvidia": url = "https://integrate.api.nvidia.com/v1" key = NVIDIA_API_KEY model_to_use = NVIDIA_MODEL elif AI_PROVIDER == "groq": url = "https://api.groq.com/openai/v1" key = GROQ_API_KEY model_to_use = GROQ_MODEL else: url = None key = OPENAI_API_KEY model_to_use = OPENAI_MODEL client = openai.AsyncOpenAI( base_url=url, api_key=key ) response = await client.chat.completions.create( model=model_to_use, messages=messages ) return response.choices[0].message.content elif AI_PROVIDER == "google": client = genai.Client(api_key=GOOGLE_API_KEY) google_history = [ types.Content( role="user" if msg["role"] == "user" else "model", parts=[ types.Part.from_text( text=msg["content"] ) ] ) for msg in history_list[:-1] ] chat = client.chats.create( model=GOOGLE_MODEL, config=types.GenerateContentConfig( system_instruction=system_prompt ), history=google_history ) return chat.send_message(user_msg).text except Exception as e: return f"Fehler bei der KI-Anfrage: {e}" # ==================================================== # BEFEHLSAUSFÜHRUNG # ==================================================== async def run_task(target, cmd): print( f"\n{SYSTEM_COLOR}" f"⚙️ STARTE TASK AUF [{target}]" f"{RESET}" ) print( f"{INFO_COLOR}" f"➡️ {cmd}" f"{RESET}\n" ) try: # ======================================== # GUI APPS ERKENNEN # ======================================== gui_apps = [ "firefox", "thunderbird", "chromium", "google-chrome", "code", "nautilus", "pcmanfm", "gedit", "vlc", "discord", "steam", "obs", "spotify" ] first_word = cmd.strip().split()[0] is_gui_app = ( first_word in gui_apps or cmd.strip().endswith("&") ) # ======================================== # GUI APPS DETACHED STARTEN # ======================================== if is_gui_app: detached_cmd = ( f"nohup {cmd.replace('&', '').strip()} " f">/dev/null 2>&1 &" ) print( f"{SYSTEM_COLOR}" f"🖥️ GUI-APP erkannt → Detached Mode" f"{RESET}" ) if target.lower() in ["localhost", "127.0.0.1"]: proc = await asyncio.create_subprocess_shell( detached_cmd ) else: conn = get_db() n = conn.execute( 'SELECT * FROM nodes WHERE ip=? OR name=?', (target, target) ).fetchone() conn.close() if not n: err = ( f"Node '{target}' " f"nicht gefunden." ) print( f"{ERROR_COLOR}{err}{RESET}" ) return err ssh_cmd = ( f"ssh " f"-o StrictHostKeyChecking=no " f"-o LogLevel=ERROR " f"{n['user']}@{n['ip']} " f"'{detached_cmd}'" ) proc = await asyncio.create_subprocess_shell( ssh_cmd ) await proc.wait() print( f"{JARVIS_COLOR}" f"✅ GUI-Programm gestartet" f"{RESET}\n" ) return "GUI application started." # ======================================== # NORMALE COMMANDS # ======================================== else: if target.lower() in ["localhost", "127.0.0.1"]: final_cmd = cmd else: conn = get_db() n = conn.execute( 'SELECT * FROM nodes WHERE ip=? OR name=?', (target, target) ).fetchone() conn.close() if not n: err = ( f"Node '{target}' " f"nicht gefunden." ) print( f"{ERROR_COLOR}{err}{RESET}" ) return err final_cmd = ( f"ssh " f"-o StrictHostKeyChecking=no " f"-o LogLevel=ERROR " f"{n['user']}@{n['ip']} " f"'{cmd}'" ) proc = await asyncio.create_subprocess_shell( final_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT ) collected_output = [] while True: line = await proc.stdout.readline() if not line: break decoded = line.decode( "utf-8", errors="ignore" ).rstrip() collected_output.append(decoded) print( f"{OUTPUT_COLOR}" f"│ {decoded}" f"{RESET}" ) await proc.wait() print() if proc.returncode == 0: print( f"{JARVIS_COLOR}" f"✅ TASK ERFOLGREICH" f"{RESET}\n" ) else: print( f"{ERROR_COLOR}" f"❌ FEHLER CODE: " f"{proc.returncode}" f"{RESET}\n" ) return "\n".join(collected_output) except Exception as e: err = f"❌ Fehler: {e}" print( f"{ERROR_COLOR}" f"{err}" f"{RESET}\n" ) return err # ==================================================== # FILE LOGGING # ==================================================== def log_to_file(role, content): now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: with open(CHAT_LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[{now}] {role.upper()}:\n{content}\n{'-'*60}\n") except Exception as e: print(f"{ERROR_COLOR}⚠️ Konnte nicht ins Log schreiben: {e}{RESET}") # ==================================================== # USER INPUT # ==================================================== async def listen_to_user(): return await asyncio.to_thread( input, f"\n{USER_COLOR}👤 Du:{RESET} " ) # ==================================================== # JARVIS OUTPUT # ==================================================== async def speak_to_user(text): print( f"\n{JARVIS_COLOR}" f"🤖 J.A.R.V.I.S." f"{RESET}" ) print( f"{JARVIS_COLOR}" f"{'-'*60}" f"{RESET}" ) print(text) print( f"{JARVIS_COLOR}" f"{'-'*60}" f"{RESET}\n" ) # ==================================================== # MAIN LOOP # ==================================================== async def main_chat_loop(): print(f"{INFO_COLOR}") print("====================================================") print("🤖 J.A.R.V.I.S. Terminal Interface geladen") print(f"🧠 Provider: {AI_PROVIDER.upper()}") print("⌨️ Tippe 'exit', um zu beenden") print("====================================================") print(RESET) chat_history = [] while True: user_msg = await listen_to_user() if user_msg.lower().strip() in ['exit', 'quit']: print( f"{SYSTEM_COLOR}" f"\nJ.A.R.V.I.S. geht offline." f"{RESET}" ) break if not user_msg.strip(): continue now = datetime.now().strftime("%d.%m.%Y %H:%M") chat_history.append({ "role": "user", "content": user_msg, "timestamp": now }) # LOG: User Eingabe hier schreiben! log_to_file("Du", user_msg) print( f"{SYSTEM_COLOR}" f"🧠 J.A.R.V.I.S. denkt nach..." f"{RESET}", end="\r" ) system_prompt = get_system_prompt() ai_response = await get_ai_response( user_msg, system_prompt, chat_history ) # ============================================ # EXECUTE TAGS SUCHEN # Unterstützt: # cmd # cmd # ============================================ commands = [] # Toleranter Regex: Erlaubt Leerzeichen vor dem '>' execute_matches = re.finditer( r']*?(?:target="(.*?)")?[^>]*>(.*?)', ai_response, re.I | re.S ) for match in execute_matches: target = match.group(1) cmd = match.group(2) if not target: target = "localhost" # Markdown-Backticks bereinigen, falls die KI sie in den Tag mogelt cmd = cmd.strip() cmd = re.sub(r'^```[a-zA-Z]*\n?', '', cmd) cmd = re.sub(r'\n?```$', '', cmd) cmd = cmd.strip() commands.append((target.strip(), cmd)) # Die Tags für die Sprach-/Textausgabe sauber entfernen clean_msg = re.sub( r']*?>.*?', '', ai_response, flags=re.I | re.S ).strip() # ============================================ # JARVIS TEXT # ============================================ if clean_msg: await speak_to_user(clean_msg) chat_history.append({ "role": "assistant", "content": clean_msg, "timestamp": now }) # LOG: Jarvis Antwort hier schreiben! log_to_file("J.A.R.V.I.S.", clean_msg) # ============================================ # COMMANDS AUSFÜHREN # ============================================ if commands: for target, cmd in commands: target = target.strip() cmd = cmd.strip() # ======================================== # SICHTBARE SYSTEMAKTION # ======================================== action_msg = ( f"⚙️ Ich führe jetzt folgenden Befehl " f"auf [{target}] aus:\n\n" f"{cmd}" ) await speak_to_user(action_msg) chat_history.append({ "role": "assistant", "content": action_msg, "timestamp": now }) # Optional: Aktion auch ins Log log_to_file("SYSTEM", action_msg) # ======================================== # COMMAND AUSFÜHREN # ======================================== output = await run_task( target, cmd ) # ======================================== # OUTPUT IM CHAT SICHTBAR MACHEN # ======================================== if output: output_msg = ( f"💻 Ergebnis der Ausführung " f"auf [{target}]:\n\n" f"{output}" ) else: output_msg = ( f"✅ Befehl auf [{target}] " f"erfolgreich abgeschlossen." ) await speak_to_user(output_msg) sys_now = datetime.now().strftime( "%d.%m.%Y %H:%M" ) chat_history.append({ "role": "assistant", "content": output_msg, "timestamp": sys_now }) # LOG: System Output hier schreiben! log_to_file("SYSTEM", output_msg) # ============================================ # HISTORY LIMIT # ============================================ if len(chat_history) > 20: chat_history = chat_history[-20:] # ==================================================== # START # ==================================================== if __name__ == "__main__": try: asyncio.run(main_chat_loop()) except KeyboardInterrupt: print( f"\n{ERROR_COLOR}" f"⛔ J.A.R.V.I.S. hart beendet." f"{RESET}" ) EOF # J.A.R.V.I.S. Start-Skript cat << 'EOF' > "$JARVIS_DIR/start.sh" #!/bin/bash cd "$(dirname "$0")" source venv/bin/activate python3 jarvis.py EOF chmod +x "$JARVIS_DIR/start.sh" # Rechte korrigieren chown -R "$REAL_USER:$REAL_USER" "$JARVIS_DIR" chown -R "$REAL_USER:$REAL_USER" "$REAL_HOME/.config" chown "$REAL_USER:$REAL_USER" "$REAL_HOME/.profile" # Python venv installieren echo "🐍 Erstelle Python-Umgebung für J.A.R.V.I.S...." sudo -u "$REAL_USER" bash -c "cd $JARVIS_DIR && python3 -m venv venv && ./venv/bin/pip install --upgrade pip && ./venv/bin/pip install -r requirements.txt" # Autostart (Waybar & JARVIS direkt in Tilix öffnen) cat << EOF > "$REAL_HOME/.config/labwc/autostart" dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=labwc waybar & tilix --title="J.A.R.V.I.S. Terminal" -e "$JARVIS_DIR/start.sh" & EOF chmod +x "$REAL_HOME/.config/labwc/autostart" chown "$REAL_USER:$REAL_USER" "$REAL_HOME/.config/labwc/autostart" echo "====================================================" echo "✅ Lokales Setup erfolgreich abgeschlossen!" echo "👉 1. Trage deine API-Keys in $JARVIS_DIR/config/.env ein." echo "👉 2. Starte das System neu oder logge dich neu ein." echo "===================================================="