hanuscode 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hanus/__init__.py +5 -0
- hanus/__main__.py +10 -0
- hanus/action_handlers.py +76 -0
- hanus/action_parser.py +82 -0
- hanus/agent_runner.py +1445 -0
- hanus/analysis/__init__.py +5 -0
- hanus/analysis/debt.py +702 -0
- hanus/analysis/dependencies.py +475 -0
- hanus/cache/__init__.py +5 -0
- hanus/cache/response_cache.py +560 -0
- hanus/config.py +401 -0
- hanus/connectors/__init__.py +19 -0
- hanus/connectors/base.py +114 -0
- hanus/connectors/claude_connector.py +146 -0
- hanus/connectors/gemini_connector.py +141 -0
- hanus/connectors/glm_connector.py +160 -0
- hanus/connectors/ollama_connector.py +174 -0
- hanus/connectors/openai_connector.py +122 -0
- hanus/connectors/registry.py +26 -0
- hanus/context/__init__.py +7 -0
- hanus/context/manager.py +837 -0
- hanus/context/selective.py +626 -0
- hanus/error_recovery/__init__.py +5 -0
- hanus/error_recovery/auto_fix.py +605 -0
- hanus/hooks/__init__.py +5 -0
- hanus/hooks/manager.py +247 -0
- hanus/instincts/__init__.py +44 -0
- hanus/instincts/cli.py +372 -0
- hanus/instincts/detector.py +281 -0
- hanus/instincts/evolver.py +361 -0
- hanus/instincts/manager.py +343 -0
- hanus/instincts/types.py +253 -0
- hanus/logger.py +81 -0
- hanus/memory/__init__.py +8 -0
- hanus/memory/manager.py +265 -0
- hanus/memory/types.py +119 -0
- hanus/monitor.py +341 -0
- hanus/parallel/__init__.py +5 -0
- hanus/parallel/executor.py +300 -0
- hanus/permissions.py +182 -0
- hanus/plan/__init__.py +8 -0
- hanus/plan/mode.py +267 -0
- hanus/plan/models.py +152 -0
- hanus/plugin_manager.py +754 -0
- hanus/plugin_registry.py +391 -0
- hanus/plugins/__init__.py +1 -0
- hanus/plugins/arena.py +630 -0
- hanus/plugins/code_review.py +123 -0
- hanus/plugins/cortex.py +1750 -0
- hanus/plugins/deps_check.py +27 -0
- hanus/plugins/git_ops.py +33 -0
- hanus/plugins/metasploit.py +530 -0
- hanus/plugins/notes.py +583 -0
- hanus/plugins/search_code.py +59 -0
- hanus/plugins/searchsploit.py +495 -0
- hanus/plugins/strategist.py +175 -0
- hanus/plugins/webui.py +5200 -0
- hanus/profiles.py +479 -0
- hanus/profiles_builtin/__init__.py +0 -0
- hanus/profiles_builtin/architect/profile.yaml +12 -0
- hanus/profiles_builtin/architect/system_prompt.txt +71 -0
- hanus/profiles_builtin/deep/profile.yaml +12 -0
- hanus/profiles_builtin/deep/system_prompt.txt +66 -0
- hanus/profiles_builtin/developer/__init__.py +0 -0
- hanus/profiles_builtin/developer/profile.yaml +9 -0
- hanus/profiles_builtin/developer/system_prompt.txt +176 -0
- hanus/profiles_builtin/speed/profile.yaml +12 -0
- hanus/profiles_builtin/speed/system_prompt.txt +51 -0
- hanus/project_tools.py +177 -0
- hanus/query_engine.py +1594 -0
- hanus/rules/__init__.py +237 -0
- hanus/search/__init__.py +5 -0
- hanus/search/semantic.py +596 -0
- hanus/session_manager.py +547 -0
- hanus/skill_manager.py +702 -0
- hanus/skills/__init__.py +4 -0
- hanus/subagent/__init__.py +8 -0
- hanus/subagent/agents/__init__.py +253 -0
- hanus/subagent/manager.py +309 -0
- hanus/subagent/types.py +266 -0
- hanus/suggestions/__init__.py +5 -0
- hanus/suggestions/proactive.py +451 -0
- hanus/tasks/__init__.py +8 -0
- hanus/tasks/manager.py +330 -0
- hanus/tasks/models.py +106 -0
- hanus/terminal_prompt.py +166 -0
- hanus/tools.py +1849 -0
- hanus/ui.py +939 -0
- hanuscode-1.0.0.dist-info/METADATA +1151 -0
- hanuscode-1.0.0.dist-info/RECORD +93 -0
- hanuscode-1.0.0.dist-info/WHEEL +5 -0
- hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
- hanuscode-1.0.0.dist-info/top_level.txt +1 -0
hanus/memory/manager.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# hanus/memory/manager.py
|
|
2
|
+
"""
|
|
3
|
+
Gestor de memoria persistente entre sesiones.
|
|
4
|
+
|
|
5
|
+
Inspired by Claude Code's memory system with MEMORY.md index.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
from hanus.memory.types import MemoryType, MemoryEntry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MemoryManager:
|
|
16
|
+
"""
|
|
17
|
+
Gestiona la memoria persistente del agente.
|
|
18
|
+
|
|
19
|
+
Estructura de directorios:
|
|
20
|
+
.hanus/memory/
|
|
21
|
+
├── MEMORY.md # Índice de memorias
|
|
22
|
+
├── user.md # Preferencias del usuario
|
|
23
|
+
├── feedback.md # Lecciones aprendidas
|
|
24
|
+
├── project.md # Conocimiento del proyecto
|
|
25
|
+
└── reference/
|
|
26
|
+
├── patterns.md # Patrones de código
|
|
27
|
+
└── apis.md # Referencias de APIs
|
|
28
|
+
|
|
29
|
+
Features:
|
|
30
|
+
- Cargar/salvar memorias
|
|
31
|
+
- Índice automático en MEMORY.md
|
|
32
|
+
- Búsqueda de memorias
|
|
33
|
+
- Token-aware loading (solo memorías relevantes)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
MEMORY_DIR = ".hanus/memory"
|
|
37
|
+
INDEX_FILE = "MEMORY.md"
|
|
38
|
+
|
|
39
|
+
def __init__(self, root_dir: Path, max_memory_tokens: int = 8000):
|
|
40
|
+
self.root_dir = Path(root_dir)
|
|
41
|
+
self.memory_dir = self.root_dir / self.MEMORY_DIR
|
|
42
|
+
self.index_file = self.memory_dir / self.INDEX_FILE
|
|
43
|
+
self.max_tokens = max_memory_tokens
|
|
44
|
+
self._memories: Dict[str, MemoryEntry] = {}
|
|
45
|
+
self._ensure_dirs()
|
|
46
|
+
|
|
47
|
+
def _ensure_dirs(self):
|
|
48
|
+
"""Crea los directorios de memoria si no existen."""
|
|
49
|
+
self.memory_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
(self.memory_dir / "reference").mkdir(exist_ok=True)
|
|
51
|
+
|
|
52
|
+
def load_memories(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Carga todas las memorías y retorna un string formateado para contexto.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
String con todas las memorías formateadas para inyección en contexto
|
|
58
|
+
"""
|
|
59
|
+
self._load_all()
|
|
60
|
+
|
|
61
|
+
if not self._memories:
|
|
62
|
+
return ""
|
|
63
|
+
|
|
64
|
+
lines = ["## Memoria Persistente\n"]
|
|
65
|
+
|
|
66
|
+
# Agrupar por tipo
|
|
67
|
+
by_type: Dict[MemoryType, List[MemoryEntry]] = {}
|
|
68
|
+
for mem in self._memories.values():
|
|
69
|
+
by_type.setdefault(mem.memory_type, []).append(mem)
|
|
70
|
+
|
|
71
|
+
type_labels = {
|
|
72
|
+
MemoryType.USER: "### Preferencias del Usuario",
|
|
73
|
+
MemoryType.FEEDBACK: "### Lecciones Aprendidas",
|
|
74
|
+
MemoryType.PROJECT: "### Conocimiento del Proyecto",
|
|
75
|
+
MemoryType.REFERENCE: "### Referencias",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for mem_type in [MemoryType.USER, MemoryType.FEED, MemoryType.PROJECT, MemoryType.REFERENCE]:
|
|
79
|
+
if mem_type in by_type:
|
|
80
|
+
lines.append(type_labels[mem_type])
|
|
81
|
+
for mem in by_type[mem_type]:
|
|
82
|
+
lines.append(f"\n**{mem.name}**")
|
|
83
|
+
if mem.description:
|
|
84
|
+
lines.append(f"*{mem.description}*")
|
|
85
|
+
lines.append(mem.content)
|
|
86
|
+
lines.append("")
|
|
87
|
+
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
def _load_all(self):
|
|
91
|
+
"""Carga todas las memorias desde disco."""
|
|
92
|
+
self._memories.clear()
|
|
93
|
+
|
|
94
|
+
# Buscar todos los archivos .md
|
|
95
|
+
for md_file in self.memory_dir.rglob("*.md"):
|
|
96
|
+
if md_file.name == self.INDEX_FILE:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
content = md_file.read_text(encoding="utf-8")
|
|
101
|
+
mem = MemoryEntry.from_markdown(content)
|
|
102
|
+
mem.name = md_file.stem # Usar nombre del archivo
|
|
103
|
+
self._memories[mem.name] = mem
|
|
104
|
+
except Exception:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
def save_memory(
|
|
108
|
+
self,
|
|
109
|
+
name: str,
|
|
110
|
+
memory_type: MemoryType,
|
|
111
|
+
content: str,
|
|
112
|
+
description: str = "",
|
|
113
|
+
tags: List[str] = None
|
|
114
|
+
) -> MemoryEntry:
|
|
115
|
+
"""
|
|
116
|
+
Guarda una nueva memoria.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
name: Nombre identificador (se usará como nombre de archivo)
|
|
120
|
+
memory_type: Tipo de memoria
|
|
121
|
+
content: Contenido de la memoria
|
|
122
|
+
description: Descripción corta
|
|
123
|
+
tags: Etiquetas opcionales
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
La entrada de memoria guardada
|
|
127
|
+
"""
|
|
128
|
+
self._ensure_dirs()
|
|
129
|
+
|
|
130
|
+
# Sanitizar nombre
|
|
131
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name)
|
|
132
|
+
|
|
133
|
+
# Determinar directorio
|
|
134
|
+
if memory_type == MemoryType.REFERENCE:
|
|
135
|
+
mem_dir = self.memory_dir / "reference"
|
|
136
|
+
else:
|
|
137
|
+
mem_dir = self.memory_dir
|
|
138
|
+
|
|
139
|
+
mem_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
mem_file = mem_dir / f"{safe_name}.md"
|
|
141
|
+
|
|
142
|
+
# Crear entrada
|
|
143
|
+
entry = MemoryEntry(
|
|
144
|
+
name=safe_name,
|
|
145
|
+
memory_type=memory_type,
|
|
146
|
+
content=content,
|
|
147
|
+
description=description,
|
|
148
|
+
tags=tags or [],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Guardar archivo
|
|
152
|
+
mem_file.write_text(entry.to_markdown(), encoding="utf-8")
|
|
153
|
+
|
|
154
|
+
# Actualizar índice
|
|
155
|
+
self._update_index()
|
|
156
|
+
|
|
157
|
+
# Guardar en memoria
|
|
158
|
+
self._memories[safe_name] = entry
|
|
159
|
+
|
|
160
|
+
return entry
|
|
161
|
+
|
|
162
|
+
def _update_index(self):
|
|
163
|
+
"""Actualiza el archivo MEMORY.md con un índice de todas las memorias."""
|
|
164
|
+
self._load_all()
|
|
165
|
+
|
|
166
|
+
lines = [
|
|
167
|
+
"# Hanus Memory Index\n",
|
|
168
|
+
"Este archivo es generado automáticamente. No editar directamente.\n",
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
# Agrupar por tipo
|
|
172
|
+
by_type: Dict[MemoryType, List[MemoryEntry]] = {}
|
|
173
|
+
for mem in self._memories.values():
|
|
174
|
+
by_type.setdefault(mem.memory_type, []).append(mem)
|
|
175
|
+
|
|
176
|
+
type_labels = {
|
|
177
|
+
MemoryType.USER: "## Preferencias del Usuario",
|
|
178
|
+
MemoryType.FEEDBACK: "## Lecciones Aprendidas",
|
|
179
|
+
MemoryType.PROJECT: "## Proyecto",
|
|
180
|
+
MemoryType.REFERENCE: "## Referencias",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for mem_type, label in type_labels.items():
|
|
184
|
+
if mem_type in by_type:
|
|
185
|
+
lines.append(f"\n{label}\n")
|
|
186
|
+
for mem in sorted(by_type[mem_type], key=lambda m: m.name):
|
|
187
|
+
desc = mem.description[:100] if mem.description else mem.content[:100]
|
|
188
|
+
lines.append(f"- [{mem.name}]({mem.memory_type.value}/{mem.name}.md) — {desc}")
|
|
189
|
+
|
|
190
|
+
self.index_file.write_text("\n".join(lines), encoding="utf-8")
|
|
191
|
+
|
|
192
|
+
def get(self, name: str) -> Optional[MemoryEntry]:
|
|
193
|
+
"""Obtiene una memoria por nombre."""
|
|
194
|
+
if not self._memories:
|
|
195
|
+
self._load_all()
|
|
196
|
+
return self._memories.get(name)
|
|
197
|
+
|
|
198
|
+
def delete(self, name: str) -> bool:
|
|
199
|
+
"""Elimina una memoria por nombre."""
|
|
200
|
+
mem = self.get(name)
|
|
201
|
+
if not mem:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# Determinar archivo
|
|
205
|
+
if mem.memory_type == MemoryType.REFERENCE:
|
|
206
|
+
mem_file = self.memory_dir / "reference" / f"{name}.md"
|
|
207
|
+
else:
|
|
208
|
+
mem_file = self.memory_dir / f"{name}.md"
|
|
209
|
+
|
|
210
|
+
if mem_file.exists():
|
|
211
|
+
mem_file.unlink()
|
|
212
|
+
|
|
213
|
+
del self._memories[name]
|
|
214
|
+
self._update_index()
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def search(self, query: str, limit: int = 5) -> List[MemoryEntry]:
|
|
218
|
+
"""
|
|
219
|
+
Busca memorias que contengan el query.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
query: Texto a buscar
|
|
223
|
+
limit: Máximo de resultados
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Lista de memorias que coinciden
|
|
227
|
+
"""
|
|
228
|
+
if not self._memories:
|
|
229
|
+
self._load_all()
|
|
230
|
+
|
|
231
|
+
query_lower = query.lower()
|
|
232
|
+
results = []
|
|
233
|
+
|
|
234
|
+
for mem in self._memories.values():
|
|
235
|
+
score = 0
|
|
236
|
+
# Buscar en nombre (peso 2)
|
|
237
|
+
if query_lower in mem.name.lower():
|
|
238
|
+
score += 2
|
|
239
|
+
# Buscar en descripción (peso 1)
|
|
240
|
+
if mem.description and query_lower in mem.description.lower():
|
|
241
|
+
score += 1
|
|
242
|
+
# Buscar en contenido (peso 1)
|
|
243
|
+
if query_lower in mem.content.lower():
|
|
244
|
+
score += 1
|
|
245
|
+
# Buscar en tags (peso 2)
|
|
246
|
+
if any(query_lower in tag.lower() for tag in mem.tags):
|
|
247
|
+
score += 2
|
|
248
|
+
|
|
249
|
+
if score > 0:
|
|
250
|
+
results.append((score, mem))
|
|
251
|
+
|
|
252
|
+
results.sort(key=lambda x: -x[0])
|
|
253
|
+
return [mem for _, mem in results[:limit]]
|
|
254
|
+
|
|
255
|
+
def list_by_type(self, memory_type: MemoryType) -> List[MemoryEntry]:
|
|
256
|
+
"""Lista todas las memorias de un tipo."""
|
|
257
|
+
if not self._memories:
|
|
258
|
+
self._load_all()
|
|
259
|
+
return [mem for mem in self._memories.values() if mem.memory_type == memory_type]
|
|
260
|
+
|
|
261
|
+
def list_all(self) -> List[MemoryEntry]:
|
|
262
|
+
"""Lista todas las memorias."""
|
|
263
|
+
if not self._memories:
|
|
264
|
+
self._load_all()
|
|
265
|
+
return list(self._memories.values())
|
hanus/memory/types.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# hanus/memory/types.py
|
|
2
|
+
"""
|
|
3
|
+
Tipos de datos para el sistema de memoria.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import List, Dict, Any, Optional
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MemoryType(Enum):
|
|
13
|
+
"""
|
|
14
|
+
Tipos de memoria disponibles.
|
|
15
|
+
|
|
16
|
+
- USER: Preferencias del usuario, rol, conocimientos
|
|
17
|
+
- FEEDBACK: Lecciones aprendidas de errores y aciertos
|
|
18
|
+
- PROJECT: Conocimiento del proyecto actual
|
|
19
|
+
- REFERENCE: Referencias externas (APIs, patrones)
|
|
20
|
+
"""
|
|
21
|
+
USER = "user"
|
|
22
|
+
FEEDBACK = "feedback"
|
|
23
|
+
PROJECT = "project"
|
|
24
|
+
REFERENCE = "reference"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class MemoryEntry:
|
|
29
|
+
"""
|
|
30
|
+
Una entrada de memoria individual.
|
|
31
|
+
"""
|
|
32
|
+
name: str # Nombre identificador
|
|
33
|
+
memory_type: MemoryType # Tipo de memoria
|
|
34
|
+
content: str # Contenido de la memoria
|
|
35
|
+
description: str = "" # Descripción corta (para el índice)
|
|
36
|
+
created_at: float = field(default_factory=time.time)
|
|
37
|
+
updated_at: float = field(default_factory=time.time)
|
|
38
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
tags: List[str] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
42
|
+
"""Serializa la entrada a diccionario."""
|
|
43
|
+
return {
|
|
44
|
+
"name": self.name,
|
|
45
|
+
"type": self.memory_type.value,
|
|
46
|
+
"content": self.content,
|
|
47
|
+
"description": self.description,
|
|
48
|
+
"created_at": self.created_at,
|
|
49
|
+
"updated_at": self.updated_at,
|
|
50
|
+
"metadata": self.metadata,
|
|
51
|
+
"tags": self.tags,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, d: Dict[str, Any]) -> "MemoryEntry":
|
|
56
|
+
"""Deserializa desde diccionario."""
|
|
57
|
+
return cls(
|
|
58
|
+
name=d["name"],
|
|
59
|
+
memory_type=MemoryType(d.get("type", "project")),
|
|
60
|
+
content=d.get("content", ""),
|
|
61
|
+
description=d.get("description", ""),
|
|
62
|
+
created_at=d.get("created_at", time.time()),
|
|
63
|
+
updated_at=d.get("updated_at", time.time()),
|
|
64
|
+
metadata=d.get("metadata", {}),
|
|
65
|
+
tags=d.get("tags", []),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def to_markdown(self) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Formatea la memoria como markdown con frontmatter.
|
|
71
|
+
|
|
72
|
+
Formato compatible con Claude Code.
|
|
73
|
+
"""
|
|
74
|
+
lines = [
|
|
75
|
+
"---",
|
|
76
|
+
f"name: {self.name}",
|
|
77
|
+
f"description: {self.description}",
|
|
78
|
+
f"type: {self.memory_type.value}",
|
|
79
|
+
]
|
|
80
|
+
if self.tags:
|
|
81
|
+
lines.append(f"tags: {', '.join(self.tags)}")
|
|
82
|
+
lines.extend([
|
|
83
|
+
"---",
|
|
84
|
+
"",
|
|
85
|
+
self.content,
|
|
86
|
+
])
|
|
87
|
+
return "\n".join(lines)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_markdown(cls, content: str) -> "MemoryEntry":
|
|
91
|
+
"""Parsea una memoria desde formato markdown con frontmatter."""
|
|
92
|
+
import re
|
|
93
|
+
|
|
94
|
+
# Parsear frontmatter
|
|
95
|
+
frontmatter = {}
|
|
96
|
+
if content.startswith("---"):
|
|
97
|
+
parts = content.split("---", 2)
|
|
98
|
+
if len(parts) >= 3:
|
|
99
|
+
for line in parts[1].strip().split("\n"):
|
|
100
|
+
if ":" in line:
|
|
101
|
+
key, _, value = line.partition(":")
|
|
102
|
+
key = key.strip()
|
|
103
|
+
value = value.strip()
|
|
104
|
+
if key == "tags":
|
|
105
|
+
value = [t.strip() for t in value.split(",")]
|
|
106
|
+
frontmatter[key] = value
|
|
107
|
+
body = parts[2].strip()
|
|
108
|
+
else:
|
|
109
|
+
body = content
|
|
110
|
+
else:
|
|
111
|
+
body = content
|
|
112
|
+
|
|
113
|
+
return cls(
|
|
114
|
+
name=frontmatter.get("name", "unnamed"),
|
|
115
|
+
memory_type=MemoryType(frontmatter.get("type", "project")),
|
|
116
|
+
content=body,
|
|
117
|
+
description=frontmatter.get("description", ""),
|
|
118
|
+
tags=frontmatter.get("tags", []),
|
|
119
|
+
)
|