Files
jarvis-ai/setup_wayland_jarvis.sh

1147 lines
34 KiB
Bash
Raw Blame History

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