Pi en el móvil: un agente de terminal con interfaz web, varias sesiones y tu modelo local
Pi es un agente de programación de terminal. Con una capa web encima, lo usamos desde el iPhone contra el modelo que corre en las GX10, con proyectos, sesiones en paralelo y hasta capturas de pantalla. Así está montado.
Pi es un agente de programación que vive en la terminal: lee tu proyecto, edita ficheros, ejecuta comandos. Lo interesante para nosotros es que acepta cualquier proveedor compatible con la API de OpenAI, así que puede trabajar con el modelo que corre en nuestras GX10 en lugar de con uno de pago.
El problema es que una terminal en el móvil es una tortura. La solución es una interfaz web —usamos PI WEB— que corre en el portátil, al lado de los proyectos, y que publicamos con la misma cadena de la guía 3.
Cómo encaja
iPhone ─► https://pi.tudominio.es ─► Nginx del servidor ─► Tailscale ─►
portátil: bridge con login (:3084) ─► PI WEB (:8504) ─► daemon de sesiones (127.0.0.1:8505)
│
el agente trabaja sobre C:\tu-carpeta-de-proyectos
│
modelo: relay de la GX10 ─► gateway ─► el modelo residente
Tres ideas que conviene tener claras:
- El agente corre en el portátil, donde están los proyectos. El móvil solo es pantalla.
- El modelo corre en la GX10. El portátil no carga pesos: habla con un alias,
gx10-local, que el gateway del laboratorio resuelve al modelo que esté residente. - Nada escucha en internet salvo el Nginx del servidor.
Paso 1 · Pi y su proveedor local
Instala Pi con npm y dile dónde está tu modelo. En ~/.pi/agent/models.json declaras un
proveedor OpenAI-compatible con su baseUrl y un modelo. El esquema exacto depende de la
versión de Pi, así que tómate esto como ilustración de la idea, no como fichero para copiar:
{
"providers": {
"gx10": {
"baseUrl": "http://100.x.y.z:8110/v1",
"apiKeyEnv": "PI_GX10_KEY",
"models": [{ "id": "gx10-local" }]
}
}
}
La clave no se escribe ahí: el fichero solo nombra la variable de entorno. El lanzador la
lee de un fichero local de secretos y la pone en memoria al arrancar el proceso. Así la clave
nunca acaba en Git, en una captura ni en un models.json que alguien copia.
Prueba rápida, sin interfaz:
$env:PI_GX10_KEY = (Get-Content "$env:USERPROFILE\secretos\clave-relay" -Raw).Trim()
pi --mode json --print --model 'gx10/gx10-local:off' 'Responde exactamente PI_OK'
Remove-Item Env:PI_GX10_KEY
Si responde PI_OK, el camino portátil → GX10 funciona. La respuesta incluye además qué
modelo contestó de verdad detrás del alias.
Paso 2 · PI WEB y su daemon de sesiones
PI WEB tiene dos procesos:
- el servidor web, que en nuestro caso escucha solo en la IP de Tailscale del portátil;
- el daemon de sesiones, en
127.0.0.1, que mantiene vivas todas las sesiones del agente.
Lo mejor de este diseño: puedes tener varias sesiones abiertas a la vez, incluso en proyectos distintos, todas colgando de un único daemon. Arrancas una tarea en el móvil, la dejas trabajando, abres otra.
En Windows los dos procesos son tareas programadas con un watchdog: si Node se cae, se relanza en segundos. Y cada watchdog tiene un mutex global para que una segunda ejecución no arranque un segundo daemon. No lances un daemon por sesión.
⚠️ Configura las tareas para que no se detengan al pasar a batería. Es el ajuste por defecto y tumba el servicio en cuanto desenchufas el portátil (P-090).
Paso 3 · El bridge con login y la app en la pantalla de inicio
Delante de PI WEB va el bridge autenticado (el mismo patrón que en la guía 3): usuario y contraseña, cookie de 30 días, 401 para todo lo demás. Con eso, en el iPhone:
Safari → Compartir → Añadir a la pantalla de inicio.
Queda como una app: icono propio, pantalla completa, sin barra del navegador.
Dos problemas que tuvimos al publicarlo
- Pantalla vacía en Safari y Chrome. El bridge modificaba el JavaScript en tránsito pero
dejaba la cabecera
content-encoding: gzipde la respuesta original. El navegador intentaba descomprimir algo que ya no estaba comprimido y descartaba el fichero. Si tu proxy toca el cuerpo de una respuesta, quita o recalculacontent-encoding. - Safari se aferra a la versión rota. Tras arreglarlo, el iPhone seguía cargando los ficheros viejos de su caché. Solución: añadir un identificador de versión a las URL de los bundles desde Nginx y, si aun así persiste, borrar solo los datos de ese dominio y volver a añadir la app a la pantalla de inicio.
Paso 4 · Imágenes: el límite que no es de Cloudflare
Pi puede recibir capturas de pantalla, y el modelo las ve. Pero nuestro endpoint acepta como máximo cuatro imágenes por petición, y una sesión larga acumula muchas en el historial. Síntoma: un HTTP 400 en mitad de una conversación que iba bien.
La solución fue una pequeña extensión de Pi que, solo en la copia que se envía, quita las imágenes más antiguas y conserva las cuatro últimas. El historial y los ficheros del proyecto no se tocan. No subas el límite del servidor ni mandes el historial entero: el 400 es una restricción real del servidor de inferencia.
Comprobaciones
Get-ScheduledTask -TaskName 'PI WEB Sessiond','PI WEB Server' | Select TaskName,State
Invoke-RestMethod http://127.0.0.1:8505/health # ok: true
Get-NetTCPConnection -LocalPort 8504,8505 | Select LocalPort,State,OwningProcess
# desde el servidor, ¿llega al portátil por Tailscale?
curl --max-time 8 http://100.x.y.z:8504/api/pi-web/version
Debe haber un solo proceso escuchando en cada puerto. Si ves dos, tienes una carrera entre watchdogs: no mates al dueño del daemon mientras haya sesiones trabajando.
Siguiente: el arnés de agentes remoto, con avisos cuando un agente te pide permiso.