Repository navigation
Expand file tree
/
Copy pathconvert_skills.py
More file actions
223 lines (192 loc) · 8.16 KB
/
Copy pathconvert_skills.py
File metadata and controls
223 lines (192 loc) · 8.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env python3
"""
Claude Skills Converter v4
Parser específico para formato Markdown con ## headers y - **Field**: value
name = folder value (no skill name)
"""
import re
import zipfile
from pathlib import Path
INPUT_DIR = Path("skills")
OUTPUT_DIR = Path("skills-converted")
ZIP_DIR = Path("skills-zip")
def parse_skill(content: str, file_path: Path) -> dict:
# Normalizar saltos de línea
content = content.replace('\r\n', '\n').replace('\r', '\n')
lines = content.split('\n')
meta = {
'name': None, # Será igual a folder
'folder': None,
'source': None,
'description': [],
'triggers': [],
'body': []
}
state = 'INIT'
for line in lines:
stripped = line.strip()
if not stripped:
if state in ('DESCRIPTION', 'BODY'):
# Preservar saltos de línea en descripción y cuerpo
if state == 'DESCRIPTION' and meta['description']:
meta['description'].append('')
elif state == 'BODY':
meta['body'].append('')
continue
# Detectar headers Markdown (## Section)
header_match = re.match(r'^##\s+(.+)', stripped, re.I)
if header_match:
section = header_match.group(1).strip().lower()
if section == 'metadata':
state = 'METADATA'
elif section == 'description':
state = 'DESCRIPTION'
elif section == 'trigger phrases':
state = 'TRIGGER'
elif section == 'instructions for claude':
state = 'BODY'
meta['body'].append('# Instructions for Claude')
elif section == 'full methodology':
state = 'BODY'
meta['body'].append('## Full Methodology')
continue
# Procesar según estado
if state == 'METADATA' and stripped.startswith('- **'):
# Formato: - **Field Name**: value
match = re.match(r'-\s*\*\*([^*]+)\*\*\s*:\s*(.+)', stripped)
if match:
key = match.group(1).strip().lower().replace(' ', '_')
val = match.group(2).strip()
if key == 'skill_name':
pass # No usamos este para name
elif key == 'folder':
meta['folder'] = val
# name = folder (según requerimiento)
meta['name'] = re.sub(r'[^a-z0-9]+', '-', val.lower()).strip('-')
elif key == 'source':
meta['source'] = val
elif state == 'DESCRIPTION':
# Ignorar líneas que sean headers o metadata
if not stripped.startswith('##') and not stripped.startswith('- **'):
meta['description'].append(stripped)
elif state == 'TRIGGER':
# Buscar bloque entre backticks: `a, b, c`
match = re.search(r'`([^`]+)`', stripped)
if match:
raw = match.group(1)
meta['triggers'] = [t.strip() for t in raw.split(',') if t.strip()]
elif state == 'BODY':
# Preservar todo el contenido restante
if not stripped.startswith('##') or stripped.startswith('###'):
meta['body'].append(line.rstrip())
# Finalizar campos
meta['description'] = ' '.join(d for d in meta['description'] if d.strip())
if not meta['description']:
meta['description'] = 'Sin descripción disponible.'
meta['body'] = '\n'.join(meta['body']) if meta['body'] else '# Instructions for Claude\n[Contenido no parseado]'
# Fallback: si no hay folder, usar nombre de carpeta del archivo
if not meta['name']:
parent = file_path.parent.name
meta['name'] = re.sub(r'[^a-z0-9]+', '-', parent.lower()).strip('-') if parent != 'skills' else file_path.stem
return meta
def build_yaml(meta: dict) -> str:
lines = ['---', f'name: {meta["name"]}', 'description: >']
words = meta['description'].split()
line = ' '
for w in words:
if len(line) + len(w) + 1 > 80:
lines.append(line)
line = ' ' + w
else:
line += (' ' if len(line) > 2 else '') + w
if line.strip():
lines.append(line)
lines.append('trigger_phrases:')
for t in meta['triggers']:
lines.append(f' - "{t}"')
if meta.get('folder'):
lines.append(f'folder: {meta["folder"]}')
if meta.get('source'):
lines.append(f'source: {meta["source"]}')
lines.append('---')
return '\n'.join(lines)
def process_file(in_path: Path, out_dir: Path, debug: bool = False) -> bool:
try:
for enc in ['utf-8', 'utf-8-sig', 'latin-1']:
try:
text = in_path.read_text(encoding=enc)
break
except UnicodeDecodeError:
continue
else:
return False
meta = parse_skill(text, in_path)
if debug:
print(f"\n🔍 DEBUG {in_path.name}")
print(f" Name: {meta['name']}")
print(f" Folder: {meta['folder']}")
print(f" Desc: {meta['description'][:120]}...")
print(f" Trig: {meta['triggers'][:3]}... ({len(meta['triggers'])} total)")
print(f" Src: {meta['source']}")
print(f" Body preview: {meta['body'][:100]}...")
return True
yaml_block = build_yaml(meta)
final = f"{yaml_block}\n\n{meta['body']}\n"
out = out_dir / meta['name']
out.mkdir(parents=True, exist_ok=True)
(out / 'SKILL.md').write_text(final, encoding='utf-8', newline='\n')
print(f"✅ {in_path.parent.name:35} → {meta['name']}/SKILL.md (Triggers: {len(meta['triggers'])})")
return True
except Exception as e:
print(f"❌ {in_path.name}: {e}")
import traceback
traceback.print_exc()
return False
def create_zips(conv_dir: Path, zip_dir: Path):
zip_dir.mkdir(parents=True, exist_ok=True)
for d in sorted(conv_dir.iterdir()):
if d.is_dir():
zp = zip_dir / f"{d.name}.zip"
with zipfile.ZipFile(zp, 'w', zipfile.ZIP_DEFLATED) as zf:
for f in d.rglob('*'):
if f.is_file():
zf.write(f, f.relative_to(d.parent))
print(f"📦 {zp.name}")
def main():
print("🚀 Claude Skills Converter v4 - Formato Markdown ##\n")
in_dir, out_dir, zip_dir = INPUT_DIR, OUTPUT_DIR, ZIP_DIR
if not in_dir.exists():
print(f"❌ Carpeta no encontrada: {in_dir.resolve()}")
return
files = set(list(in_dir.rglob('SKILL.md')) + list(in_dir.rglob('skill.md')))
if not files:
print("⚠️ No se encontraron archivos SKILL.md")
return
print(f"🔍 Encontrados {len(files)} archivos\n")
# Modo debug: procesar solo 1 archivo
import sys
if '--debug' in sys.argv:
f = sorted(files)[0]
process_file(f, out_dir, debug=True)
return
ok = sum(1 for f in sorted(files) if process_file(f, out_dir))
if ok:
print(f"\n📦 Generando {ok} ZIP(s)...")
create_zips(out_dir, zip_dir)
print(f"\n✨ {ok}/{len(files)} skills convertidas exitosamente.")
print(f"📁 Skills: {out_dir.resolve()}")
print(f"🗜️ ZIPs: {zip_dir.resolve()}")
print(f"\n📋 Para subir a Claude Console:")
print(f" • Cada ZIP contiene: [folder-name]/SKILL.md")
print(f" • El 'name' en YAML = nombre de la carpeta dentro del ZIP")
else:
print(f"\n❌ No se pudo convertir ninguna skill. Ejecuta con --debug para diagnosticar.")
if __name__ == '__main__':
# Requerimiento: PyYAML
try:
import yaml
except ImportError:
print("⚠️ Instalando dependencia PyYAML...")
import subprocess
subprocess.check_call(['pip', 'install', 'pyyaml'])
main()