diff --git a/jarvis.py b/jarvis.py index 319661d..d31b51b 100644 --- a/jarvis.py +++ b/jarvis.py @@ -1,234 +1,772 @@ import os -import asyncio -import sqlite3 import re -from datetime import datetime -from pathlib import Path -import subprocess +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 -# --- FARBEN FÜR DAS TERMINAL --- -class Color: - USER = '\033[94m' # Blau - JARVIS = '\033[92m' # Grün - SYSTEM = '\033[93m' # Gelb - EXEC = '\033[95m' # Magenta für SSH - ERROR = '\033[91m' # Rot - RESET = '\033[0m' # Zurücksetzen - BOLD = '\033[1m' +from colorama import init, Fore, Style + + +# ==================================================== +# INITIALISIERUNG +# ==================================================== + +init(autoreset=True) + + +# ==================================================== +# PFADE & SETUP +# ==================================================== -# --- PFADE & SETUP --- BASE_DIR = Path(__file__).resolve().parent -ROOT_DIR = BASE_DIR.parent -CONFIG_DIR = ROOT_DIR / "config" -DATA_DIR = ROOT_DIR / "data" -WORKSPACE_DIR = ROOT_DIR / "workspace" +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" -PROMPT_FILE = CONFIG_DIR / "system_prompt.txt" 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", "Admin") +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 +# ==================================================== -# Ordner & Dateien anlegen 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") + 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 +# ==================================================== -# --- 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", "") -OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1") +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.0-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.5") +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 +# ==================================================== -# --- 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 + 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 -def get_system_prompt(current_user=WEB_USER_NAME): + +# ==================================================== +# SYSTEM PROMPT +# ==================================================== + +def get_system_prompt(): + prompt_path = CONFIG_DIR / "system_prompt.txt" - if prompt_path.exists(): - prompt = prompt_path.read_text(encoding="utf-8") - else: - prompt = f"Hallo {current_user}, ich bin J.A.R.V.I.S., dein lokaler Systemassistent." - prompt = prompt.replace("{user_name}", current_user) + 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." + ) - conn = get_db() - nodes = conn.execute('SELECT * FROM nodes').fetchall() - conn.close() - - node_info = "" - for n in nodes: - docker_str = "Ja" if n['docker_installed'] else "Nein" - node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n" - - prompt = prompt.replace("{node_info}", node_info) + 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)) - - return prompt -# --- KI KOMMUNIKATION --- -async def get_ai_response(user_msg, system_prompt, history_list): - try: - if AI_PROVIDER in ["openai", "ollama", "nvidia"]: - messages = [{"role": "system", "content": system_prompt}] + history_list - - if AI_PROVIDER == "ollama": - url = OLLAMA_BASE_URL if OLLAMA_BASE_URL.endswith('/v1') else OLLAMA_BASE_URL.rstrip('/') + '/v1' - key, model_to_use = "ollama", OLLAMA_MODEL - elif AI_PROVIDER == "nvidia": - url, key, model_to_use = "https://integrate.api.nvidia.com/v1", NVIDIA_API_KEY, NVIDIA_MODEL - else: - url, key, model_to_use = None, OPENAI_API_KEY, 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": - if not GOOGLE_API_KEY: return "Fehler: GOOGLE_API_KEY fehlt!" - 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"{Color.ERROR}Fehler bei der KI-Anfrage: {e}{Color.RESET}" - -# --- SSH BEFEHLSAUSFÜHRUNG --- -async def run_remote_task(target, cmd): - print(f"\n{Color.EXEC}🚀 J.A.R.V.I.S. führt aus auf {target}:{Color.RESET}\n> {cmd}\n") - conn = get_db() - n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target, target)).fetchone() + + nodes = conn.execute( + 'SELECT * FROM nodes' + ).fetchall() + conn.close() - if not n: - msg = f"{Color.ERROR}⚠️ Node '{target}' nicht in der Datenbank gefunden.{Color.RESET}" - print(msg) - return msg + 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: - proc = await asyncio.create_subprocess_shell( - f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR {n['user']}@{n['ip']} '{cmd}'", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT - ) - stdout, _ = await proc.communicate() - output = stdout.decode('utf-8', errors='ignore').strip() - - print(f"{Color.EXEC}💻 Output:\n{output or '✅ Ohne Output ausgeführt.'}{Color.RESET}\n") - return output + + 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: - err = f"{Color.ERROR}❌ Fehler: {e}{Color.RESET}" - print(err) + + 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 -# --- HAUPT-TERMINAL-LOOP --- +# ==================================================== +# 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(): - # Bunter Boot-Screen - print(f"{Color.SYSTEM}===================================================={Color.RESET}") - print(f"{Color.BOLD}{Color.JARVIS}🤖 J.A.R.V.I.S. Terminal Interface geladen.{Color.RESET}") - print(f"{Color.SYSTEM}Provider: {AI_PROVIDER.upper()}") - print(f"Tippe 'exit' oder 'quit' um zu beenden.{Color.RESET}") - print(f"{Color.SYSTEM}===================================================={Color.RESET}\n") - + + 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 = [] - system_prompt = get_system_prompt() while True: - # User Eingabe abwarten (Blau und Fett) - user_msg = await asyncio.to_thread(input, f"\n{Color.BOLD}{Color.USER}Du: {Color.RESET}") - + + user_msg = await listen_to_user() + if user_msg.lower().strip() in ['exit', 'quit']: - print(f"{Color.SYSTEM}J.A.R.V.I.S. wird beendet. Auf Wiedersehen!{Color.RESET}") + + 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}) - # Ladeindikator - print(f"{Color.SYSTEM}J.A.R.V.I.S. verarbeitet...{Color.RESET}", end="\r") + chat_history.append({ + "role": "user", + "content": user_msg, + "timestamp": now + }) - # KI antworten lassen - ai_response = await get_ai_response(user_msg, system_prompt, chat_history) + # 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 + # ============================================ - # Nach Tags suchen - commands = re.findall(r'(.*?)', ai_response, re.I | re.S) - clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() + 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 + # ============================================ - # Ladezeile säubern und KI-Antwort ausgeben (Grün) - print("\r" + " " * 30 + "\r", end="") - print(f"{Color.BOLD}{Color.JARVIS}🤖 J.A.R.V.I.S.:{Color.RESET}") - print(f"{Color.JARVIS}{clean_msg}{Color.RESET}") + if clean_msg: - chat_history.append({"role": "assistant", "content": clean_msg, "timestamp": now}) + 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 + # ============================================ - # Ausführung der gefundenen Befehle if commands: - for target, cmd in commands: - output = await run_remote_task(target.strip(), cmd.strip()) - # System-Output ebenfalls in die Historie packen - sys_now = datetime.now().strftime("%d.%m.%Y %H:%M") - chat_history.append({ - "role": "user", - "content": f"[SYSTEM] Befehl '{cmd}' auf {target} abgeschlossen. Output:\n{output}", - "timestamp": sys_now - }) - # RAM-Schutz: Historie nicht ins Unendliche wachsen lassen + 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{Color.ERROR}J.A.R.V.I.S. hart beendet.{Color.RESET}") \ No newline at end of file + + print( + f"\n{ERROR_COLOR}" + f"⛔ J.A.R.V.I.S. hart beendet." + f"{RESET}" + ) diff --git a/requirements.txt b/requirements.txt index 0a1b1e5..98b9b5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ openai google-genai -python-dotenv \ No newline at end of file +python-dotenv diff --git a/setup_wayland_jarvis.sh b/setup_wayland_jarvis.sh index 14edbf2..f18ed35 100644 --- a/setup_wayland_jarvis.sh +++ b/setup_wayland_jarvis.sh @@ -21,11 +21,12 @@ 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 fonts-noto-color-emoji wofi +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) @@ -53,7 +54,7 @@ if [ -z "$REAL_WDOTOOL" ]; then echo "❌ Fehler: wdotool nicht gefunden!"; exit sudo mv "$REAL_WDOTOOL" /usr/local/bin/wdotool.real -# 4. wdotool Wrapper erstellen +# 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) @@ -62,11 +63,132 @@ 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 labwc Autostart und Tastaturlayout..." +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 @@ -152,14 +274,18 @@ EOF # J.A.R.V.I.S. .env Template cat << 'EOF' > "$JARVIS_DIR/config/.env" -WEB_USER_NAME=Meik -AI_PROVIDER=nvidia +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.5 +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 @@ -171,133 +297,819 @@ UMGEBUNG & GEDÄCHTNIS: - Notizen: {notes_file} - Todos: {todo_file} -DESKTOP STEUERUNG & WIDOTOOL HANDBUCH: -Um die grafische Oberfläche (Wayland/labwc) zu steuern, nutzt du das Tool 'wdotool'. Hier ist deine Befehlsreferenz: -- Tastatur: wdotool key ctrl+l, wdotool key alt+Tab, wdotool type "Hallo", wdotool type --delay 30 "Langsam tippen" -- Maus: wdotool mousemove 500 400 (absolut), wdotool click 1 (1=links, 3=rechts), wdotool scroll 0 3 (runter scrollen) -- Fenster-Management: - wdotool search --name "Firefox" (liefert die Fenster-) - wdotool search --class tilix (liefert die basierend auf der App-ID) - wdotool getactivewindow (liefert des aktuellen Fokus-Fensters) - wdotool windowactivate (holt das Fenster in den Vordergrund) - wdotool windowclose (schließt das Fenster) +DESKTOP STEUERUNG & FENSTER-MANAGEMENT: +Du steuerst die grafische Oberfläche (Wayland/labwc) über Befehlszeilen-Tools. -WICHTIGE REGEL: Wenn du eine Systemaktion ausführen möchtest, umschließe den Linux-Bash-Befehl EXAKT mit und . -Beispiel 1 (Fenster in den Vordergrund holen): wdotool search --name "Firefox" | xargs wdotool windowactivate -Beispiel 2 (Tastenkürzel): wdotool key ctrl+c +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 -Du darfst mehrere Befehle mit && oder Pipes (|) verketten. 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, re, asyncio, subprocess, openai +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") + 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", "") -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_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}." + + 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)) - return prompt + + 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"]: - messages = [{"role": "system", "content": system_prompt}] + history_list - if AI_PROVIDER == "ollama": url, key, model = OLLAMA_BASE_URL.rstrip('/') + '/v1', "ollama", OLLAMA_MODEL - elif AI_PROVIDER == "nvidia": url, key, model = "https://integrate.api.nvidia.com/v1", NVIDIA_API_KEY, NVIDIA_MODEL - else: url, key, model = None, OPENAI_API_KEY, OPENAI_MODEL - client = openai.AsyncOpenAI(base_url=url, api_key=key) - response = await client.chat.completions.create(model=model, 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}" -async def run_task(cmd): - print(f"\n⚙️ J.A.R.V.I.S. führt aus: {cmd}") try: - proc = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) - stdout, _ = await proc.communicate() - output = stdout.decode('utf-8', errors='ignore').strip() - print(f"💻 Output:\n{output or '✅ Ohne Ausgabe beendet.'}\n") - return output - except Exception as e: return f"❌ Fehler: {e}" + + 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, "\nDu: ") + + return await asyncio.to_thread( + input, + f"\n{USER_COLOR}👤 Du:{RESET} " + ) + + +# ==================================================== +# JARVIS OUTPUT +# ==================================================== async def speak_to_user(text): - print(f"\n🤖 J.A.R.V.I.S.:\n{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. Desktop Terminal geladen.") - print(f"Provider: {AI_PROVIDER.upper()}") + 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']: break - if not user_msg.strip(): continue + + 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}) + + chat_history.append({ + "role": "user", + "content": user_msg, + "timestamp": now + }) - ai_response = await get_ai_response(user_msg, get_system_prompt(), chat_history) + # 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 + # ============================================ - # EXAKT nach deinem neuen Prompt-Format matchen - commands = re.findall(r'(.*?)', ai_response, re.I | re.S) - clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() + 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}) + + 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 cmd in commands: - output = await run_task(cmd.strip()) - sys_now = datetime.now().strftime("%d.%m.%Y %H:%M") - chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' abgeschlossen. Output:\n{output}", "timestamp": sys_now}) - if len(chat_history) > 20: chat_history = chat_history[-20:] + + 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("\nJ.A.R.V.I.S. hart beendet.") + + 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 @@ -320,6 +1132,7 @@ sudo -u "$REAL_USER" bash -c "cd $JARVIS_DIR && python3 -m venv venv && ./venv/b # 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 @@ -330,4 +1143,4 @@ 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 "====================================================" \ No newline at end of file +echo "====================================================" diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index 4db3bc4..874c0e5 --- a/start.sh +++ b/start.sh @@ -1,10 +1,4 @@ #!/bin/bash cd "$(dirname "$0")" - -if [ ! -d "venv" ]; then - echo "❌ Fehler: venv nicht gefunden. Bitte zuerst ./setup.sh ausführen." - exit 1 -fi - source venv/bin/activate -python3 jarvis.py \ No newline at end of file +python3 jarvis.py