saber: lo instalado en la maquina y los docs de repos clonados entran al indice

instalado.py lee los .desktop y los paquetes para saber que hay en el equipo y
para que sirve cada cosa. indexa.py lo engancha, y con el los docs markdown de
las herramientas clonadas en CODERS. Las dos fuentes son personales: se quedan
en el indice de la maquina y no entran en el publico.

busca_cli.py expone el mismo motor que usa el cerebro (saber.busca) para
consultar el indice desde la terminal, con --n y --json.

reentrena_noche.sh encadena las fases del reentrenamiento largo y las deja
corriendo horas; cada fase es reanudable por su cuenta.

eval del indice vivo: hit@1 43/57 (75%), hit@5 55/57 (96%), sim 0.909.
This commit is contained in:
sito 2026-08-18 12:14:57 +02:00
parent b1f8118272
commit 7c5381eea7
7 changed files with 369 additions and 2 deletions

View file

@ -36,6 +36,20 @@ CATALOGOS = [
"LISTAS", "CC0-1.0", True),
("awesome-hacking", os.path.join("_pages", "index.rst"), "rst",
"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`

61
nucleo/saber/busca_cli.py Executable file
View file

@ -0,0 +1,61 @@
#!/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,3 +43,15 @@
{"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": "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

@ -0,0 +1,71 @@
#!/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,6 +61,14 @@ def _fragmentos():
frags.append(e)
for e in awesome.entradas(): # catalogo: ya trae `publico`
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,
0 if d.get("tipo") == "metodologia" else 1))
return frags

View file

@ -200,6 +200,57 @@ def _ganchos(solo_publico=False):
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():
"""El pack de metodologia y manuales que viene con el repo (conocimiento/)."""
fragmentos = []
@ -325,14 +376,24 @@ def construye(solo_cuenta=False, solo_cofre=False):
if catalogo:
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)
if ganchos:
print(f" GANCHOS {len(ganchos):5d} preguntas sintetizadas")
todo = fichas + fragmentos + sistema + conocimiento + catalogo + ganchos
todo = (fichas + fragmentos + sistema + conocimiento + catalogo
+ instalado + docs_extra + ganchos)
print(f"\n total: {len(fichas)} fichas + {len(fragmentos)} apuntes + "
f"{len(sistema)} sistema + {len(conocimiento)} conocimiento + "
f"{len(catalogo)} catalogo + {len(ganchos)} ganchos = {len(todo)} entradas")
f"{len(catalogo)} catalogo + {len(instalado)} instalado + "
f"{len(docs_extra)} docs + {len(ganchos)} ganchos = {len(todo)} entradas")
if solo_cuenta:
return 0

140
nucleo/saber/instalado.py Executable file
View file

@ -0,0 +1,140 @@
#!/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())