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.
Files changed (93) hide show
  1. hanus/__init__.py +5 -0
  2. hanus/__main__.py +10 -0
  3. hanus/action_handlers.py +76 -0
  4. hanus/action_parser.py +82 -0
  5. hanus/agent_runner.py +1445 -0
  6. hanus/analysis/__init__.py +5 -0
  7. hanus/analysis/debt.py +702 -0
  8. hanus/analysis/dependencies.py +475 -0
  9. hanus/cache/__init__.py +5 -0
  10. hanus/cache/response_cache.py +560 -0
  11. hanus/config.py +401 -0
  12. hanus/connectors/__init__.py +19 -0
  13. hanus/connectors/base.py +114 -0
  14. hanus/connectors/claude_connector.py +146 -0
  15. hanus/connectors/gemini_connector.py +141 -0
  16. hanus/connectors/glm_connector.py +160 -0
  17. hanus/connectors/ollama_connector.py +174 -0
  18. hanus/connectors/openai_connector.py +122 -0
  19. hanus/connectors/registry.py +26 -0
  20. hanus/context/__init__.py +7 -0
  21. hanus/context/manager.py +837 -0
  22. hanus/context/selective.py +626 -0
  23. hanus/error_recovery/__init__.py +5 -0
  24. hanus/error_recovery/auto_fix.py +605 -0
  25. hanus/hooks/__init__.py +5 -0
  26. hanus/hooks/manager.py +247 -0
  27. hanus/instincts/__init__.py +44 -0
  28. hanus/instincts/cli.py +372 -0
  29. hanus/instincts/detector.py +281 -0
  30. hanus/instincts/evolver.py +361 -0
  31. hanus/instincts/manager.py +343 -0
  32. hanus/instincts/types.py +253 -0
  33. hanus/logger.py +81 -0
  34. hanus/memory/__init__.py +8 -0
  35. hanus/memory/manager.py +265 -0
  36. hanus/memory/types.py +119 -0
  37. hanus/monitor.py +341 -0
  38. hanus/parallel/__init__.py +5 -0
  39. hanus/parallel/executor.py +300 -0
  40. hanus/permissions.py +182 -0
  41. hanus/plan/__init__.py +8 -0
  42. hanus/plan/mode.py +267 -0
  43. hanus/plan/models.py +152 -0
  44. hanus/plugin_manager.py +754 -0
  45. hanus/plugin_registry.py +391 -0
  46. hanus/plugins/__init__.py +1 -0
  47. hanus/plugins/arena.py +630 -0
  48. hanus/plugins/code_review.py +123 -0
  49. hanus/plugins/cortex.py +1750 -0
  50. hanus/plugins/deps_check.py +27 -0
  51. hanus/plugins/git_ops.py +33 -0
  52. hanus/plugins/metasploit.py +530 -0
  53. hanus/plugins/notes.py +583 -0
  54. hanus/plugins/search_code.py +59 -0
  55. hanus/plugins/searchsploit.py +495 -0
  56. hanus/plugins/strategist.py +175 -0
  57. hanus/plugins/webui.py +5200 -0
  58. hanus/profiles.py +479 -0
  59. hanus/profiles_builtin/__init__.py +0 -0
  60. hanus/profiles_builtin/architect/profile.yaml +12 -0
  61. hanus/profiles_builtin/architect/system_prompt.txt +71 -0
  62. hanus/profiles_builtin/deep/profile.yaml +12 -0
  63. hanus/profiles_builtin/deep/system_prompt.txt +66 -0
  64. hanus/profiles_builtin/developer/__init__.py +0 -0
  65. hanus/profiles_builtin/developer/profile.yaml +9 -0
  66. hanus/profiles_builtin/developer/system_prompt.txt +176 -0
  67. hanus/profiles_builtin/speed/profile.yaml +12 -0
  68. hanus/profiles_builtin/speed/system_prompt.txt +51 -0
  69. hanus/project_tools.py +177 -0
  70. hanus/query_engine.py +1594 -0
  71. hanus/rules/__init__.py +237 -0
  72. hanus/search/__init__.py +5 -0
  73. hanus/search/semantic.py +596 -0
  74. hanus/session_manager.py +547 -0
  75. hanus/skill_manager.py +702 -0
  76. hanus/skills/__init__.py +4 -0
  77. hanus/subagent/__init__.py +8 -0
  78. hanus/subagent/agents/__init__.py +253 -0
  79. hanus/subagent/manager.py +309 -0
  80. hanus/subagent/types.py +266 -0
  81. hanus/suggestions/__init__.py +5 -0
  82. hanus/suggestions/proactive.py +451 -0
  83. hanus/tasks/__init__.py +8 -0
  84. hanus/tasks/manager.py +330 -0
  85. hanus/tasks/models.py +106 -0
  86. hanus/terminal_prompt.py +166 -0
  87. hanus/tools.py +1849 -0
  88. hanus/ui.py +939 -0
  89. hanuscode-1.0.0.dist-info/METADATA +1151 -0
  90. hanuscode-1.0.0.dist-info/RECORD +93 -0
  91. hanuscode-1.0.0.dist-info/WHEEL +5 -0
  92. hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
  93. hanuscode-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,475 @@
