JARVIS/nucleo/saber/instalado.py
sito 7c5381eea7 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.
2026-08-18 12:14:57 +02:00

140 lines
4.8 KiB
Python
Executable file

#!/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())