Compare commits
2 commits
master
...
JARVIS-BLU
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28a6a6e935 | ||
|
|
38a8daf9ab |
11 changed files with 1977 additions and 2371 deletions
200
README.md
200
README.md
|
|
@ -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.
|
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||

|
|
||||||
-76B900)
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||

|
## Para qué sirvió
|
||||||
|
|
||||||

|
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 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.
|
|
||||||
|
|
|
||||||
89
azul/docs/arquitectura.svg
Normal file
89
azul/docs/arquitectura.svg
Normal 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">TÚ</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
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
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
@ -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"]}
|
|
||||||
|
|
|
||||||
|
|
@ -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 ==="
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue