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