Compare commits

..

2 commits

Author SHA1 Message Date
sito
28a6a6e935 azul: la captura del panel real en vez de la imagen de ejemplo
La que habia era la promocional de Newelle, con un proveedor en la nube en la
barra de titulo y ningun rastro de JARVIS. Ahora es el panel a pantalla
completa corriendo en local: reactor, estado de la maquina, catalogo de
acciones y las tarjetas de lo hecho. Las rutas de los resultados van
desenfocadas, como en las del carril verde.
2026-08-18 12:08:06 +02:00
sito
38a8daf9ab carril azul (Newelle): documentacion, diagrama y captura
Rama JARVIS-BLUE, independiente de master. Documenta el primer carril de JARVIS,
montado sobre Newelle (GNOME, GPL) y hoy congelado en favor del nucleo naranja.

- README propio del carril azul con diagrama de arquitectura y captura del interfaz.
- azul/docs/: el grafico (tema azul) y la captura de Newelle.
- Explica el stack, lo que JARVIS puso encima y por que se congelo.
2026-08-18 11:37:21 +02:00
11 changed files with 1977 additions and 2371 deletions

200
README.md
View file

@ -1,184 +1,48 @@
# JARVIS # JARVIS-AZUL 🔵
Asistente de voz **100 % local** para Linux. Ni el micrófono, ni las El **primer** carril de JARVIS, montado sobre **[Newelle](https://github.com/qwersyk/Newelle)** (el asistente de escritorio para GNOME, GPL). Hoy está **congelado**: sirvió para arrancar rápido y aprender qué se quería, y esas lecciones dieron pie al núcleo naranja. Esta rama lo documenta; el `master` es el núcleo naranja.
conversaciones, ni el modelo salen de la máquina. Sin cuentas, sin claves, sin
nube. Núcleo propio, en castellano, que corre en **cualquier Linux** —de un
portátil modesto a una torre— y aprovecha la GPU si la hay, o la CPU si no.
![Linux](https://img.shields.io/badge/Linux-cualquier%20distro-FCC624?logo=linux&logoColor=black) ![Arquitectura del carril azul](azul/docs/arquitectura.svg)
![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white)
![Ollama](https://img.shields.io/badge/Ollama-qwen3.5%3A4b-000000?logo=ollama&logoColor=white)
![GPU](https://img.shields.io/badge/GPU-opcional%20(NVIDIA%2FAMD%2FCPU)-76B900)
![whisper.cpp](https://img.shields.io/badge/whisper.cpp-STT-555555)
![Piper](https://img.shields.io/badge/Piper-TTS-555555)
![License](https://img.shields.io/badge/licencia-GPL--3.0-blue)
![Arquitectura de JARVIS](docs/stack.svg) ## Para qué sirvió
![El panel de JARVIS](docs/panel.png) JARVIS tiene tres carriles: **azul** (Newelle, este, congelado), **naranja** (el núcleo propio) y **verde** (Hermes Agent). El azul fue el punto de partida: en vez de escribir un asistente desde cero, se cogió Newelle —una app de GNOME que ya trae chat con LLM local, RAG de documentos, memoria, TTS, búsqueda web y extensiones— y se le puso encima la identidad de JARVIS.
*El panel: telemetría del sistema, el reactor, lo que oye y hace, el catálogo de Fue la forma más rápida de tener algo que funcionara y, sobre todo, de **descubrir qué faltaba**. Lo que se aprendió aquí es lo que define hoy al naranja.
acciones, y las métricas de rendimiento del propio asistente. Hay un
[vídeo de ejemplo](docs/demo.webm) (47 s) en `docs/`.* ## El interfaz
![El panel del carril azul](azul/docs/panel.png)
*El panel a pantalla completa: el reactor, el estado de la máquina a la izquierda, el catálogo de acciones a la derecha y las tarjetas de lo ya hecho. Todo en local —Ollama en `127.0.0.1`—. Las rutas de los resultados van tapadas.*
## El stack ## El stack
| Capa | Pieza | Papel | | Capa | Pieza | Licencia | ¿Nube? |
|---|---|---| |---|---|---|---|
| Oír | whisper.cpp | STT en castellano (GPU si hay, si no CPU) | | Front-end | Newelle (GNOME, Flatpak) | GPL | no |
| Pensar | Ollama · `qwen3.5:4b` | el cerebro, con tool-calling | | Cerebro | Ollama · modelo `qwen` local | Apache-2.0 | **no**`127.0.0.1` |
| Saber | model2vec + búsqueda híbrida | RAG sobre los apuntes del usuario | | Hablar | Piper (voz clonada) | MIT | **no** |
| Hablar | Piper (o voz clonada con XTTS) | TTS local | | Oír | Whisper local | MIT | **no** |
| Cara | panel web · WebSocket · Canvas | el HUD, con el reactor y las métricas | | Saber | Documentos locales de Newelle (RAG) | — | **no** |
| Manos | catálogo de acciones + shell | lo que ejecuta en la máquina |
Todo corre en `127.0.0.1`. No hay ninguna llamada a la nube en el uso normal; lo ## Lo que JARVIS le puso encima
único que puede salir es una búsqueda web si el modelo decide usarla, y se avisa.
## Portabilidad Newelle es genérico; JARVIS lo personalizó (los ficheros viven en `config/` del núcleo):
Diseñado para correr en **cualquier Linux**, no en un equipo concreto: - **Persona y prompt** propios (`set_persona.py`).
- **Voz** por Piper, la misma cadena de audio que usa el naranja (`jarvis-piper.sh`, `jarvis-voz.sh`).
- **Modelo** local afinado (`qwen3.5-4b-jarvis.Modelfile`, `apuntar_modelo_jarvis.py`).
- **Herramientas** propias.
- **Panel a pantalla completa** (`jarvis_full`): reactor, estado de la máquina,
catálogo de acciones y el registro de lo hecho, sin pasar por el chat.
| | | ## Por qué se congeló
|---|---|
| Distro | cualquiera. `install.sh` detecta apt / dnf / pacman / zypper |
| GPU | **opcional**. Ollama reparte el modelo entre GPU y CPU solo: va con NVIDIA (CUDA), AMD (ROCm) o **solo CPU**. Ningún parámetro está atado a una tarjeta |
| Escritorio | GNOME, KDE, lo que sea. Las acciones de **ventanas** necesitan X11 + `xdotool`; en Wayland esas órdenes se desactivan solas y el resto funciona igual |
| Audio | PipeWire, PulseAudio o ALSA. El micro prueba `pw-record`, `parec` y `arecord`, la primera que haya; la voz prueba `paplay`, `pw-play` y `aplay` |
| whisper | el binario se toma del build local o del `PATH`. Los modelos se buscan solos, o los fijas con `JARVIS_WHISPER_MODELOS=/ruta` |
**Requisito real:** que quepa el modelo. `qwen3.5:4b` va cómodo desde ~4 GB de Newelle es potente, pero va empaquetado como **Flatpak sobre GNOME**, y ese sandbox limita lo que el asistente puede tocar en la máquina. Se quería más: **control total del núcleo** —latencia baja, acciones rápidas que no pasan por el modelo, root gestionado a demanda, un panel que no dependa del ciclo de vida de la app—. Nada de eso encajaba bien dentro de la app de escritorio de otro.
VRAM; con menos, usa un modelo más pequeño (`qwen3.5:2b`) o tira de CPU (hay RAM
de sobra). whisper.cpp se compila para tu plataforma —CUDA, ROCm, Metal o CPU—;
la guía está en [`config/README_whisper_gpu.md`](config/README_whisper_gpu.md).
### Ajuste en tarjetas pequeñas (4 GB) Así que el azul se congeló y nació el **carril naranja**, un núcleo escrito a medida. El azul se queda como referencia y como reconocimiento: fue el que enseñó el camino.
Solo si vas MUY justo de VRAM y el cerebro y whisper se pelean por ella: fijar ---
`num_gpu 24` en el `Modelfile` reserva sitio a whisper. Está explicado en los
comentarios del propio `Modelfile`. En una torre con una tarjeta normal **no
hace falta tocar nada**.
## Instalación Newelle es de [@qwersyk](https://github.com/qwersyk/Newelle) (GPL). Este carril es esa base con la capa de JARVIS encima. Para el núcleo actual, ver la rama `master`; para el banco de pruebas agéntico, la rama `JARVIS-GREEN`.
Necesita, en la máquina: [Ollama](https://ollama.com), whisper.cpp (compilado
para tu plataforma, ver [`config/README_whisper_gpu.md`](config/README_whisper_gpu.md))
y [Piper](https://github.com/rhasspy/piper).
```bash
git clone https://gitea.laenre.net/hacklab/JARVIS.git
cd JARVIS
./install.sh # comprueba servicios, crea el venv y el modelo de Ollama
```
`install.sh` no descarga modelos pesados ni toca el sistema sin avisar: solo
prepara el entorno de Python y crea `qwen3.5:4b-jarvis` a partir del `Modelfile`.
Después:
```bash
./nucleo/saber/indexa.py # (opcional) indexa tus apuntes para el RAG
./nucleo/arranca.sh # el asistente y el panel, en 127.0.0.1
```
El RAG funciona nada más clonar: el repo trae un índice público pre-construido, así
que no hace falta indexar para que sepa de metodología, comandos y herramientas.
### Comprobar el RAG
Si la búsqueda de conocimiento no responde bien, este diagnóstico revisa la cadena
entera y marca en verde/rojo cada eslabón —dependencias, índice, embedder,
búsqueda y Ollama— con una línea de qué hacer si algo falla:
```bash
./probar_rag.sh # comprobaciones rápidas
./probar_rag.sh --eval # además mide la calidad de recuperación (hit@k)
./probar_rag.sh --verboso # enseña el primer resultado de cada búsqueda
```
No necesita nada arrancado y sale con código distinto de cero si hay algún fallo
duro, así que sirve también para CI.
## Qué le puedes pedir
Habla en castellano, natural. **150 acciones** en el catálogo, agrupadas:
| Grupo | Nº | Ejemplos |
|---|---|---|
| sistema | 59 | "cuánto espacio queda", "la memoria", "la carga de CPU", "la temperatura" |
| firefox | 54 | "abre el Gmail", "abre YouTube", "abre el GitHub", "abre Maps" |
| ventanas | 12 | "manda la ventana a la izquierda", "maximiza", "centra la ventana" |
| ficheros | 11 | "busca un fichero llamado…", "lee el fichero…", "cuánto ocupa la carpeta…" |
| pentest | 8 | "escanea la red", "los puertos del host…", "resuelve el dominio…", "el whois de…" |
| oasis | 6 | "está corriendo Oasis", "cuántas conexiones tiene" |
Si lo que pides no está en el catálogo, el modelo lo resuelve con un **comando de
shell** (de solo lectura por defecto). Y si pregunta *cómo* se hace algo técnico,
puede **buscar en tus apuntes** (RAG) antes de responder.
El RAG es lo que hace que el asistente dé el comando que **tú ya has probado** en
vez de inventarse los flags. Y no es un adorno: medido sobre un conjunto de
preguntas de pentesting, la recuperación sube de **hit@1 48 % → 82 %** y
**hit@5 77 % → 95 %**. El método, el pipeline y esos números están en
[docs/rag.md](docs/rag.md).
Y **JARVIS elige su propio cerebro**: si una pregunta es más dura de lo normal
puede cambiar a un modelo más potente (`elegir_modelo`), y volver al rápido para
lo simple. Dile "usa un modelo más potente" o fija uno con `JARVIS_MODELO`.
## Órdenes con root — importante
Algunas órdenes que gestionan el sistema (instalar un paquete, reiniciar un
servicio, montar un disco) necesitan `sudo`. Conviene saber cómo se maneja,
porque es donde un asistente de voz puede hacer daño:
- **No pide la contraseña al arrancar.** Molestar de entrada echa para atrás, y
la mayoría de lo que se le pide no necesita root. Arranca y funciona sin nada.
- **Se pide solo cuando hace falta, y una vez.** La primera vez que una orden
necesita `sudo`, aparece un diálogo pidiendo tu contraseña. La tecleas, la
repites, y a partir de ahí el permiso queda abierto para la sesión.
- **Caduca a los 15 minutos** de inactividad, y al cerrar el panel. No es un
"root para siempre".
- La contraseña **nunca se escribe en disco**: vive solo en la memoria del
proceso mientras dura la sesión, y se pasa a `sudo -S` por la entrada estándar.
- **La voz no puede autoaprobar root.** El permiso de sesión ahorra re-teclear
la contraseña, nunca salta el pedirla la primera vez.
Si prefieres el modo seguro (bloquear el panel y pedir la contraseña al abrir,
para que nadie que pase por delante dé órdenes), arranca con `--candado`.
## Privacidad
Es la premisa, y se verifica. Ver [PRIVACIDAD.md](PRIVACIDAD.md). Este
repositorio **no incluye** los apuntes indexados ni el diario de conversación:
son del usuario, y el `.gitignore` los bloquea.
## Estructura
```
nucleo/ el asistente (oido, cerebro, boca, manos, cara, saber)
config/ configuración: modelo, voz, whisper
voz/ clonado de voz con XTTS y caché de frases
entrena/ fine-tuning con QLoRA
install.sh el instalador
```
## Estado y limitaciones
Proyecto en desarrollo. En el radar: acabar de generalizar los scripts de `voz/`
(algunos aún asumen la disposición del autor), las acciones de ventanas sobre
**Wayland** (hoy solo X11), y el fine-tuning de `entrena/`, pendiente de un
ajuste para modelos multimodales.
## Contribuir
Se agradecen los commits: es software libre y hay mucho por hacer. Las ideas
acotadas están en [ROADMAP.md](ROADMAP.md) (más gestores de ventanas, más
acciones, probar otros modelos, el RAG...) y cómo empezar en
[CONTRIBUTING.md](CONTRIBUTING.md). No hay que compilar nada: es Python, bash y
una página web.
## Licencia
[GPL-3.0](LICENSE). Software libre y copyleft: si lo modificas y lo distribuyes,
el resultado sigue siendo libre.

View file

@ -0,0 +1,89 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1060 700" font-family="'DejaVu Sans',Verdana,sans-serif">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0a1c33"/><stop offset="1" stop-color="#071224"/>
</linearGradient>
<marker id="ar" markerWidth="10" markerHeight="10" refX="7" refY="3" orient="auto">
<path d="M0,0 L7,3 L0,6 Z" fill="#38bdf8"/>
</marker>
</defs>
<rect width="1060" height="700" rx="16" fill="url(#bg)"/>
<rect x="1" y="1" width="1058" height="698" rx="16" fill="none" stroke="#1e3a5f" stroke-width="2"/>
<text x="40" y="52" fill="#eef4fb" font-size="30" font-weight="bold">JARVIS · carril azul</text>
<text x="40" y="78" fill="#38bdf8" font-size="16" font-weight="bold">Newelle</text>
<text x="140" y="78" fill="#7f9bbf" font-size="15">— el primer carril, sobre una app de escritorio GNOME</text>
<!-- sello congelado -->
<g transform="translate(830,38)">
<rect x="0" y="0" width="190" height="46" rx="10" fill="#12233d" stroke="#5b6b86" stroke-width="1.5"/>
<text x="16" y="22" fill="#a9c2e0" font-size="13" font-weight="bold">❄ CONGELADO</text>
<text x="16" y="39" fill="#6f8bb0" font-size="11">reemplazado por el naranja</text>
</g>
<!-- TU -->
<text x="40" y="128" fill="#7f9bbf" font-size="13" font-weight="bold" letter-spacing="1"></text>
<g>
<rect x="40" y="140" width="200" height="86" rx="10" fill="#0d2340" stroke="#22d3ee" stroke-width="1.5"/>
<text x="60" y="170" fill="#eef4fb" font-size="15" font-weight="bold">Voz y texto</text>
<text x="60" y="192" fill="#a9c2e0" font-size="12.5">en la ventana de Newelle</text>
<text x="60" y="210" fill="#7089aa" font-size="11.5">chat GTK · micrófono</text>
</g>
<!-- NEWELLE -->
<rect x="300" y="120" width="440" height="470" rx="14" fill="#0b2138" stroke="#38bdf8" stroke-width="2"/>
<text x="320" y="150" fill="#38bdf8" font-size="18" font-weight="bold">NEWELLE (GNOME)</text>
<text x="320" y="171" fill="#7f9bbf" font-size="12.5">app de escritorio · orquesta el asistente</text>
<text x="320" y="205" fill="#7dd3fc" font-size="13" font-weight="bold">Sus funciones (interruptores)</text>
<g fill="#0f2b47" stroke="#1d4166" font-size="11.5">
<rect x="320" y="214" width="188" height="26" rx="6"/><text x="330" y="231" fill="#cfe0f2">Documentos locales (RAG)</text>
<rect x="516" y="214" width="200" height="26" rx="6"/><text x="526" y="231" fill="#cfe0f2">Memoria de largo plazo</text>
<rect x="320" y="246" width="120" height="26" rx="6"/><text x="330" y="263" fill="#cfe0f2">TTS</text>
<rect x="448" y="246" width="268" height="26" rx="6"/><text x="458" y="263" fill="#cfe0f2">Virtualización de comandos</text>
<rect x="320" y="278" width="180" height="26" rx="6"/><text x="330" y="295" fill="#cfe0f2">Búsqueda web</text>
<rect x="508" y="278" width="208" height="26" rx="6"/><text x="518" y="295" fill="#cfe0f2">Extensiones (Python)</text>
</g>
<text x="320" y="340" fill="#7dd3fc" font-size="13" font-weight="bold">Lo que puso JARVIS encima</text>
<g font-size="11.5">
<rect x="320" y="349" width="188" height="26" rx="6" fill="#123350" stroke="#38bdf8"/><text x="330" y="366" fill="#bfe3fb">Persona + prompt propio</text>
<rect x="516" y="349" width="200" height="26" rx="6" fill="#123350" stroke="#38bdf8"/><text x="526" y="366" fill="#bfe3fb">Voz clonada (Piper)</text>
<rect x="320" y="381" width="188" height="26" rx="6" fill="#123350" stroke="#38bdf8"/><text x="330" y="398" fill="#bfe3fb">Modelo qwen local</text>
<rect x="516" y="381" width="200" height="26" rx="6" fill="#123350" stroke="#38bdf8"/><text x="526" y="398" fill="#bfe3fb">Herramientas propias</text>
</g>
<!-- por que se congelo -->
<rect x="320" y="430" width="396" height="142" rx="10" fill="#0e2036" stroke="#294a70" stroke-width="0"/>
<rect x="320" y="430" width="396" height="142" rx="10" fill="#0e2036" stroke="#294a70"/>
<text x="336" y="456" fill="#f0d488" font-size="13" font-weight="bold">Por qué se congeló</text>
<text x="336" y="480" fill="#b9cbe2" font-size="11.5">Newelle es potente y va sobre GNOME + Flatpak. Pero el</text>
<text x="336" y="498" fill="#b9cbe2" font-size="11.5">sandbox limita lo que el asistente puede tocar en la</text>
<text x="336" y="516" fill="#b9cbe2" font-size="11.5">máquina, y se quería control total del núcleo: latencia,</text>
<text x="336" y="534" fill="#b9cbe2" font-size="11.5">acciones rápidas sin pasar por el modelo, el HUD propio.</text>
<text x="336" y="558" fill="#7dd3fc" font-size="11.5" font-weight="bold">→ nació el carril naranja (núcleo a medida).</text>
<!-- EN TU MAQUINA -->
<text x="800" y="128" fill="#7f9bbf" font-size="13" font-weight="bold" letter-spacing="1">EN TU MÁQUINA</text>
<g>
<rect x="800" y="140" width="220" height="92" rx="10" fill="#0d2340" stroke="#22d3ee" stroke-width="1.5"/>
<text x="820" y="170" fill="#eef4fb" font-size="15" font-weight="bold">Cerebro</text>
<text x="820" y="192" fill="#a9c2e0" font-size="12.5">Ollama · qwen local</text>
<text x="820" y="211" fill="#7089aa" font-size="11.5">127.0.0.1 · sin nube</text>
</g>
<g>
<rect x="800" y="248" width="220" height="92" rx="10" fill="#0d2340" stroke="#22d3ee" stroke-width="1.5"/>
<text x="820" y="278" fill="#eef4fb" font-size="15" font-weight="bold">Voz</text>
<text x="820" y="300" fill="#a9c2e0" font-size="12.5">Piper (habla) · Whisper (oye)</text>
<text x="820" y="319" fill="#7089aa" font-size="11.5">local, la misma cadena de audio</text>
</g>
<g stroke="#38bdf8" stroke-width="2" fill="none" marker-end="url(#ar)">
<path d="M240,183 L296,220"/>
<path d="M740,186 L796,186"/>
<path d="M740,300 L796,294"/>
</g>
<text x="530" y="632" fill="#38bdf8" font-size="13" font-weight="bold" text-anchor="middle">Software libre (GPL) · corría 100 % en local</text>
<text x="530" y="656" fill="#7089aa" font-size="12" text-anchor="middle">el punto de partida: rápido de montar, con RAG, memoria y voz de serie</text>
<text x="530" y="680" fill="#546c8c" font-size="11" text-anchor="middle">la lección que se llevó el naranja: buenas ideas, pero el sandbox y la latencia pedían un núcleo propio</text>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
azul/docs/panel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

File diff suppressed because one or more lines are too long

View file

@ -36,20 +36,6 @@ CATALOGOS = [
"LISTAS", "CC0-1.0", True), "LISTAS", "CC0-1.0", True),
("awesome-hacking", os.path.join("_pages", "index.rst"), "rst", ("awesome-hacking", os.path.join("_pages", "index.rst"), "rst",
"awesome-hacking (jekil)", "PENTESTERS", "", False), "awesome-hacking (jekil)", "PENTESTERS", "", False),
# Segunda tanda (16-17 ago). publico=False hasta verificar licencia: no salen
# al indice publico, solo al personal que usa JARVIS en esta maquina.
("awesome-sysadmin", "README.md", "md", "awesome-sysadmin", "SISTEMA", "", False),
("awesome-cli-apps", "readme.md", "md", "awesome-cli-apps", "SISTEMA", "", False),
("awesome-security", "README.md", "md", "awesome-security", "PENTESTERS", "", False),
("awesome-pentest", "README.md", "md", "awesome-pentest", "PENTESTERS", "", False),
("awesome-shell", "README.md", "md", "awesome-shell", "SISTEMA", "", False),
("awesome-linux", "README.md", "md", "awesome-linux", "SISTEMA", "", False),
# Tercera tanda (madrugada 17 ago). Todas publico=False hasta revisar licencia.
("awesome-osint", "README.md", "md", "awesome-osint", "PENTESTERS", "", False),
("awesome-devops", "README.md", "md", "awesome-devops", "SISTEMA", "", False),
("awesome-docker", "README.md", "md", "awesome-docker", "SISTEMA", "", False),
("awesome-go", "README.md", "md", "awesome-go", "SISTEMA", "", False),
("awesome-python", "README.md", "md", "awesome-python", "SISTEMA", "", False),
] ]
# markdown: - [Nombre](http...) - Descripcion ... `Lic` `Lenguaje` # markdown: - [Nombre](http...) - Descripcion ... `Lic` `Lenguaje`

View file

@ -1,61 +0,0 @@
#!/usr/bin/env python3
"""Busca en el indice del RAG desde la linea de comandos.
busca_cli.py "como saco una shell reversa"
busca_cli.py --n 3 "gestor de contraseñas autoalojado"
busca_cli.py --json "kerberoasting"
Es el mismo motor que usa el cerebro del naranja (saber.busca), expuesto para que
lo llame cualquiera: un script, o un agente por su herramienta de terminal. Asi el
carril verde (Hermes) puede consultar tus apuntes sin duplicar el RAG.
"""
import argparse
import json
import os
import sys
AQUI = os.path.dirname(os.path.abspath(__file__))
NUCLEO = os.path.dirname(AQUI)
if NUCLEO not in sys.path:
sys.path.insert(0, NUCLEO)
def main():
p = argparse.ArgumentParser(description="Busca en los apuntes indexados (RAG)")
p.add_argument("consulta", nargs="+", help="lo que quieres buscar")
p.add_argument("--n", type=int, default=5, help="cuantos resultados (def. 5)")
p.add_argument("--json", action="store_true", help="salida JSON en vez de texto")
a = p.parse_args()
from saber import busca
if not busca.disponible():
print("no hay indice: corre nucleo/saber/indexa.py", file=sys.stderr)
return 1
consulta = " ".join(a.consulta)
res = busca.busca(consulta, cuantos=a.n)
if a.json:
salida = [{"herramienta": r.get("herramienta"),
"fuente": r.get("fuente") or r.get("perfil"),
"fichero": r.get("fichero"),
"texto": r.get("texto", "")[:600]} for r in res]
print(json.dumps(salida, ensure_ascii=False, indent=2))
return 0
if not res:
print("sin resultados")
return 0
for i, r in enumerate(res, 1):
fuente = r.get("fuente") or r.get("perfil") or ""
cabecera = f"{i}. {r.get('herramienta', '?')}"
if fuente:
cabecera += f" ({fuente})"
print(cabecera)
print(" " + r.get("texto", "").strip().replace("\n", "\n ")[:500])
print()
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -43,15 +43,3 @@
{"pregunta": "como escaneo redes wifi y capturo handshakes", "espera": ["airodump", "aircrack", "handshake", "wpa", "airgeddon"]} {"pregunta": "como escaneo redes wifi y capturo handshakes", "espera": ["airodump", "aircrack", "handshake", "wpa", "airgeddon"]}
{"pregunta": "usar crackmapexec para barrer una red windows", "espera": ["crackmapexec", "cme ", "smb", "--shares", "spray"]} {"pregunta": "usar crackmapexec para barrer una red windows", "espera": ["crackmapexec", "cme ", "smb", "--shares", "spray"]}
{"pregunta": "leer un hash ntlm y reutilizarlo por la red", "espera": ["ntlm", "hash", "relay", "responder", "ntlmrelayx"]} {"pregunta": "leer un hash ntlm y reutilizarlo por la red", "espera": ["ntlm", "hash", "relay", "responder", "ntlmrelayx"]}
{"pregunta": "alternativa self-hosted a google analytics", "espera": ["analytic", "plausible", "matomo", "goatcounter"]}
{"pregunta": "servidor multimedia para ver peliculas en casa", "espera": ["jellyfin", "plex", "streaming", "media"]}
{"pregunta": "gestor de contraseñas autoalojado", "espera": ["password", "vaultwarden", "bitwarden", "contrasen"]}
{"pregunta": "herramienta de linea de comandos para procesar json", "espera": ["jq", "jp", "fx", "json"]}
{"pregunta": "monitorizar los recursos del sistema en linux", "espera": ["htop", "netdata", "monitor", "glances"]}
{"pregunta": "framework de plugins para la shell zsh", "espera": ["zsh", "oh-my-zsh", "prezto", "shell"]}
{"pregunta": "agente de inteligencia artificial para pentesting automatico", "espera": ["strix", "pentest", "vulnerab"]}
{"pregunta": "programa para modelado 3d y renderizado", "espera": ["blender", "3d"]}
{"pregunta": "software para grabar y mezclar audio multipista", "espera": ["ardour", "audio"]}
{"pregunta": "herramienta para automatizar el hogar domotica", "espera": ["home", "iot", "domot", "assistant"]}
{"pregunta": "distribucion de linux para privacidad", "espera": ["tails", "privac", "linux"]}
{"pregunta": "escaner de vulnerabilidades web", "espera": ["nuclei", "nikto", "sqlmap", "scanner", "vulnerab"]}

View file

@ -1,71 +0,0 @@
#!/usr/bin/env bash
# Reentrenamiento nocturno AMPLIADO del RAG. Encadena las fases y las deja
# corriendo horas, sin tocar gitea. Cada fase es reanudable por su cuenta.
#
# ./reentrena_noche.sh
#
# Fases:
# 1. Espera a que termine cualquier reindex en curso.
# 2. Sintesis FRESCA de todo el contenido nuevo (conocimiento + catalogo +
# apps instaladas + docs de strix) -> preguntas-gancho.
# 3. Reindex personal con los ganchos horneados.
# 4. Evaluacion hit@k (numeros).
# 5. Refresco de COFRE: cosecha + sintesis de los apuntes.
# 6. Reindex final + evaluacion.
#
# Todo el ruido va al log; a la salida estandar solo hitos con hora.
set -uo pipefail
AQUI=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)
NUCLEO=$(cd "$AQUI/../.." && pwd)
PY="$NUCLEO/venv/bin/python"
[ -x "$PY" ] || PY=python3
DATOS="$NUCLEO/datos"
LOG="${1:-$DATOS/reentrena_noche.log}"
hora() { date +%H:%M:%S; }
hito() { echo "[$(hora)] $*" | tee -a "$LOG"; }
cd "$NUCLEO"
hito "=== ARRANCA reentrenamiento nocturno ampliado ==="
# --- Fase 1: no competir con un reindex en curso ---
while pgrep -f "saber.indexa" | grep -qv $$ 2>/dev/null; do
sleep 20
done
hito "fase 1 ok: no hay reindex compitiendo"
# --- Fase 2: sintesis fresca del contenido nuevo ---
hito "fase 2: sintesis del catalogo+nuevo (fresca, puede tardar 1-2 h)"
rm -f "$DATOS/sintesis-catalogo.jsonl" "$DATOS/sintesis-catalogo.jsonl.hecho"
"$PY" -m saber.enriquece.sintetiza_catalogo >>"$LOG" 2>&1
hito "fase 2 ok: $(wc -l < "$DATOS/sintesis-catalogo.jsonl" 2>/dev/null) preguntas-gancho nuevas"
# --- Fase 3: reindex con los ganchos ---
hito "fase 3: reindex personal con ganchos"
"$PY" -m saber.indexa >>"$LOG" 2>&1
hito "fase 3 ok: indice reconstruido"
# --- Fase 4: evaluacion ---
hito "fase 4: evaluacion hit@k"
"$PY" -m saber.enriquece.evalua "$DATOS/saber.jsonl" "$DATOS/saber.jsonl" 2>>"$LOG" \
| grep -iE "hit@|sim" | head -3 | tee -a "$LOG"
# --- Fase 5: refresco de COFRE (cosecha + sintesis) ---
if [ "${SIN_COFRE:-0}" = "1" ]; then
hito "fase 5: OMITIDA (SIN_COFRE=1, el refresco de COFRE ya se hizo)"
else
hito "fase 5: refresco de COFRE (cosecha + sintesis, largo)"
"$PY" -m saber.enriquece.cosecha >>"$LOG" 2>&1 && hito " cosecha ok" || hito " cosecha fallo (sigo)"
rm -f "$DATOS/sintesis.jsonl.hecho"
"$PY" -m saber.enriquece.sintetiza >>"$LOG" 2>&1 && hito " sintesis COFRE ok" || hito " sintesis COFRE fallo (sigo)"
fi
# --- Fase 6: reindex final + evaluacion ---
hito "fase 6: reindex final"
"$PY" -m saber.indexa >>"$LOG" 2>&1
hito "fase 6: evaluacion final"
"$PY" -m saber.enriquece.evalua "$DATOS/saber.jsonl" "$DATOS/saber.jsonl" 2>>"$LOG" \
| grep -iE "hit@|sim" | head -3 | tee -a "$LOG"
hito "=== FIN del reentrenamiento nocturno ==="

View file

@ -61,14 +61,6 @@ def _fragmentos():
frags.append(e) frags.append(e)
for e in awesome.entradas(): # catalogo: ya trae `publico` for e in awesome.entradas(): # catalogo: ya trae `publico`
frags.append(e) frags.append(e)
for e in indexa._instalado(): # apps de esta maquina: personal
e = dict(e)
e["publico"] = False
frags.append(e)
for e in indexa._docs_extra(): # docs de strix, etc.: personal
e = dict(e)
e["publico"] = False
frags.append(e)
frags.sort(key=lambda d: (0 if d.get("publico") else 1, frags.sort(key=lambda d: (0 if d.get("publico") else 1,
0 if d.get("tipo") == "metodologia" else 1)) 0 if d.get("tipo") == "metodologia" else 1))
return frags return frags

View file

@ -200,57 +200,6 @@ def _ganchos(solo_publico=False):
return ganchos return ganchos
def _instalado():
"""Las aplicaciones instaladas en esta maquina (instalado.py). Personal."""
try:
try:
from saber import instalado
except ImportError:
import instalado
except ImportError:
return []
fragmentos = []
for e in instalado.entradas():
e.pop("publico", None)
fragmentos.append(e)
return fragmentos
# Repos de ~/COFRE/CODERS cuyos docs (markdown) vale la pena indexar aunque no
# sean un perfil de COFRE. Personal: son herramientas de terceros clonadas aqui.
DOCS_REPOS = [
("strix", "PENTESTERS", ["README.md", "AGENTS.md", "docs", "benchmarks"]),
]
def _docs_extra():
"""Docs markdown de herramientas clonadas en CODERS (strix, etc.). Personal."""
coders = os.path.join(COFRE, "CODERS")
fragmentos = []
for nombre, perfil, rutas in DOCS_REPOS:
base = os.path.join(coders, nombre)
if not os.path.isdir(base):
continue
ficheros = []
for r in rutas:
p = os.path.join(base, r)
if os.path.isfile(p) and p.endswith(".md"):
ficheros.append(p)
elif os.path.isdir(p):
for raiz, dirs, fs in os.walk(p):
if IGNORA.search(raiz):
dirs[:] = []
continue
ficheros += [os.path.join(raiz, f) for f in fs if f.endswith(".md")]
for doc in ficheros:
for trozo in trocea(doc):
fragmentos.append({
"tipo": "metodologia", "herramienta": nombre, "perfil": perfil,
"tema": nombre, "fichero": os.path.relpath(doc, coders),
"texto": trozo})
return fragmentos
def _conocimiento(): def _conocimiento():
"""El pack de metodologia y manuales que viene con el repo (conocimiento/).""" """El pack de metodologia y manuales que viene con el repo (conocimiento/)."""
fragmentos = [] fragmentos = []
@ -376,24 +325,14 @@ def construye(solo_cuenta=False, solo_cofre=False):
if catalogo: if catalogo:
print(f" CATALOGO {len(catalogo):5d} fragmentos (awesome lists)") print(f" CATALOGO {len(catalogo):5d} fragmentos (awesome lists)")
instalado = _instalado()
if instalado:
print(f" INSTALADO {len(instalado):5d} apps/paquetes de esta maquina")
docs_extra = _docs_extra()
if docs_extra:
print(f" DOCS-CODERS {len(docs_extra):5d} fragmentos (strix, etc.)")
ganchos = _ganchos(solo_publico=False) ganchos = _ganchos(solo_publico=False)
if ganchos: if ganchos:
print(f" GANCHOS {len(ganchos):5d} preguntas sintetizadas") print(f" GANCHOS {len(ganchos):5d} preguntas sintetizadas")
todo = (fichas + fragmentos + sistema + conocimiento + catalogo todo = fichas + fragmentos + sistema + conocimiento + catalogo + ganchos
+ instalado + docs_extra + ganchos)
print(f"\n total: {len(fichas)} fichas + {len(fragmentos)} apuntes + " print(f"\n total: {len(fichas)} fichas + {len(fragmentos)} apuntes + "
f"{len(sistema)} sistema + {len(conocimiento)} conocimiento + " f"{len(sistema)} sistema + {len(conocimiento)} conocimiento + "
f"{len(catalogo)} catalogo + {len(instalado)} instalado + " f"{len(catalogo)} catalogo + {len(ganchos)} ganchos = {len(todo)} entradas")
f"{len(docs_extra)} docs + {len(ganchos)} ganchos = {len(todo)} entradas")
if solo_cuenta: if solo_cuenta:
return 0 return 0

View file

@ -1,140 +0,0 @@
#!/usr/bin/env python3
"""Indexa las aplicaciones instaladas en ESTA maquina: que hay y para que sirve.
./instalado.py di cuantas entradas saldrian
./instalado.py --volcado imprime unas cuantas de muestra
Dos fuentes, ambas locales:
1. Los .desktop (/usr/share/applications, ~/.local, flatpak): las apps que el
usuario ve y lanza, con su nombre, para que sirven (Comment) y el comando.
2. Los paquetes que el usuario instalo a mano (apt-mark showmanual), con su
descripcion de una linea. No las dependencias: eso es ruido.
Es privado por definicion dice que software tiene esta maquina, asi que
indexa.py lo mete SOLO en el indice personal, nunca en el publico ni en gitea.
"""
import glob
import os
import re
import subprocess
import sys
DIRS_DESKTOP = [
"/usr/share/applications",
os.path.expanduser("~/.local/share/applications"),
"/var/lib/flatpak/exports/share/applications",
os.path.expanduser("~/.local/share/flatpak/exports/share/applications"),
"/var/lib/snapd/desktop/applications",
]
def _campo(texto, clave):
m = re.search(r"^%s=(.+)$" % re.escape(clave), texto, re.M)
return m.group(1).strip() if m else ""
def _apps_desktop():
vistos = set()
for d in DIRS_DESKTOP:
for ruta in sorted(glob.glob(os.path.join(d, "*.desktop"))):
base = os.path.basename(ruta)
if base in vistos:
continue
vistos.add(base)
try:
with open(ruta, encoding="utf-8", errors="ignore") as f:
txt = f.read()
except OSError:
continue
if _campo(txt, "NoDisplay").lower() == "true":
continue
nombre = _campo(txt, "Name")
if not nombre:
continue
comentario = _campo(txt, "Comment") or _campo(txt, "GenericName")
categorias = _campo(txt, "Categories").replace(";", ", ").strip(", ")
exe = re.sub(r"%[a-zA-Z]", "", _campo(txt, "Exec")).strip()
cmd = exe.split()[0] if exe else ""
partes = [f"{nombre}"]
if comentario:
partes.append(f"{comentario}")
if cmd:
partes.append(f"(se lanza con: {cmd})")
if categorias:
partes.append(f"[{categorias}]")
yield {
"tipo": "instalado", "herramienta": nombre, "perfil": "SISTEMA",
"publico": False, "tema": categorias.lower(),
"fichero": f"app instalada: {base}",
"texto": "Aplicacion instalada: " + " ".join(partes),
}
def _paquetes_manuales():
"""Los paquetes apt que instalo el usuario (no dependencias), con su resumen."""
try:
man = subprocess.run(["apt-mark", "showmanual"], capture_output=True,
text=True, timeout=30).stdout.split()
except (OSError, subprocess.SubprocessError):
return
if not man:
return
# resumen de cada uno en un solo dpkg-query (rapido)
try:
salida = subprocess.run(
["dpkg-query", "-W", "-f=${Package}\t${binary:Summary}\n", *man],
capture_output=True, text=True, timeout=60).stdout
except (OSError, subprocess.SubprocessError):
return
lineas = []
for l in salida.splitlines():
if "\t" not in l:
continue
pkg, resumen = l.split("\t", 1)
if resumen.strip():
lineas.append(f"{pkg}: {resumen.strip()}")
# se agrupan en fragmentos (no una entrada por paquete: son cientos)
buffer = "Paquetes que el usuario instalo a mano en esta maquina:\n"
for ln in lineas:
if len(buffer) + len(ln) > 1100 and buffer:
yield _pkg_entry(buffer)
buffer = "Paquetes instalados (cont.):\n"
buffer += "- " + ln + "\n"
if buffer.strip():
yield _pkg_entry(buffer)
def _pkg_entry(texto):
return {"tipo": "instalado", "herramienta": "paquetes apt", "perfil": "SISTEMA",
"publico": False, "tema": "paquetes", "fichero": "apt-mark showmanual",
"texto": texto.strip()}
def entradas():
yield from _apps_desktop()
yield from _paquetes_manuales()
def main():
import argparse
p = argparse.ArgumentParser()
p.add_argument("--volcado", action="store_true")
a = p.parse_args()
apps = pkgs = 0
muestras = []
for e in entradas():
if e["herramienta"] == "paquetes apt":
pkgs += 1
else:
apps += 1
if len(muestras) < 6:
muestras.append(e)
print(f" {apps} apps (.desktop) + {pkgs} fragmentos de paquetes apt")
if a.volcado:
for e in muestras:
print(" -", e["texto"][:160].replace("\n", " "))
return 0
if __name__ == "__main__":
sys.exit(main())