"""Indexar el conocimiento de la propia maquina: comandos, servidores, extensiones. El RAG de COFRE (indexa.py) sabe de las herramientas del usuario. Esto añade lo otro que pidio: que sepa hacer CUALQUIER cosa en GNU/Linux. La fuente ya esta en el disco —9.541 man pages, 3.000 comandos— y es la mejor que hay: la documentacion oficial de cada herramienta, escrita por quien la hizo. ## Que se extrae de cada man page, y por que no entera Una man page entera son cientos de lineas: todas las opciones, los ejemplos, las notas, los bugs. Meterla entera hace el indice enorme y recupera peor —hay tanto texto que la parte relevante se diluye—. Se coge lo que contesta "¿que hace y como se llama?": NAME una linea: que es SYNOPSIS como se invoca, con sus opciones DESCRIPTION los primeros parrafos, donde esta lo esencial Con eso, preguntar "como listo los puertos abiertos" recupera la man de ss o netstat con su sintaxis, y el cerebro construye el comando desde ahi. ## Solo los comandos que EXISTEN en esta maquina No las 9.541 man pages: muchas son de librerias de C, formatos de fichero y cosas que no se invocan desde la terminal. Se indexan las de los comandos que estan de verdad en el PATH (secciones 1 y 8), que es lo que el usuario puede pedir que se ejecute. """ import os import re import subprocess def _limpio(texto: str) -> str: # las man pages traen backspaces para negrita (c\bco\bom\bmo); se quitan texto = re.sub(r".\x08", "", texto) texto = re.sub(r"\x1b\[[0-9;]*m", "", texto) # colores ANSI return re.sub(r"[ \t]+", " ", texto) def _comandos_del_path() -> list: vistos = set() for carpeta in os.environ.get("PATH", "").split(":"): try: for nombre in os.listdir(carpeta): ruta = os.path.join(carpeta, nombre) if nombre not in vistos and os.access(ruta, os.X_OK) \ and not os.path.isdir(ruta): vistos.add(nombre) except OSError: continue return sorted(vistos) def _man(comando: str) -> str: """NAME + SYNOPSIS + arranque de DESCRIPTION de un comando, o ''.""" try: r = subprocess.run(["man", comando], capture_output=True, text=True, timeout=8, env={**os.environ, "MANWIDTH": "80", "MAN_KEEP_FORMATTING": ""}) except (OSError, subprocess.TimeoutExpired): return "" if r.returncode != 0 or not r.stdout: return "" texto = _limpio(r.stdout) trozos = [] seccion = None buffer = [] # se cortan las secciones por sus titulos en mayuscula al margen izquierdo for linea in texto.splitlines(): cabecera = re.match(r"^([A-Z][A-Z ]{2,20})$", linea.strip()) if cabecera: if seccion in ("NAME", "SYNOPSIS", "DESCRIPTION") and buffer: trozos.append((seccion, "\n".join(buffer).strip())) seccion = cabecera.group(1).strip() buffer = [] if seccion in ("OPTIONS", "SEE ALSO", "AUTHOR", "COPYRIGHT", "REPORTING BUGS", "EXAMPLES"): seccion = None # dejar de acumular elif seccion: buffer.append(linea) if seccion in ("NAME", "SYNOPSIS", "DESCRIPTION") and buffer: trozos.append((seccion, "\n".join(buffer).strip())) partes = [] for sec, cont in trozos: if sec == "DESCRIPTION": cont = cont[:600] # solo el arranque de la descripcion partes.append(cont) return "\n".join(partes).strip() def comandos(): """Una entrada por comando del PATH que tenga man page.""" for cmd in _comandos_del_path(): texto = _man(cmd) if len(texto) < 40: continue yield { "tipo": "comando", "herramienta": cmd, "perfil": "LINUX", "fichero": f"man {cmd}", "texto": texto[:1400], } def servidores(): """Una ficha por host del ssh config, para que sepa a que puede conectarse.""" ruta = os.path.expanduser("~/.ssh/config") if not os.path.exists(ruta): return host = None datos = {} def suelta(): if host and "*" not in host: hn = datos.get("hostname", host) usuario = datos.get("user", "") desc = (f"Servidor SSH «{host}»: se conecta con «ssh {host}». " f"Direccion {hn}." + (f" Usuario {usuario}." if usuario else "") + " Para ejecutar algo alli: ssh " + host + " ''.") return {"tipo": "servidor", "herramienta": host, "perfil": "SSH", "fichero": "~/.ssh/config", "texto": desc} return None for linea in open(ruta, encoding="utf-8", errors="ignore"): m = re.match(r"^\s*(\w+)\s+(.+?)\s*$", linea) if not m: continue clave, valor = m.group(1).lower(), m.group(2) if clave == "host": hecho = suelta() if hecho: yield hecho host, datos = valor.split()[0], {} elif host: datos[clave] = valor hecho = suelta() if hecho: yield hecho def extensiones_firefox(): """Que extensiones tiene puestas, para que sepa con que cuenta en el navegador. Aviso honesto de alcance: JARVIS puede ABRIR firefox y llevarlo a una URL, pero NO puede pulsar los botones de una extension —eso vive dentro del navegador, fuera del alcance de un comando de shell—. Lo que si puede es saber que tienes puestas y abrir lo que cada una gestiona por URL. """ import glob import json as _json base = os.path.expanduser("~/.mozilla/firefox") perfiles = glob.glob(os.path.join(base, "*", "extensions.json")) nombres = [] for p in perfiles: try: datos = _json.load(open(p, encoding="utf-8")) except (OSError, ValueError): continue for addon in datos.get("addons", []): if not addon.get("active") or addon.get("type") != "extension": continue nombre = ((addon.get("defaultLocale") or {}).get("name") or addon.get("id", "")) if nombre and nombre not in nombres: nombres.append(nombre) if nombres: yield { "tipo": "navegador", "herramienta": "firefox", "perfil": "NAVEGADOR", "fichero": "extensiones de firefox", "texto": ("Extensiones instaladas en Firefox: " + ", ".join(nombres) + ". JARVIS puede abrir Firefox y llevarlo a una URL; los " "botones de cada extension se pulsan dentro del navegador."), } def todo(): """Todas las entradas de sistema, para que indexa.py las sume a las de COFRE.""" from saber import glosario yield from glosario.entradas() # el glosario en castellano, primero yield from servidores() yield from extensiones_firefox() yield from comandos()