#!/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 libasound2-dev libportaudio2 unzip # 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 PARAM1=$3 WD="wdotool --backend wlr-protocols" 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 if [ -z "$WAYLAND_DISPLAY" ]; then export WAYLAND_DISPLAY=$(ls /run/user/$(id -u)/wayland-* 2>/dev/null | head -n 1 | xargs basename) [ -z "$WAYLAND_DISPLAY" ] && export WAYLAND_DISPLAY="wayland-0" fi if [ -z "$DISPLAY" ]; then export DISPLAY=":0" fi nohup "$@" >/dev/null 2>&1 & echo "✅ Programm '$APP_NAME' wurde entkoppelt im Hintergrund gestartet (Display: $WAYLAND_DISPLAY)." exit 0 fi # ========================================================= # 2. SONDERFALL: PROGRAMM SCHLIESSEN (Direkt & unfehlbar) # ========================================================= if [ "$ACTION" == "close" ]; then # Namen in Kleinbuchstaben umwandeln für maximale Trefferquote LOW_APP=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]') # Direktes Signal an das System senden pkill -f "$LOW_APP" echo "✅ Schließ-Signal (pkill) an '$APP_NAME' gesendet." exit 0 fi # ========================================================= # 3. WAYLAND-SUCHE (Nur noch für activate, snap, maximize) # ========================================================= if [ "$APP_NAME" == "active" ]; then WINDOW_ID=$($WD getactivewindow 2>/dev/null | awk '{print $1}') else 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 case "$ACTION" in activate) if [ -n "$WINDOW_ID" ]; then $WD windowactivate "$WINDOW_ID" echo "✅ Fenster '$APP_NAME' (ID: $WINDOW_ID) ist jetzt im Fokus." else $WD key alt+Tab echo "⚠️ Keine direkte Fenster-ID gefunden, wechsle Fokus via Alt+Tab." fi ;; maximize) if [ -n "$WINDOW_ID" ]; then $WD windowactivate "$WINDOW_ID" sleep 0.1 fi $WD key super+up echo "✅ Maximieren-Signal gesendet." ;; snap) if [ -z "$PARAM1" ]; then echo "❌ Fehler: Für 'snap' wird eine Richtung benötigt." exit 1 fi if [ -n "$WINDOW_ID" ]; then $WD windowactivate "$WINDOW_ID" sleep 0.1 fi case "$PARAM1" in left) $WD key super+Left ;; right) $WD key super+Right ;; up) $WD key super+Up ;; down) $WD key super+Down ;; 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: $PARAM1" ; exit 1 ;; esac echo "✅ Fenster '$APP_NAME' nach $PARAM1 angedockt." ;; *) echo "❌ Unbekannte Aktion: $ACTION." 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}' {installed_apps} WICHTIGE REGELN FÜR DIE AUSFÜHRUNG: 1. Wenn der Nutzer nach einer App fragt (z.B. "starte den Editor"), schaue in der obigen Liste nach dem passenden Programmnamen und nimm EXAKT den dort definierten Befehl. 2. Rate niemals Befehle, die nicht in der Liste stehen! 3. Wenn du eine Aktion ausführst, MUSST du den Linux-Befehl EXAKT in und Tags setzen. 4. VERBOTEN: Verwende NIEMALS Markdown-Codeblöcke (```) um oder in den -Tags! Schreibe die Tags als simplen, rohen Text. 5. 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 WICHTIGE REGELN FÜR TEXTE IN EDITOREN: 1. Wenn der Nutzer einen Text (wie eine Einladung, Notiz oder Code) in einem Editor wie Geany erstellen möchte, erstelle den Text NIEMALS direkt mit "wdotool type" in einer langen Kette! Das ist zu fehleranfällig. 2. Nutze stattdessen IMMER diesen zweistufigen, krisenfesten Weg: Schritt A: Schreibe den generierten Text zuerst in eine temporäre Datei (z.B. mit echo oder cat). Schritt B: Öffne diese Datei anschließend direkt mit Geany. Beispiel für das korrekte Vorgehen: cat << 'EOF' > /tmp/einladung.txt Liebe Familie... EOF geany /tmp/einladung.txt 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" #################################### # Voice setup #################################### # Modell herunterladen wget https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip # Entpacken unzip vosk-model-small-de-0.15.zip # Ordner umbenennen, damit das Skript ihn leicht findet mv vosk-model-small-de-0.15 model rm vosk-model-small-de-0.15.zip cat << 'EOF' > "$JARVIS_DIR/wakeword.py" #!/usr/bin/env python3 import os import sys import json import queue import time import subprocess import sounddevice as sd import numpy as np from vosk import Model, KaldiRecognizer from pathlib import Path MODEL_PATH = "model" AUDIO_RATE = 48000 LOCK_FILE = Path("/tmp/.jarvis_speaking") if not os.path.exists(MODEL_PATH): print(f"❌ Modell-Ordner '{MODEL_PATH}' wurde nicht gefunden!") sys.exit(1) audio_queue = queue.Queue() def audio_callback(indata, frames, time, status): if status: print(status, file=sys.stderr) audio_queue.put(bytes(indata)) print("🧠 J.A.R.V.I.S. lädt das Sprachmodell...") model = Model(MODEL_PATH) # Zwei Recognizer: Einer für das Wake-Word, einer für den eigentlichen Befehl (offen) wake_recognizer = KaldiRecognizer(model, AUDIO_RATE, '["jarvis", "[unk]"]') command_recognizer = KaldiRecognizer(model, AUDIO_RATE) # Sucht nach JEDEM deutschen Wort print("🎙️ J.A.R.V.I.S. ist online und lauscht... (Sag 'Jarvis')") with sd.RawInputStream(samplerate=AUDIO_RATE, blocksize=8000, dtype='int16', channels=1, callback=audio_callback): while True: data = audio_queue.get() # NEU: Wenn J.A.R.V.I.S. gerade spricht, leere die Queue und ignoriere das Audio if LOCK_FILE.exists(): while not audio_queue.empty(): audio_queue.get() wake_recognizer.Reset() # Verhindert, dass Bruchstücke von vorhin gespeichert bleiben continue # Phase 1: Auf Wake-Word warten if wake_recognizer.AcceptWaveform(data): result = json.loads(wake_recognizer.Result()) if "jarvis" in result.get("text", ""): print("\n⚡ [WAKEWORD DETECTED] Ja, Sir?") # Bestätigungston abspielen # Kurzer, smarter Beep-Ton (800 Hz, 0.1 Sekunden) duration = 0.1 frequency = 800.0 t = np.linspace(0, duration, int(AUDIO_RATE * duration), endpoint=False) beep = np.sin(2 * np.pi * frequency * t) * 0.3 # 0.3 für angenehme Lautstärke sd.play(beep, samplerate=AUDIO_RATE) sd.wait() # Warteschlange leeren, um alten Ton nicht als Befehl zu interpretieren while not audio_queue.empty(): audio_queue.get() print("👂 Höre zu...") command_text = "" start_time = time.time() # Phase 2: Für 4 Sekunden den darauffolgenden Befehl aufnehmen while time.time() - start_time < 4.0: cmd_data = audio_queue.get() if command_recognizer.AcceptWaveform(cmd_data): res = json.loads(command_recognizer.Result()) command_text += " " + res.get("text", "") # Letzten Rest auslesen final_res = json.loads(command_recognizer.FinalResult()) command_text += " " + final_res.get("text", "") command_text = command_text.strip() if command_text: print(f"🗣️ Erkannter Befehl: '{command_text}'") print("🧠 Übermittle an J.A.R.V.I.S. Gehirn...") # Rufe jarvis.py im virtuellen Environment auf und übergib den Befehl # (Wir nutzen hier Google Gemini oder was auch immer in deiner .env aktiv ist!) subprocess.run([ "/home/meik/jarvis-ai/venv/bin/python3", "/home/meik/jarvis-ai/jarvis.py", "--voice-cmd", command_text ]) else: print("🔇 Kein Befehl verstanden.") print("\n🎙️ Zurück im Standby. Lausche auf 'Jarvis'...") wake_recognizer.Reset() command_recognizer.Reset() EOF # Piper installieren wget https://github.com/rhasspy/piper/releases/download/v1.2.0/piper_amd64.tar.gz tar -xf piper_amd64.tar.gz rm piper_amd64.tar.gz # Das eigentliche Sprachmodell (.onnx) wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/high/de_DE-thorsten-high.onnx # Die dazugehörige Konfigurationsdatei (.json) wget https://huggingface.co/rhasspy/piper-voices/resolve/main/de/de_DE/thorsten/high/de_DE-thorsten-high.onnx.json 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 "===================================================="