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.
190 lines
6.5 KiB
Python
Executable file
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())
|