#!/usr/bin/env bash # JARVIS — lanzador del asistente local (Newelle) # # sin argumentos abre JARVIS a pantalla completa, sin pasar por el chat # --chat abre Newelle normal, con sus pestañas y su historial # --stop la cierra y libera el modelo # --status que corre, con que cerebro y cuanto ocupa # # El lanzador NO decide el motor: lo lee de gsettings (language-model) y se # adapta. Asi no hay que tocarlo cada vez que se cambia de cerebro, que es # justo lo que lo dejo desincronizado antes: precargaba Ollama mientras la # app usaba su llamacpp interno, ocupando 2.9 GB de VRAM para nada. # # Y el MODELO tampoco, por lo mismo. Estaba escrito a mano aqui y volvio a # pasar exactamente lo de antes: al cambiar la app a qwen3.5:4b-jarvis, el # lanzador seguia precargando qwen3.5:4b. Los dos no caben en 3717 MiB, asi # que ollama tenia que descargar uno para cargar el otro con el panel ya # esperando: 6,55 s hasta la primera respuesta contra 3,70 con los dos de # acuerdo. El sintoma no era "va lento", era "no se abre el panel solo". set -uo pipefail APPID=io.github.qwersyk.Newelle OLLAMA_URL=http://localhost:11434 # La salida de la app iba entera a /dev/null. Cuando el panel dejo de abrirse, # el traceback que lo explicaba —una linea— no estaba en ningun sitio, y el # fallo se diagnostico dos veces mal antes de mirarlo. Ahora se guarda. # misma idea que en los scripts de voz: la raiz sale de donde vive esto RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd) REGISTRO=$RAIZ/jarvis-app.log : > "$REGISTRO" 2>/dev/null || REGISTRO=/dev/null GGUF=$HOME/.var/app/$APPID/config/models/custom_models/qwen3.5-4b.gguf # Elige rama: //master es la version propia (jarvis-skin, con reactor arc); # //stable es la de Flathub, la red de seguridad. # # El chequeo mira la typelib de Vte en lib/: si un build deja mal el libdir, # la app arranca y muere con "Namespace Vte not available". Asi el icono # nunca abre una version rota, y se pasa solo a la propia cuando este sana. pick_branch() { local base=$HOME/.local/share/flatpak/app/$APPID/x86_64 if [ -f "$base/master/active/files/lib/girepository-1.0/Vte-3.91.typelib" ]; then echo "master" else echo "stable" fi } RAMA=$(pick_branch) motor() { flatpak run --command=gsettings "$APPID//$RAMA" get "$APPID" language-model 2>/dev/null \ | tr -d "'" | tr -d '\n' } # El modelo que la app tiene elegido de verdad, leido de sus ajustes. Si no se # puede leer se cae al 4b pelado, que es mejor que precargar algo que no existe. modelo() { flatpak run --command=gsettings "$APPID//$RAMA" get "$APPID" llm-settings 2>/dev/null \ | python3 -c " import json, sys s = sys.stdin.read().strip() try: d = json.loads(s[1:-1].replace('\\\\\\\\', '\\\\').replace(\"\\\\'\", \"'\")) print(d.get('ollama', {}).get('model') or 'qwen3.5:4b') except Exception: print('qwen3.5:4b') " 2>/dev/null || echo qwen3.5:4b } MODEL=$(modelo) notify() { command -v notify-send >/dev/null && notify-send -i jarvis "JARVIS" "$1"; } ollama_up() { curl -s --max-time 2 "$OLLAMA_URL/api/version" >/dev/null 2>&1; } app_corriendo() { pgrep -f "bin/newelle" >/dev/null; } # El llama-server interno de la app, no el de Ollama: el suyo vive en /app/bin. servidor_interno() { pgrep -f "/app/bin/llama-server" | head -1; } case "${1:-start}" in --stop) # flatpak kill y no pkill: el patron "bin/newelle" tambien casa con la # propia linea de comandos cuando esto se invoca desde un bash -c inline, # y el script se mataba a si mismo antes de llegar aqui. flatpak kill "$APPID" 2>/dev/null || pkill -f "bin/newelle" 2>/dev/null if [ "$(motor)" = "ollama" ] && ollama_up; then curl -s "$OLLAMA_URL/api/generate" \ -d "{\"model\":\"$MODEL\",\"keep_alive\":0}" >/dev/null 2>&1 notify "Detenido, VRAM liberada" else notify "Detenido" fi echo "JARVIS detenido." ;; --status|--status-notify) MOTOR=$(motor) app_corriendo && S1="app: abierta (//$RAMA)" || S1="app: cerrada" S2="cerebro: ${MOTOR:-?}" if [ "$MOTOR" = "ollama" ]; then S3=$(curl -s "$OLLAMA_URL/api/ps" 2>/dev/null \ | python3 -c 'import json,sys ms = json.load(sys.stdin).get("models", []) print("\n".join("cargado: %s %.1f GB (%.1f en GPU)" % (m["name"], m.get("size",0)/1e9, m.get("size_vram",0)/1e9) for m in ms) or "sin modelos cargados")' 2>/dev/null) [ -n "$S3" ] || S3="ollama: caido" else PID=$(servidor_interno) if [ -n "$PID" ]; then S3="cargado en RAM ($(awk '/VmRSS/{printf "%.1f GB", $2/1048576}' \ "/proc/$PID/status" 2>/dev/null))" else S3="sin modelos cargados" fi fi S4=$(nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader 2>/dev/null \ | sed 's/^/VRAM usada\/libre: /') if [ "$1" = "--status-notify" ]; then notify "$S1 · $S2"$'\n'"$S3"$'\n'"$S4" else printf '%s\n%s\n\n%s\n%s\n' "$S1" "$S2" "$S3" "$S4" fi ;; *) MOTOR=$(motor) if [ "$MOTOR" = "ollama" ]; then if ! ollama_up; then systemctl start ollama 2>/dev/null || sudo systemctl start ollama for _ in $(seq 20); do ollama_up && break; sleep 0.5; done fi ollama_up || { notify "Ollama no arranca"; echo "ERROR: ollama no responde" >&2; exit 1; } # precargar en GPU para que la primera respuesta no tarde. # think:false o se va a 40 s pensando en voz alta. curl -s "$OLLAMA_URL/api/generate" \ -d "{\"model\":\"$MODEL\",\"prompt\":\"ok\",\"stream\":false,\"think\":false,\"keep_alive\":\"30m\"}" \ >/dev/null 2>&1 & else [ -f "$GGUF" ] || { notify "Falta el modelo: $(basename "$GGUF")" echo "ERROR: no esta $GGUF" >&2; exit 1; } fi # --chat abre Newelle tal cual; sin argumentos va derecho al asistente DIRECTO=si [ "${1:-}" = "--chat" ] && DIRECTO=no if app_corriendo; then command -v xdotool >/dev/null && \ xdotool search --name "^newelle$" windowactivate 2>/dev/null elif [ "$DIRECTO" = "no" ]; then exec flatpak run "$APPID//$RAMA" else # En segundo plano y no con exec: hay que esperar a que exista la ventana # antes de pedir la pestaña. --run-action se procesa antes de que la # ventana este construida, asi que lanzarlo de golpe no serviria. # # Y esperar al PROCESO no basta: el proceso existe en cuanto python # arranca, varios segundos antes de que haya ventana. Con `sleep 3` a # ojo, un arranque un poco mas lento pierde la carrera y la accion # revienta con "'MyApp' object has no attribute 'win'" — la app abre, # pero en el chat y sin panel. Se espera a la VENTANA, que es la # condicion de verdad. flatpak run "$APPID//$RAMA" >>"$REGISTRO" 2>&1 & for _ in $(seq 60); do if command -v xdotool >/dev/null; then xdotool search --name "^newelle$" >/dev/null 2>&1 && break else pgrep -f "bin/newelle" >/dev/null && break fi sleep 0.5 done # sin xdotool no hay forma de saberlo: se vuelve al margen a ojo command -v xdotool >/dev/null || sleep 3 fi if [ "$DIRECTO" = "si" ]; then # jarvis_full y no jarvis+hud: el segundo alterna, asi que pulsar el # icono con la app ya abierta te sacaria de pantalla completa. flatpak run "$APPID//$RAMA" --run-action=jarvis_full >>"$REGISTRO" 2>&1 fi ;; esac