JARVIS/config/descubrir.py
sito 8e4bc8ad94 JARVIS: asistente de voz local para Linux
Nucleo propio: oye con whisper.cpp, piensa con un modelo de Ollama, habla
con Piper, y hace RAG sobre los apuntes del usuario. 100% local, sin
cuentas ni claves.

Escrito bajo una restriccion dura, 4 GB de VRAM: el cerebro y whisper
comparten tarjeta y solo caben porque estan dimensionados para ello. El
RAG usa embeddings estaticos con busqueda hibrida; la voz clonada se sirve
de una cache de frases.

Incluye instalador (install.sh), requisitos, y documentacion del stack,
del manejo de root y de las acciones. Los apuntes indexados y el diario NO
se incluyen: son privados y el .gitignore los bloquea.
2026-08-16 15:34:37 +02:00

190 lines
6.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""Cataloga las herramientas de COFRE para que JARVIS sepa cuales hay.
python3 ~/COFRE/CODERS/JARVIS/config/descubrir.py
Produce jarvis-herramientas.json: una ficha por herramienta con su nombre, que
hace, como se invoca y en que carpeta vive. NO es el catalogo de acciones
rapidas (esas son las ~20 que dices por voz a diario, escritas a mano). Esto es
el registro que el modelo consulta cuando le pides algo que no esta en el
catalogo, para componer el comando en vez de inventarselo.
Que hace y que NO hace:
- SI: encuentra los puntos de entrada reales (scripts con shebang y binarios
en los primeros niveles de cada proyecto, no los modulos internos).
- SI: saca una descripcion del README, del docstring o de la cabecera, SIN
ejecutar nada. Ejecutar un script desconocido con --help para leerlo seria
ejecutar un script desconocido, que es justo lo que no se quiere hacer a
ciegas.
- NO: ejecuta ninguna herramienta. Esto solo mira.
El resultado se revisa a mano antes de usarlo: es material de terceros y algun
"tool" sera un ejemplo o un exploit, no algo que quieras ofrecer.
"""
import ast
import json
import os
import re
import stat
import sys
COFRE = os.path.expanduser("~/COFRE")
SALIDA = os.path.expanduser("~/COFRE/CODERS/JARVIS/config/jarvis-herramientas.json")
CARPETAS = ["CODERS", "PENTESTERS", "AUTOMATAS", "DOSER", "HARD-WARERS",
"PHISHERS", "PROTECTORS", "REVERSERS", "SNEAKERS"]
# Solo los primeros niveles: un punto de entrada vive cerca de la raiz del
# proyecto, no enterrado en lib/ o en site-packages.
PROFUNDIDAD = 3
FUERA = {"node_modules", ".git", "venv", ".venv", "site-packages", "__pycache__",
"lib", "libs", "tests", "test", "docs", "examples", "vendor", "dist",
"build", ".tox", "exploitdb"}
# Nombres que casi nunca son la herramienta en si
IGNORAR_NOMBRES = {"setup.py", "conftest.py", "__init__.py", "test.py",
"tests.py", "wsgi.py", "manage.py", "conf.py"}
def es_ejecutable(ruta: str) -> bool:
try:
return bool(os.stat(ruta).st_mode & stat.S_IXUSR)
except OSError:
return False
def tiene_shebang(ruta: str) -> bool:
try:
with open(ruta, "rb") as f:
return f.read(2) == b"#!"
except OSError:
return False
def descripcion_readme(carpeta: str) -> str:
"""Primera linea con contenido de un README cercano."""
for nombre in ("README.md", "README.rst", "README.txt", "README"):
ruta = os.path.join(carpeta, nombre)
if not os.path.exists(ruta):
continue
try:
with open(ruta, errors="ignore") as f:
for linea in f:
limpia = linea.strip().lstrip("#").lstrip(">").strip()
# saltar titulos de una palabra, badges y separadores
if len(limpia) > 25 and not limpia.startswith(("![", "[!", "---", "===")):
return limpia[:200]
except OSError:
pass
return ""
def descripcion_script(ruta: str) -> str:
"""Docstring del modulo (Python) o cabecera de comentarios (shell)."""
try:
with open(ruta, errors="ignore") as f:
texto = f.read(4000)
except OSError:
return ""
if ruta.endswith(".py"):
try:
doc = ast.get_docstring(ast.parse(texto))
if doc:
return doc.strip().split("\n")[0][:200]
except (SyntaxError, ValueError):
pass
# cabecera de comentarios: las primeras lineas que empiezan por # tras el shebang
lineas = []
for linea in texto.splitlines()[1:8]:
s = linea.strip()
if s.startswith("#"):
limpio = s.lstrip("#").strip()
if len(limpio) > 15:
lineas.append(limpio)
elif lineas:
break
return " ".join(lineas)[:200]
def recorre():
herramientas = []
vistos = set()
for carpeta in CARPETAS:
raiz = os.path.join(COFRE, carpeta)
if not os.path.isdir(raiz):
continue
for dirpath, dirnames, ficheros in os.walk(raiz):
profundidad = dirpath[len(raiz):].count(os.sep)
if profundidad >= PROFUNDIDAD:
dirnames[:] = []
continue
dirnames[:] = [d for d in dirnames
if d not in FUERA and not d.startswith(".")]
for nombre in ficheros:
if nombre in IGNORAR_NOMBRES:
continue
ruta = os.path.join(dirpath, nombre)
ext = os.path.splitext(nombre)[1].lower()
es_script = ext in (".py", ".sh") and (tiene_shebang(ruta) or es_ejecutable(ruta))
es_binario = not ext and es_ejecutable(ruta) and not tiene_shebang(ruta)
if not (es_script or es_binario):
continue
clave = nombre.lower()
if clave in vistos:
continue
vistos.add(clave)
desc = descripcion_script(ruta) if es_script else ""
if not desc:
desc = descripcion_readme(dirpath)
herramientas.append({
"nombre": os.path.splitext(nombre)[0],
"carpeta": carpeta,
"ruta": ruta,
"tipo": "python" if ext == ".py" else "shell" if ext == ".sh" else "binario",
"descripcion": desc,
"invocar": (f"python3 {ruta}" if ext == ".py"
else f"bash {ruta}" if ext == ".sh"
else ruta),
})
return herramientas
def main():
print("recorriendo COFRE (solo lectura, no se ejecuta nada)...\n", flush=True)
tools = recorre()
porcarpeta = {}
con_desc = 0
for t in tools:
porcarpeta[t["carpeta"]] = porcarpeta.get(t["carpeta"], 0) + 1
if t["descripcion"]:
con_desc += 1
for carpeta in CARPETAS:
if carpeta in porcarpeta:
print(f" {carpeta:<13} {porcarpeta[carpeta]:>4} herramientas")
print(f"\n total {len(tools)}, con descripcion {con_desc} "
f"({100*con_desc//max(1,len(tools))} %)")
with open(SALIDA, "w") as f:
json.dump({"version": 1, "herramientas": tools}, f,
indent=2, ensure_ascii=False)
print(f"\nescrito {SALIDA}")
print("REVISALO a mano antes de usarlo: es material de terceros y algun")
print("'tool' sera un ejemplo o un exploit, no algo que quieras ofrecer.")
return 0
if __name__ == "__main__":
sys.exit(main())