1
+ # hanus/analysis/dependencies.py
2
+ """
3
+ Analizador de dependencias de cΓ³digo.
4
+
5
+ Funcionalidades:
6
+ - AnΓ‘lisis de imports/dependencias
7
+ - DetecciΓ³n de dependencias circulares
8
+ - VisualizaciΓ³n de grafo de dependencias
9
+ - MΓ©tricas de acoplamiento
10
+ """
11
+ from __future__ import annotations
12
+ import ast
13
+ import re
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Dict, List, Set, Optional, Any, Tuple
17
+ from collections import defaultdict
18
+ from enum import Enum
19
+
20
+
21
+ class DependencyType(Enum):
22
+ """Tipo de dependencia."""
23
+ IMPORT = "import" # import X
24
+ FROM_IMPORT = "from" # from X import Y
25
+ RELATIVE = "relative" # from . import X
26
+ DYNAMIC = "dynamic" # __import__, importlib
27
+
28
+
29
+ class NodeType(Enum):
30
+ """Tipo de nodo en el grafo."""
31
+ FILE = "file"
32
+ MODULE = "module"
33
+ PACKAGE = "package"
34
+ CLASS = "class"
35
+ FUNCTION = "function"
36
+ VARIABLE = "variable"
37
+
38
+
39
+ @dataclass
40
+ class DependencyNode:
41
+ """Un nodo en el grafo de dependencias."""
42
+ id: str # Identificador ΓΊnico (ruta o nombre)
43
+ name: str # Nombre legible
44
+ type: NodeType
45
+ path: Optional[str] = None # Ruta del archivo si aplica
46
+ imports: List[str] = field(default_factory=list) # Lo que este nodo importa
47
+ imported_by: List[str] = field(default_factory=list) # Quien importa este nodo
48
+ exports: List[str] = field(default_factory=list) # Lo que exporta
49
+ loc: int = 0 # Lines of code
50
+ metadata: Dict[str, Any] = field(default_factory=dict)
51
+
52
+ def to_dict(self) -> Dict:
53
+ return {
54
+ "id": self.id,
55
+ "name": self.name,
56
+ "type": self.type.value,
57
+ "path": self.path,
58
+ "imports": self.imports,
59
+ "imported_by": self.imported_by,
60
+ "exports": self.exports,
61
+ "loc": self.loc,
62
+ "metadata": self.metadata,
63
+ }
64
+
65
+
66
+ @dataclass
67
+ class DependencyEdge:
68
+ """Una arista en el grafo de dependencias."""
69
+ source: str # ID del nodo origen
70
+ target: str # ID del nodo destino
71
+ type: DependencyType
72
+ symbols: List[str] = field(default_factory=list) # SΓ­mbolos importados
73
+ line: int = 0 # LΓ­nea donde aparece el import
74
+
75
+ def to_dict(self) -> Dict:
76
+ return {
77
+ "source": self.source,
78
+ "target": self.target,
79
+ "type": self.type.value,
80
+ "symbols": self.symbols,
81
+ "line": self.line,
82
+ }
83
+
84
+
85
+ @dataclass
86
+ class DependencyGraph:
87
+ """Grafo completo de dependencias."""
88
+ nodes: Dict[str, DependencyNode] = field(default_factory=dict)
89
+ edges: List[DependencyEdge] = field(default_factory=list)
90
+
91
+ # Índices para búsqueda rÑpida
92
+ _outgoing: Dict[str, List[str]] = field(default_factory=dict) # nodo -> [nodos destino]
93
+ _incoming: Dict[str, List[str]] = field(default_factory=dict) # nodo -> [nodos origen]
94
+
95
+ def add_node(self, node: DependencyNode) -> None:
96
+ """AΓ±ade un nodo al grafo."""
97
+ self.nodes[node.id] = node
98
+ if node.id not in self._outgoing:
99
+ self._outgoing[node.id] = []
100
+ if node.id not in self._incoming:
101
+ self._incoming[node.id] = []
102
+
103
+ def add_edge(self, edge: DependencyEdge) -> None:
104
+ """AΓ±ade una arista al grafo."""
105
+ self.edges.append(edge)
106
+ if edge.source not in self._outgoing:
107
+ self._outgoing[edge.source] = []
108
+ self._outgoing[edge.source].append(edge.target)
109
+
110
+ if edge.target not in self._incoming:
111
+ self._incoming[edge.target] = []
112
+ self._incoming[edge.target].append(edge.source)
113
+
114
+ def get_dependencies(self, node_id: str) -> List[str]:
115
+ """Obtiene los nodos de los que depende un nodo."""
116
+ return self._outgoing.get(node_id, [])
117
+
118
+ def get_dependents(self, node_id: str) -> List[str]:
119
+ """Obtiene los nodos que dependen de un nodo."""
120
+ return self._incoming.get(node_id, [])
121
+
122
+ def find_cycles(self) -> List[List[str]]:
123
+ """Encuentra todos los ciclos en el grafo."""
124
+ cycles = []
125
+ visited = set()
126
+ rec_stack = set()
127
+ path = []
128
+
129
+ def dfs(node: str) -> None:
130
+ visited.add(node)
131
+ rec_stack.add(node)
132
+ path.append(node)
133
+
134
+ for neighbor in self._outgoing.get(node, []):
135
+ if neighbor not in visited:
136
+ dfs(neighbor)
137
+ elif neighbor in rec_stack:
138
+ # Encontramos un ciclo
139
+ cycle_start = path.index(neighbor)
140
+ cycle = path[cycle_start:] + [neighbor]
141
+ cycles.append(cycle)
142
+
143
+ path.pop()
144
+ rec_stack.remove(node)
145
+
146
+ for node in self.nodes:
147
+ if node not in visited:
148
+ dfs(node)
149
+
150
+ return cycles
151
+
152
+ def get_coupling_metrics(self) -> Dict[str, Any]:
153
+ """Calcula mΓ©tricas de acoplamiento."""
154
+ afferent = {} # CuΓ‘ntos nodos dependen de este (incoming)
155
+ efferent = {} # De cuΓ‘ntos nodos depende este (outgoing)
156
+
157
+ for node_id in self.nodes:
158
+ afferent[node_id] = len(self._incoming.get(node_id, []))
159
+ efferent[node_id] = len(self._outgoing.get(node_id, []))
160
+
161
+ # Calcular inestabilidad: I = Ce / (Ca + Ce)
162
+ # I cercano a 0 = estable, I cercano a 1 = inestable
163
+ instability = {}
164
+ for node_id in self.nodes:
165
+ ca = afferent.get(node_id, 0)
166
+ ce = efferent.get(node_id, 0)
167
+ total = ca + ce
168
+ instability[node_id] = ce / total if total > 0 else 0
169
+
170
+ return {
171
+ "afferent_coupling": afferent, # Afferent coupling (Ca)
172
+ "efferent_coupling": efferent, # Efferent coupling (Ce)
173
+ "instability": instability,
174
+ "total_nodes": len(self.nodes),
175
+ "total_edges": len(self.edges),
176
+ }
177
+
178
+ def get_top_dependencies(self, limit: int = 10) -> List[Dict]:
179
+ """Obtiene los nodos mΓ‘s importados."""
180
+ incoming_counts = [
181
+ (node_id, len(self._incoming.get(node_id, [])))
182
+ for node_id in self.nodes
183
+ ]
184
+ incoming_counts.sort(key=lambda x: -x[1])
185
+
186
+ return [
187
+ {
188
+ "id": node_id,
189
+ "name": self.nodes[node_id].name,
190
+ "imported_by_count": count,
191
+ }
192
+ for node_id, count in incoming_counts[:limit]
193
+ ]
194
+
195
+ def to_dict(self) -> Dict:
196
+ """Convierte el grafo a diccionario para serializaciΓ³n."""
197
+ return {
198
+ "nodes": [n.to_dict() for n in self.nodes.values()],
199
+ "edges": [e.to_dict() for e in self.edges],
200
+ "metrics": self.get_coupling_metrics(),
201
+ }
202
+
203
+ def to_vis_format(self) -> Dict:
204
+ """Convierte el grafo a formato para visualizaciΓ³n (vis.js, d3.js)."""
205
+ nodes = []
206
+ for node in self.nodes.values():
207
+ # Determinar grupo/color
208
+ if node.type == NodeType.FILE:
209
+ group = "file"
210
+ elif node.type == NodeType.MODULE:
211
+ group = "module"
212
+ else:
213
+ group = "other"
214
+
215
+ nodes.append({
216
+ "id": node.id,
217
+ "label": node.name,
218
+ "group": group,
219
+ "title": f"{node.name}\nImports: {len(node.imports)}\nImported by: {len(node.imported_by)}",
220
+ "value": len(node.imported_by) + 1, # TamaΓ±o basado en uso
221
+ })
222
+
223
+ edges = []
224
+ for edge in self.edges:
225
+ edges.append({
226
+ "from": edge.source,
227
+ "to": edge.target,
228
+ "arrows": "to",
229
+ "title": f"{edge.type.value}: {', '.join(edge.symbols[:3])}",
230
+ })
231
+
232
+ return {"nodes": nodes, "edges": edges}
233
+
234
+
235
+ class DependencyAnalyzer:
236
+ """
237
+ Analiza dependencias de cΓ³digo en un proyecto.
238
+
239
+ Soporta:
240
+ - Python (.py)
241
+ - JavaScript/TypeScript (.js, .ts)
242
+ - JSON dependencies (package.json, requirements.txt)
243
+ """
244
+
245
+ PYTHON_IMPORT_PATTERNS = [
246
+ # import X
247
+ re.compile(r'^import\s+([a-zA-Z_][\w\.]*)', re.MULTILINE),
248
+ # from X import Y
249
+ re.compile(r'^from\s+([a-zA-Z_][\w\.]*)\s+import', re.MULTILINE),
250
+ ]
251
+
252
+ JS_IMPORT_PATTERNS = [
253
+ # import X from 'Y'
254
+ re.compile(r'''import\s+.*?\s+from\s+['"]([^'"]+)['"]'''),
255
+ # import 'X'
256
+ re.compile(r'''import\s+['"]([^'"]+)['"]'''),
257
+ # require('X')
258
+ re.compile(r'''require\s*\(\s*['"]([^'"]+)['"]\s*\)'''),
259
+ ]
260
+
261
+ def __init__(self, project_root: Path):
262
+ self.project_root = project_root
263
+ self._file_cache: Dict[str, str] = {}
264
+
265
+ def analyze_project(
266
+ self,
267
+ include_patterns: List[str] = None,
268
+ exclude_patterns: List[str] = None
269
+ ) -> DependencyGraph:
270
+ """
271
+ Analiza todo el proyecto y construye el grafo de dependencias.
272
+
273
+ Args:
274
+ include_patterns: Patrones de archivos a incluir (ej: ["*.py", "*.js"])
275
+ exclude_patterns: Patrones a excluir (ej: ["test_*", "__pycache__"])
276
+
277
+ Returns:
278
+ DependencyGraph con todas las dependencias
279
+ """
280
+ graph = DependencyGraph()
281
+
282
+ include_patterns = include_patterns or ["*.py", "*.js", "*.ts"]
283
+ exclude_patterns = exclude_patterns or ["__pycache__", "node_modules", ".git", "venv", "env"]
284
+
285
+ # Recopilar todos los archivos
286
+ files = self._collect_files(include_patterns, exclude_patterns)
287
+
288
+ # Analizar cada archivo
289
+ for file_path in files:
290
+ self._analyze_file(file_path, graph)
291
+
292
+ # Construir Γ­ndices inversos
293
+ self._build_reverse_indices(graph)
294
+
295
+ return graph
296
+
297
+ def analyze_file(self, file_path: Path) -> DependencyGraph:
298
+ """Analiza un solo archivo."""
299
+ graph = DependencyGraph()
300
+ self._analyze_file(file_path, graph)
301
+ return graph
302
+
303
+ def find_circular_dependencies(self, graph: DependencyGraph) -> List[List[str]]:
304
+ """Encuentra dependencias circulares."""
305
+ return graph.find_cycles()
306
+
307
+ def get_dependency_path(self, graph: DependencyGraph, from_node: str, to_node: str) -> List[str]:
308
+ """Encuentra el camino de dependencia entre dos nodos."""
309
+ visited = set()
310
+ path = []
311
+
312
+ def dfs(current: str) -> bool:
313
+ if current == to_node:
314
+ path.append(current)
315
+ return True
316
+
317
+ if current in visited:
318
+ return False
319
+
320
+ visited.add(current)
321
+ path.append(current)
322
+
323
+ for neighbor in graph.get_dependencies(current):
324
+ if dfs(neighbor):
325
+ return True
326
+
327
+ path.pop()
328
+ return False
329
+
330
+ dfs(from_node)
331
+ return path
332
+
333
+ # ══════════════════════════════════════════════════════════════════════════
334
+ # MÉTODOS PRIVADOS
335
+ # ══════════════════════════════════════════════════════════════════════════
336
+
337
+ def _collect_files(
338
+ self,
339
+ include_patterns: List[str],
340
+ exclude_patterns: List[str]
341
+ ) -> List[Path]:
342
+ """Recolecta archivos del proyecto."""
343
+ files = []
344
+
345
+ for pattern in include_patterns:
346
+ for file_path in self.project_root.rglob(pattern):
347
+ # Verificar exclusiones
348
+ rel_path = file_path.relative_to(self.project_root)
349
+ if any(exc in str(rel_path) for exc in exclude_patterns):
350
+ continue
351
+ files.append(file_path)
352
+
353
+ return files
354
+
355
+ def _analyze_file(self, file_path: Path, graph: DependencyGraph) -> None:
356
+ """Analiza un archivo y aΓ±ade sus dependencias al grafo."""
357
+ try:
358
+ content = file_path.read_text(encoding="utf-8", errors="replace")
359
+ except Exception:
360
+ return
361
+
362
+ rel_path = str(file_path.relative_to(self.project_root))
363
+ node_id = rel_path.replace("/", ".").replace("\\", ".")
364
+
365
+ # Crear nodo para el archivo
366
+ node = DependencyNode(
367
+ id=node_id,
368
+ name=file_path.name,
369
+ type=NodeType.FILE,
370
+ path=str(file_path),
371
+ loc=content.count("\n") + 1,
372
+ )
373
+
374
+ # Detectar imports segΓΊn el tipo de archivo
375
+ ext = file_path.suffix.lower()
376
+
377
+ if ext == ".py":
378
+ imports = self._parse_python_imports(content, file_path)
379
+ elif ext in (".js", ".ts", ".jsx", ".tsx"):
380
+ imports = self._parse_js_imports(content, file_path)
381
+ else:
382
+ imports = []
383
+
384
+ node.imports = [imp["module"] for imp in imports]
385
+
386
+ # AΓ±adir nodo y aristas
387
+ graph.add_node(node)
388
+
389
+ for imp in imports:
390
+ target_id = imp["module"]
391
+
392
+ # Crear nodo para el mΓ³dulo importado si no existe
393
+ if target_id not in graph.nodes:
394
+ graph.add_node(DependencyNode(
395
+ id=target_id,
396
+ name=target_id.split(".")[-1],
397
+ type=NodeType.MODULE,
398
+ ))
399
+
400
+ # AΓ±adir arista
401
+ graph.add_edge(DependencyEdge(
402
+ source=node_id,
403
+ target=target_id,
404
+ type=DependencyType(imp["type"]),
405
+ symbols=imp.get("names", []),
406
+ line=imp.get("line", 0),
407
+ ))
408
+
409
+ def _parse_python_imports(self, content: str, file_path: Path) -> List[Dict]:
410
+ """Parsea imports de Python usando AST."""
411
+ imports = []
412
+
413
+ try:
414
+ tree = ast.parse(content)
415
+
416
+ for node in ast.walk(tree):
417
+ if isinstance(node, ast.Import):
418
+ for alias in node.names:
419
+ imports.append({
420
+ "module": alias.name,
421
+ "names": [alias.name],
422
+ "type": "import",
423
+ "line": node.lineno,
424
+ })
425
+
426
+ elif isinstance(node, ast.ImportFrom):
427
+ module = node.module or ""
428
+ if node.level > 0:
429
+ # Import relativo
430
+ module = "." * node.level + module
431
+
432
+ names = [alias.name for alias in node.names]
433
+ imports.append({
434
+ "module": module,
435
+ "names": names,
436
+ "type": "from" if node.level == 0 else "relative",
437
+ "line": node.lineno,
438
+ })
439
+
440
+ except SyntaxError:
441
+ # Fallback a regex si hay error de sintaxis
442
+ imports = self._parse_imports_regex(content, self.PYTHON_IMPORT_PATTERNS, "import")
443
+
444
+ return imports
445
+
446
+ def _parse_js_imports(self, content: str, file_path: Path) -> List[Dict]:
447
+ """Parsea imports de JavaScript/TypeScript."""
448
+ return self._parse_imports_regex(content, self.JS_IMPORT_PATTERNS, "import")
449
+
450
+ def _parse_imports_regex(
451
+ self,
452
+ content: str,
453
+ patterns: List[re.Pattern],
454
+ default_type: str
455
+ ) -> List[Dict]:
456
+ """Parsea imports usando expresiones regulares."""
457
+ imports = []
458
+
459
+ for pattern in patterns:
460
+ for match in pattern.finditer(content):
461
+ module = match.group(1)
462
+ imports.append({
463
+ "module": module,
464
+ "names": [],
465
+ "type": default_type,
466
+ "line": content[:match.start()].count("\n") + 1,
467
+ })
468
+
469
+ return imports
470
+
471
+ def _build_reverse_indices(self, graph: DependencyGraph) -> None:
472
+ """Construye Γ­ndices inversos (imported_by) para cada nodo."""
473
+ for edge in graph.edges:
474
+ if edge.target in graph.nodes:
475
+ graph.nodes[edge.target].imported_by.append(edge.source)
@@ -0,0 +1,5 @@
1
+ # hanus/cache/__init__.py
2
+ """Cache system for HanusCode."""
3
+ from .response_cache import ResponseCache, CacheEntry, CacheStats
4
+
5
+ __all__ = ["ResponseCache", "CacheEntry", "CacheStats"]