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/skill_manager.py
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
# hanus/skill_manager.py — Sistema de Skills
|
|
2
|
+
"""
|
|
3
|
+
Gestión de skills: comandos reutilizables que pueden instalarse desde URLs.
|
|
4
|
+
|
|
5
|
+
Soporta múltiples formatos para compatibilidad:
|
|
6
|
+
1. Python (.py) - Skills ejecutables con función run()
|
|
7
|
+
2. Markdown (.md) - Plantillas de instrucciones para el agente
|
|
8
|
+
3. Claude Code (SKILL.md) - Formato de Claude Code con directorios
|
|
9
|
+
4. Cursor (.mdc) - Formato de Cursor con scoping por globs
|
|
10
|
+
|
|
11
|
+
Formatos de argumentos soportados:
|
|
12
|
+
- HanusCode: {{args}}, {{arg1}}, {{arg2}}
|
|
13
|
+
- Claude Code: $ARGUMENTS, $ARGUMENTS[0], $name (de frontmatter)
|
|
14
|
+
- Genérico: $0, $1, $2
|
|
15
|
+
|
|
16
|
+
Contexto dinámico:
|
|
17
|
+
- Claude Code: !`command` ejecuta comando y reemplaza
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
import importlib.util
|
|
21
|
+
import subprocess
|
|
22
|
+
import urllib.request
|
|
23
|
+
import urllib.error
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import yaml
|
|
27
|
+
import shlex
|
|
28
|
+
import traceback
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Dict, List, Optional, Any
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from enum import Enum
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SkillType(Enum):
|
|
36
|
+
PYTHON = "python"
|
|
37
|
+
MARKDOWN = "markdown"
|
|
38
|
+
CLAUDE_CODE = "claude_code" # SKILL.md format
|
|
39
|
+
CURSOR = "cursor" # .mdc format
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Directorios de skills
|
|
43
|
+
SKILLS_DIR_BUILTIN = Path(__file__).parent / "skills"
|
|
44
|
+
SKILLS_DIR_USER = Path.home() / ".hanus" / "skills"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Skill:
|
|
49
|
+
"""Representa un skill cargado."""
|
|
50
|
+
name: str
|
|
51
|
+
description: str
|
|
52
|
+
usage: str
|
|
53
|
+
version: str
|
|
54
|
+
author: str
|
|
55
|
+
path: Path
|
|
56
|
+
skill_type: SkillType = SkillType.PYTHON
|
|
57
|
+
module: Any = None
|
|
58
|
+
content: str = "" # Para skills markdown
|
|
59
|
+
is_builtin: bool = False
|
|
60
|
+
# Claude Code specific
|
|
61
|
+
arguments: List[str] = field(default_factory=list) # Named arguments
|
|
62
|
+
allowed_tools: List[str] = field(default_factory=list)
|
|
63
|
+
disable_auto_invoke: bool = False
|
|
64
|
+
# Cursor specific
|
|
65
|
+
globs: List[str] = field(default_factory=list)
|
|
66
|
+
always_apply: bool = False
|
|
67
|
+
|
|
68
|
+
def run(self, args: str = "") -> str:
|
|
69
|
+
"""Ejecuta el skill."""
|
|
70
|
+
if self.skill_type in (SkillType.MARKDOWN, SkillType.CLAUDE_CODE, SkillType.CURSOR):
|
|
71
|
+
return self._run_markdown(args)
|
|
72
|
+
else:
|
|
73
|
+
return self._run_python(args)
|
|
74
|
+
|
|
75
|
+
def _run_python(self, args: str = "") -> str:
|
|
76
|
+
"""Ejecuta un skill Python."""
|
|
77
|
+
try:
|
|
78
|
+
if self.module is None:
|
|
79
|
+
return f"[Skill {self.name}] Módulo no cargado"
|
|
80
|
+
fn = getattr(self.module, "run", None)
|
|
81
|
+
if not callable(fn):
|
|
82
|
+
return f"[Skill {self.name}] No tiene función run()"
|
|
83
|
+
return str(fn(args))
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return f"[Skill {self.name}] Error: {e}\n{traceback.format_exc()}"
|
|
86
|
+
|
|
87
|
+
def _run_markdown(self, args: str = "") -> str:
|
|
88
|
+
"""Ejecuta un skill Markdown (inyecta instrucciones)."""
|
|
89
|
+
content = self.content
|
|
90
|
+
|
|
91
|
+
# Procesar contexto dinámico (!command)
|
|
92
|
+
content = self._process_dynamic_context(content)
|
|
93
|
+
|
|
94
|
+
# Reemplazar argumentos en múltiples formatos
|
|
95
|
+
content = self._replace_arguments(content, args)
|
|
96
|
+
|
|
97
|
+
# Formatear como instrucción para el agente
|
|
98
|
+
output = [
|
|
99
|
+
f"[SKILL: {self.name}]",
|
|
100
|
+
"",
|
|
101
|
+
content,
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
if args:
|
|
105
|
+
output.insert(1, f"**Argumentos:** {args}")
|
|
106
|
+
|
|
107
|
+
return "\n".join(output)
|
|
108
|
+
|
|
109
|
+
def _process_dynamic_context(self, content: str) -> str:
|
|
110
|
+
"""Procesa comandos dinámicos estilo Claude Code (!`command`)."""
|
|
111
|
+
# Buscar patrón !`command` o !`command args`
|
|
112
|
+
pattern = r'!`([^`]+)`'
|
|
113
|
+
|
|
114
|
+
def run_command(match):
|
|
115
|
+
cmd = match.group(1).strip()
|
|
116
|
+
try:
|
|
117
|
+
result = subprocess.run(
|
|
118
|
+
cmd,
|
|
119
|
+
shell=True,
|
|
120
|
+
capture_output=True,
|
|
121
|
+
text=True,
|
|
122
|
+
timeout=30
|
|
123
|
+
)
|
|
124
|
+
output = result.stdout.strip()
|
|
125
|
+
if result.returncode != 0 and result.stderr:
|
|
126
|
+
output += f"\n[stderr: {result.stderr.strip()}]"
|
|
127
|
+
return output
|
|
128
|
+
except subprocess.TimeoutExpired:
|
|
129
|
+
return "[timeout]"
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return f"[error: {e}]"
|
|
132
|
+
|
|
133
|
+
return re.sub(pattern, run_command, content)
|
|
134
|
+
|
|
135
|
+
def _replace_arguments(self, content: str, args: str) -> str:
|
|
136
|
+
"""Reemplaza argumentos en múltiples formatos."""
|
|
137
|
+
args_parts = args.split()
|
|
138
|
+
|
|
139
|
+
# IMPORTANTE: Reemplazar $ARGUMENTS[n] ANTES de $ARGUMENTS
|
|
140
|
+
# para evitar que el reemplazo de $ARGUMENTS afecte los índices
|
|
141
|
+
|
|
142
|
+
# $ARGUMENTS[0], $ARGUMENTS[1], etc.
|
|
143
|
+
for i in range(len(args_parts)):
|
|
144
|
+
content = content.replace(f"$ARGUMENTS[{i}]", args_parts[i])
|
|
145
|
+
content = content.replace(f"${i}", args_parts[i])
|
|
146
|
+
|
|
147
|
+
# Formato Claude Code: $ARGUMENTS (todo el string)
|
|
148
|
+
content = content.replace("$ARGUMENTS", args)
|
|
149
|
+
|
|
150
|
+
# Named arguments from frontmatter (Claude Code style)
|
|
151
|
+
for i, arg_name in enumerate(self.arguments):
|
|
152
|
+
if i < len(args_parts):
|
|
153
|
+
content = content.replace(f"${arg_name}", args_parts[i])
|
|
154
|
+
|
|
155
|
+
# Formato HanusCode: {{args}}, {{arg1}}, {{arg2}}
|
|
156
|
+
content = content.replace("{{args}}", args)
|
|
157
|
+
for i, arg in enumerate(args_parts, 1):
|
|
158
|
+
content = content.replace(f"{{{{arg{i}}}}}", arg)
|
|
159
|
+
|
|
160
|
+
return content
|
|
161
|
+
|
|
162
|
+
def get_doc(self) -> str:
|
|
163
|
+
"""Retorna documentación del skill."""
|
|
164
|
+
type_icons = {
|
|
165
|
+
SkillType.PYTHON: "🐍",
|
|
166
|
+
SkillType.MARKDOWN: "📝",
|
|
167
|
+
SkillType.CLAUDE_CODE: "🤖",
|
|
168
|
+
SkillType.CURSOR: "⚡",
|
|
169
|
+
}
|
|
170
|
+
icon = type_icons.get(self.skill_type, "📄")
|
|
171
|
+
|
|
172
|
+
lines = [
|
|
173
|
+
f"### {icon} Skill: /{self.name}",
|
|
174
|
+
f"**Descripción:** {self.description}",
|
|
175
|
+
f"**Uso:** `/{self.name} {self.usage}`",
|
|
176
|
+
f"**Versión:** {self.version}",
|
|
177
|
+
f"**Autor:** {self.author}",
|
|
178
|
+
f"**Tipo:** {self.skill_type.value}",
|
|
179
|
+
f"**Origen:** {'Builtin' if self.is_builtin else 'Instalado'}",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
if self.arguments:
|
|
183
|
+
lines.append(f"**Argumentos:** {', '.join(self.arguments)}")
|
|
184
|
+
|
|
185
|
+
if self.globs:
|
|
186
|
+
lines.append(f"**Aplica a:** {', '.join(self.globs)}")
|
|
187
|
+
|
|
188
|
+
# Para skills Python, añadir docstrings
|
|
189
|
+
if self.skill_type == SkillType.PYTHON and self.module:
|
|
190
|
+
if self.module.__doc__:
|
|
191
|
+
lines.append(f"\n{self.module.__doc__}")
|
|
192
|
+
agent_doc = getattr(self.module, "AGENT_DOC", "")
|
|
193
|
+
if agent_doc:
|
|
194
|
+
lines.append(f"\n{agent_doc}")
|
|
195
|
+
|
|
196
|
+
# Para skills Markdown, mostrar preview
|
|
197
|
+
elif self.content:
|
|
198
|
+
preview = self.content[:300] + "..." if len(self.content) > 300 else self.content
|
|
199
|
+
lines.append(f"\n**Preview:**")
|
|
200
|
+
lines.append(f"```markdown")
|
|
201
|
+
lines.append(preview)
|
|
202
|
+
lines.append(f"```")
|
|
203
|
+
|
|
204
|
+
return "\n".join(lines)
|
|
205
|
+
|
|
206
|
+
def get_prompt(self, args: str = "") -> str:
|
|
207
|
+
"""Obtiene el prompt/instrucción para el agente."""
|
|
208
|
+
if self.skill_type != SkillType.PYTHON:
|
|
209
|
+
return self._run_markdown(args)
|
|
210
|
+
else:
|
|
211
|
+
return self.run(args)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class SkillManager:
|
|
215
|
+
"""Gestiona el ciclo de vida de los skills."""
|
|
216
|
+
|
|
217
|
+
def __init__(self):
|
|
218
|
+
self.skills: Dict[str, Skill] = {}
|
|
219
|
+
self._load_all()
|
|
220
|
+
|
|
221
|
+
def _load_all(self):
|
|
222
|
+
"""Carga todos los skills de todos los directorios."""
|
|
223
|
+
self.skills.clear()
|
|
224
|
+
|
|
225
|
+
# Cargar skills builtin
|
|
226
|
+
if SKILLS_DIR_BUILTIN.exists():
|
|
227
|
+
self._load_from_directory(SKILLS_DIR_BUILTIN, is_builtin=True)
|
|
228
|
+
|
|
229
|
+
# Cargar skills del usuario
|
|
230
|
+
if SKILLS_DIR_USER.exists():
|
|
231
|
+
self._load_from_directory(SKILLS_DIR_USER, is_builtin=False)
|
|
232
|
+
|
|
233
|
+
def _load_from_directory(self, directory: Path, is_builtin: bool = False):
|
|
234
|
+
"""Carga skills de un directorio con múltiples formatos."""
|
|
235
|
+
# Python skills (.py)
|
|
236
|
+
for py in sorted(directory.glob("*.py")):
|
|
237
|
+
if not py.name.startswith("_"):
|
|
238
|
+
self._load_python(py, is_builtin=is_builtin)
|
|
239
|
+
|
|
240
|
+
# Markdown skills (.md)
|
|
241
|
+
for md in sorted(directory.glob("*.md")):
|
|
242
|
+
if not md.name.startswith("_"):
|
|
243
|
+
self._load_markdown(md, is_builtin=is_builtin)
|
|
244
|
+
|
|
245
|
+
# Cursor skills (.mdc)
|
|
246
|
+
for mdc in sorted(directory.glob("*.mdc")):
|
|
247
|
+
if not mdc.name.startswith("_"):
|
|
248
|
+
self._load_cursor(mdc, is_builtin=is_builtin)
|
|
249
|
+
|
|
250
|
+
# Claude Code skills (SKILL.md en subdirectorios)
|
|
251
|
+
for skill_dir in sorted(directory.iterdir()):
|
|
252
|
+
if skill_dir.is_dir() and not skill_dir.name.startswith("_"):
|
|
253
|
+
skill_md = skill_dir / "SKILL.md"
|
|
254
|
+
if skill_md.exists():
|
|
255
|
+
self._load_claude_code(skill_md, is_builtin=is_builtin)
|
|
256
|
+
|
|
257
|
+
def _load_python(self, path: Path, is_builtin: bool = False) -> Optional[Skill]:
|
|
258
|
+
"""Carga un skill Python desde archivo."""
|
|
259
|
+
try:
|
|
260
|
+
import sys
|
|
261
|
+
module_name = f"hanus_skill_{path.stem}"
|
|
262
|
+
|
|
263
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
264
|
+
if not spec or not spec.loader:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
module = importlib.util.module_from_spec(spec)
|
|
268
|
+
sys.modules[module_name] = module
|
|
269
|
+
spec.loader.exec_module(module)
|
|
270
|
+
|
|
271
|
+
name = getattr(module, "NAME", path.stem).lower().replace("-", "_")
|
|
272
|
+
description = getattr(module, "DESCRIPTION", "Sin descripción")
|
|
273
|
+
usage = getattr(module, "USAGE", "")
|
|
274
|
+
version = getattr(module, "VERSION", "1.0")
|
|
275
|
+
author = getattr(module, "AUTHOR", "Desconocido")
|
|
276
|
+
|
|
277
|
+
skill = Skill(
|
|
278
|
+
name=name,
|
|
279
|
+
description=description,
|
|
280
|
+
usage=usage,
|
|
281
|
+
version=version,
|
|
282
|
+
author=author,
|
|
283
|
+
path=path,
|
|
284
|
+
skill_type=SkillType.PYTHON,
|
|
285
|
+
module=module,
|
|
286
|
+
is_builtin=is_builtin,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
self.skills[name] = skill
|
|
290
|
+
return skill
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
print(f" [SkillManager] Error cargando Python skill '{path.name}': {e}")
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
def _load_markdown(self, path: Path, is_builtin: bool = False) -> Optional[Skill]:
|
|
297
|
+
"""Carga un skill Markdown (formato HanusCode o Claude Code simple)."""
|
|
298
|
+
try:
|
|
299
|
+
content = path.read_text(encoding="utf-8")
|
|
300
|
+
|
|
301
|
+
metadata = self._parse_frontmatter(content, path)
|
|
302
|
+
body = content
|
|
303
|
+
|
|
304
|
+
# Separar frontmatter del body
|
|
305
|
+
if content.startswith("---"):
|
|
306
|
+
parts = content.split("---", 2)
|
|
307
|
+
if len(parts) >= 3:
|
|
308
|
+
body = parts[2].strip()
|
|
309
|
+
|
|
310
|
+
# Determinar si es formato Claude Code
|
|
311
|
+
is_claude_format = "arguments" in metadata or "allowed-tools" in metadata
|
|
312
|
+
|
|
313
|
+
skill = Skill(
|
|
314
|
+
name=str(metadata.get("name", path.stem)).lower().replace("-", "_"),
|
|
315
|
+
description=str(metadata.get("description", "Sin descripción")),
|
|
316
|
+
usage=str(metadata.get("usage", "")),
|
|
317
|
+
version=str(metadata.get("version", "1.0")),
|
|
318
|
+
author=str(metadata.get("author", metadata.get("created-by", "Desconocido"))),
|
|
319
|
+
path=path,
|
|
320
|
+
skill_type=SkillType.CLAUDE_CODE if is_claude_format else SkillType.MARKDOWN,
|
|
321
|
+
content=body,
|
|
322
|
+
is_builtin=is_builtin,
|
|
323
|
+
arguments=metadata.get("arguments", []),
|
|
324
|
+
allowed_tools=metadata.get("allowed-tools", []),
|
|
325
|
+
disable_auto_invoke=metadata.get("disable-model-invocation", False),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
self.skills[skill.name] = skill
|
|
329
|
+
return skill
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
print(f" [SkillManager] Error cargando Markdown skill '{path.name}': {e}")
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
def _load_claude_code(self, path: Path, is_builtin: bool = False) -> Optional[Skill]:
|
|
336
|
+
"""Carga un skill en formato Claude Code (SKILL.md en directorio)."""
|
|
337
|
+
try:
|
|
338
|
+
content = path.read_text(encoding="utf-8")
|
|
339
|
+
metadata = self._parse_frontmatter(content, path)
|
|
340
|
+
|
|
341
|
+
# Separar body
|
|
342
|
+
body = content
|
|
343
|
+
if content.startswith("---"):
|
|
344
|
+
parts = content.split("---", 2)
|
|
345
|
+
if len(parts) >= 3:
|
|
346
|
+
body = parts[2].strip()
|
|
347
|
+
|
|
348
|
+
# Nombre del directorio como fallback
|
|
349
|
+
dir_name = path.parent.name.lower().replace("-", "_")
|
|
350
|
+
name = str(metadata.get("name", dir_name)).lower().replace("-", "_")
|
|
351
|
+
|
|
352
|
+
skill = Skill(
|
|
353
|
+
name=name,
|
|
354
|
+
description=str(metadata.get("description", "Sin descripción")),
|
|
355
|
+
usage=str(metadata.get("usage", "")),
|
|
356
|
+
version=str(metadata.get("version", "1.0")),
|
|
357
|
+
author=str(metadata.get("created-by", metadata.get("author", "Desconocido"))),
|
|
358
|
+
path=path,
|
|
359
|
+
skill_type=SkillType.CLAUDE_CODE,
|
|
360
|
+
content=body,
|
|
361
|
+
is_builtin=is_builtin,
|
|
362
|
+
arguments=metadata.get("arguments", []),
|
|
363
|
+
allowed_tools=self._parse_allowed_tools(metadata.get("allowed-tools", [])),
|
|
364
|
+
disable_auto_invoke=metadata.get("disable-model-invocation", False),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
self.skills[name] = skill
|
|
368
|
+
return skill
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
print(f" [SkillManager] Error cargando Claude Code skill '{path}': {e}")
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
def _load_cursor(self, path: Path, is_builtin: bool = False) -> Optional[Skill]:
|
|
375
|
+
"""Carga un skill en formato Cursor (.mdc)."""
|
|
376
|
+
try:
|
|
377
|
+
content = path.read_text(encoding="utf-8")
|
|
378
|
+
metadata = self._parse_frontmatter(content, path)
|
|
379
|
+
|
|
380
|
+
# Separar body
|
|
381
|
+
body = content
|
|
382
|
+
if content.startswith("---"):
|
|
383
|
+
parts = content.split("---", 2)
|
|
384
|
+
if len(parts) >= 3:
|
|
385
|
+
body = parts[2].strip()
|
|
386
|
+
|
|
387
|
+
name = path.stem.lower().replace("-", "_")
|
|
388
|
+
|
|
389
|
+
skill = Skill(
|
|
390
|
+
name=name,
|
|
391
|
+
description=str(metadata.get("description", "Sin descripción")),
|
|
392
|
+
usage="",
|
|
393
|
+
version="1.0",
|
|
394
|
+
author="Cursor",
|
|
395
|
+
path=path,
|
|
396
|
+
skill_type=SkillType.CURSOR,
|
|
397
|
+
content=body,
|
|
398
|
+
is_builtin=is_builtin,
|
|
399
|
+
globs=metadata.get("globs", []),
|
|
400
|
+
always_apply=metadata.get("alwaysApply", False),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
self.skills[name] = skill
|
|
404
|
+
return skill
|
|
405
|
+
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(f" [SkillManager] Error cargando Cursor skill '{path.name}': {e}")
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def _parse_frontmatter(self, content: str, path: Path) -> Dict:
|
|
411
|
+
"""Parsea frontmatter YAML de un archivo."""
|
|
412
|
+
metadata = {"name": path.stem.lower().replace("-", "_")}
|
|
413
|
+
|
|
414
|
+
if content.startswith("---"):
|
|
415
|
+
parts = content.split("---", 2)
|
|
416
|
+
if len(parts) >= 2:
|
|
417
|
+
frontmatter = parts[1].strip()
|
|
418
|
+
try:
|
|
419
|
+
fm_data = yaml.safe_load(frontmatter) or {}
|
|
420
|
+
metadata.update(fm_data)
|
|
421
|
+
except yaml.YAMLError:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
# Normalizar arguments (puede ser lista o string)
|
|
425
|
+
if "arguments" in metadata and isinstance(metadata["arguments"], str):
|
|
426
|
+
# Formato: "issue branch" -> ["issue", "branch"]
|
|
427
|
+
metadata["arguments"] = metadata["arguments"].split()
|
|
428
|
+
|
|
429
|
+
return metadata
|
|
430
|
+
|
|
431
|
+
def _parse_allowed_tools(self, tools) -> List[str]:
|
|
432
|
+
"""Parsea allowed-tools que puede ser string, lista o None."""
|
|
433
|
+
if tools is None:
|
|
434
|
+
return []
|
|
435
|
+
if isinstance(tools, str):
|
|
436
|
+
return tools.split()
|
|
437
|
+
return list(tools)
|
|
438
|
+
|
|
439
|
+
def reload(self):
|
|
440
|
+
"""Recarga todos los skills."""
|
|
441
|
+
self._load_all()
|
|
442
|
+
|
|
443
|
+
def get(self, name: str) -> Optional[Skill]:
|
|
444
|
+
"""Obtiene un skill por nombre."""
|
|
445
|
+
return self.skills.get(name.lower())
|
|
446
|
+
|
|
447
|
+
def list_skills(self) -> List[Skill]:
|
|
448
|
+
"""Lista todos los skills."""
|
|
449
|
+
return list(self.skills.values())
|
|
450
|
+
|
|
451
|
+
def run(self, name: str, args: str = "") -> str:
|
|
452
|
+
"""Ejecuta un skill."""
|
|
453
|
+
skill = self.get(name)
|
|
454
|
+
if skill is None:
|
|
455
|
+
available = list(self.skills.keys())
|
|
456
|
+
return f"[Error] Skill '{name}' no encontrado. Disponibles: {available}"
|
|
457
|
+
return skill.run(args)
|
|
458
|
+
|
|
459
|
+
def install(self, url: str, name: Optional[str] = None) -> str:
|
|
460
|
+
"""Instala un skill desde una URL (soporta múltiples formatos)."""
|
|
461
|
+
SKILLS_DIR_USER.mkdir(parents=True, exist_ok=True)
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
req = urllib.request.Request(url, headers={
|
|
465
|
+
"User-Agent": "HanusCode/1.0"
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
469
|
+
content = resp.read().decode("utf-8")
|
|
470
|
+
|
|
471
|
+
# Detectar tipo de skill
|
|
472
|
+
if url.endswith(".mdc"):
|
|
473
|
+
return self._install_cursor(content, url, name)
|
|
474
|
+
elif url.endswith("SKILL.md") or "SKILL.md" in url:
|
|
475
|
+
return self._install_claude_code(content, url, name)
|
|
476
|
+
elif url.endswith(".md") or content.strip().startswith("---"):
|
|
477
|
+
return self._install_markdown(content, url, name)
|
|
478
|
+
else:
|
|
479
|
+
return self._install_python(content, url, name)
|
|
480
|
+
|
|
481
|
+
except urllib.error.HTTPError as e:
|
|
482
|
+
return f"Error HTTP {e.code}: {e.reason}"
|
|
483
|
+
except urllib.error.URLError as e:
|
|
484
|
+
return f"Error de conexión: {e.reason}"
|
|
485
|
+
except Exception as e:
|
|
486
|
+
return f"Error instalando skill: {e}"
|
|
487
|
+
|
|
488
|
+
def _install_python(self, content: str, url: str, name: Optional[str] = None) -> str:
|
|
489
|
+
"""Instala un skill Python."""
|
|
490
|
+
if "def run(" not in content:
|
|
491
|
+
return "Error: El archivo no es un skill Python válido (falta función run())"
|
|
492
|
+
|
|
493
|
+
if not name:
|
|
494
|
+
name_match = re.search(r'NAME\s*=\s*["\'](\w+)["\']', content)
|
|
495
|
+
name = name_match.group(1) if name_match else Path(url).stem
|
|
496
|
+
|
|
497
|
+
name = name.lower().replace("-", "_").replace(" ", "_")
|
|
498
|
+
dest_path = SKILLS_DIR_USER / f"{name}.py"
|
|
499
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
500
|
+
|
|
501
|
+
skill = self._load_python(dest_path, is_builtin=False)
|
|
502
|
+
return f"✓ Skill Python '{skill.name}' instalado\n{skill.get_doc()}" if skill else f"✓ Guardado en {dest_path}"
|
|
503
|
+
|
|
504
|
+
def _install_markdown(self, content: str, url: str, name: Optional[str] = None) -> str:
|
|
505
|
+
"""Instala un skill Markdown."""
|
|
506
|
+
metadata = self._parse_frontmatter(content, Path(url))
|
|
507
|
+
name = name or metadata.get("name", Path(url).stem)
|
|
508
|
+
name = name.lower().replace("-", "_").replace(" ", "_")
|
|
509
|
+
|
|
510
|
+
dest_path = SKILLS_DIR_USER / f"{name}.md"
|
|
511
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
512
|
+
|
|
513
|
+
skill = self._load_markdown(dest_path, is_builtin=False)
|
|
514
|
+
return f"✓ Skill Markdown '{skill.name}' instalado\n{skill.get_doc()}" if skill else f"✓ Guardado en {dest_path}"
|
|
515
|
+
|
|
516
|
+
def _install_claude_code(self, content: str, url: str, name: Optional[str] = None) -> str:
|
|
517
|
+
"""Instala un skill Claude Code (crea directorio con SKILL.md)."""
|
|
518
|
+
metadata = self._parse_frontmatter(content, Path(url))
|
|
519
|
+
skill_name = name or metadata.get("name", "imported")
|
|
520
|
+
skill_name = skill_name.lower().replace("-", "_").replace(" ", "_")
|
|
521
|
+
|
|
522
|
+
# Crear directorio para el skill
|
|
523
|
+
skill_dir = SKILLS_DIR_USER / skill_name
|
|
524
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
525
|
+
|
|
526
|
+
dest_path = skill_dir / "SKILL.md"
|
|
527
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
528
|
+
|
|
529
|
+
skill = self._load_claude_code(dest_path, is_builtin=False)
|
|
530
|
+
return f"✓ Skill Claude Code '{skill.name}' instalado\n{skill.get_doc()}" if skill else f"✓ Guardado en {dest_path}"
|
|
531
|
+
|
|
532
|
+
def _install_cursor(self, content: str, url: str, name: Optional[str] = None) -> str:
|
|
533
|
+
"""Instala un skill Cursor (.mdc)."""
|
|
534
|
+
name = name or Path(url).stem
|
|
535
|
+
name = name.lower().replace("-", "_").replace(" ", "_")
|
|
536
|
+
|
|
537
|
+
dest_path = SKILLS_DIR_USER / f"{name}.mdc"
|
|
538
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
539
|
+
|
|
540
|
+
skill = self._load_cursor(dest_path, is_builtin=False)
|
|
541
|
+
return f"✓ Skill Cursor '{skill.name}' instalado\n{skill.get_doc()}" if skill else f"✓ Guardado en {dest_path}"
|
|
542
|
+
|
|
543
|
+
def install_from_gist(self, gist_url: str) -> str:
|
|
544
|
+
"""Instala un skill desde un GitHub Gist."""
|
|
545
|
+
gist_id = gist_url.split("/")[-1].split("?")[0]
|
|
546
|
+
api_url = f"https://api.github.com/gists/{gist_id}"
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
req = urllib.request.Request(api_url, headers={
|
|
550
|
+
"User-Agent": "HanusCode/1.0",
|
|
551
|
+
"Accept": "application/vnd.github.v3+json"
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
555
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
556
|
+
|
|
557
|
+
for filename, file_data in data.get("files", {}).items():
|
|
558
|
+
content = file_data.get("content", "")
|
|
559
|
+
if not content:
|
|
560
|
+
continue
|
|
561
|
+
|
|
562
|
+
SKILLS_DIR_USER.mkdir(parents=True, exist_ok=True)
|
|
563
|
+
|
|
564
|
+
if filename.endswith(".mdc"):
|
|
565
|
+
dest_path = SKILLS_DIR_USER / filename
|
|
566
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
567
|
+
skill = self._load_cursor(dest_path, is_builtin=False)
|
|
568
|
+
elif filename == "SKILL.md":
|
|
569
|
+
# Claude Code format - crear directorio
|
|
570
|
+
skill_dir = SKILLS_DIR_USER / Path(filename).stem
|
|
571
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
572
|
+
dest_path = skill_dir / "SKILL.md"
|
|
573
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
574
|
+
skill = self._load_claude_code(dest_path, is_builtin=False)
|
|
575
|
+
elif filename.endswith(".md"):
|
|
576
|
+
dest_path = SKILLS_DIR_USER / filename
|
|
577
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
578
|
+
skill = self._load_markdown(dest_path, is_builtin=False)
|
|
579
|
+
elif filename.endswith(".py"):
|
|
580
|
+
dest_path = SKILLS_DIR_USER / filename
|
|
581
|
+
dest_path.write_text(content, encoding="utf-8")
|
|
582
|
+
skill = self._load_python(dest_path, is_builtin=False)
|
|
583
|
+
else:
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
if skill:
|
|
587
|
+
return f"✓ Skill '{skill.name}' instalado desde Gist\n{skill.get_doc()}"
|
|
588
|
+
|
|
589
|
+
return "Error: No se encontró ningún archivo de skill en el Gist"
|
|
590
|
+
|
|
591
|
+
except Exception as e:
|
|
592
|
+
return f"Error instalando desde Gist: {e}"
|
|
593
|
+
|
|
594
|
+
def create_markdown(self, name: str, description: str, content: str = "") -> str:
|
|
595
|
+
"""Crea un nuevo skill Markdown."""
|
|
596
|
+
SKILLS_DIR_USER.mkdir(parents=True, exist_ok=True)
|
|
597
|
+
|
|
598
|
+
name = name.lower().replace("-", "_").replace(" ", "_")
|
|
599
|
+
dest_path = SKILLS_DIR_USER / f"{name}.md"
|
|
600
|
+
|
|
601
|
+
if dest_path.exists():
|
|
602
|
+
return f"Error: Ya existe un skill con nombre '{name}'"
|
|
603
|
+
|
|
604
|
+
template = f"""---
|
|
605
|
+
name: {name}
|
|
606
|
+
description: {description}
|
|
607
|
+
version: "1.0"
|
|
608
|
+
author: "User"
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
# {description.title()}
|
|
612
|
+
|
|
613
|
+
{content or "Instrucciones para el agente..."}
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
dest_path.write_text(template, encoding="utf-8")
|
|
617
|
+
skill = self._load_markdown(dest_path, is_builtin=False)
|
|
618
|
+
|
|
619
|
+
return f"✓ Skill '{skill.name}' creado\nArchivo: {dest_path}" if skill else "Error creando skill"
|
|
620
|
+
|
|
621
|
+
def remove(self, name: str) -> str:
|
|
622
|
+
"""Elimina un skill instalado."""
|
|
623
|
+
skill = self.get(name)
|
|
624
|
+
|
|
625
|
+
if skill is None:
|
|
626
|
+
return f"Error: Skill '{name}' no encontrado"
|
|
627
|
+
|
|
628
|
+
if skill.is_builtin:
|
|
629
|
+
return f"Error: No se pueden eliminar skills builtin"
|
|
630
|
+
|
|
631
|
+
try:
|
|
632
|
+
# Si es Claude Code format, eliminar directorio
|
|
633
|
+
if skill.skill_type == SkillType.CLAUDE_CODE and skill.path.name == "SKILL.md":
|
|
634
|
+
import shutil
|
|
635
|
+
shutil.rmtree(skill.path.parent)
|
|
636
|
+
else:
|
|
637
|
+
skill.path.unlink()
|
|
638
|
+
|
|
639
|
+
del self.skills[name.lower()]
|
|
640
|
+
return f"✓ Skill '{name}' eliminado"
|
|
641
|
+
except Exception as e:
|
|
642
|
+
return f"Error eliminando skill: {e}"
|
|
643
|
+
|
|
644
|
+
def get_skill_docs(self) -> str:
|
|
645
|
+
"""Retorna documentación de todos los skills."""
|
|
646
|
+
if not self.skills:
|
|
647
|
+
return ""
|
|
648
|
+
|
|
649
|
+
docs = ["## Skills disponibles\n"]
|
|
650
|
+
docs.append("Los skills son comandos reutilizables. Invócalos con `/nombre_skill`")
|
|
651
|
+
docs.append("")
|
|
652
|
+
|
|
653
|
+
# Agrupar por tipo
|
|
654
|
+
by_type = {
|
|
655
|
+
SkillType.MARKDOWN: [],
|
|
656
|
+
SkillType.PYTHON: [],
|
|
657
|
+
SkillType.CLAUDE_CODE: [],
|
|
658
|
+
SkillType.CURSOR: [],
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for skill in self.skills.values():
|
|
662
|
+
by_type[skill.skill_type].append(skill)
|
|
663
|
+
|
|
664
|
+
type_labels = {
|
|
665
|
+
SkillType.MARKDOWN: ("📝 Skills Markdown", "HanusCode format"),
|
|
666
|
+
SkillType.PYTHON: ("🐍 Skills Python", "Ejecutables"),
|
|
667
|
+
SkillType.CLAUDE_CODE: ("🤖 Skills Claude Code", "Compatible"),
|
|
668
|
+
SkillType.CURSOR: ("⚡ Skills Cursor", "Compatible"),
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for skill_type, skills_list in by_type.items():
|
|
672
|
+
if skills_list:
|
|
673
|
+
label, hint = type_labels[skill_type]
|
|
674
|
+
docs.append(f"### {label} ({hint})")
|
|
675
|
+
for skill in sorted(skills_list, key=lambda s: s.name):
|
|
676
|
+
docs.append(f"- `/{skill.name}` — {skill.description}")
|
|
677
|
+
docs.append("")
|
|
678
|
+
|
|
679
|
+
return "\n".join(docs)
|
|
680
|
+
|
|
681
|
+
def get_commands(self) -> Dict[str, str]:
|
|
682
|
+
"""Retorna diccionario de comandos para UI."""
|
|
683
|
+
type_prefix = {
|
|
684
|
+
SkillType.MARKDOWN: "[MD]",
|
|
685
|
+
SkillType.PYTHON: "[PY]",
|
|
686
|
+
SkillType.CLAUDE_CODE: "[CC]",
|
|
687
|
+
SkillType.CURSOR: "[MC]",
|
|
688
|
+
}
|
|
689
|
+
return {name: f"{type_prefix.get(s.skill_type, '')} {s.description}"
|
|
690
|
+
for name, s in self.skills.items()}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
# Singleton global
|
|
694
|
+
_skill_manager: Optional[SkillManager] = None
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def get_skill_manager() -> SkillManager:
|
|
698
|
+
"""Obtiene el gestor de skills."""
|
|
699
|
+
global _skill_manager
|
|
700
|
+
if _skill_manager is None:
|
|
701
|
+
_skill_manager = SkillManager()
|
|
702
|
+
return _skill_manager
|