elsabro 3.8.2 → 4.0.0
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.
- package/agents/elsabro-orchestrator.md +124 -2
- package/bin/install.js +10 -11
- package/commands/elsabro/execute.md +164 -4
- package/flows/development-flow.json +183 -23
- package/hooks/hooks-config-updated.json +63 -101
- package/hooks/skill-discovery.sh +412 -385
- package/package.json +2 -2
- package/references/agent-teams-integration.md +313 -0
- package/references/flow-orchestration.md +70 -0
- package/templates/skill-marketplace-config.json +57 -358
package/hooks/skill-discovery.sh
CHANGED
|
@@ -1,535 +1,562 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# skill-discovery.sh - ELSABRO
|
|
2
|
+
# skill-discovery.sh - ELSABRO Skill Discovery Hook (v4.0.0)
|
|
3
|
+
#
|
|
4
|
+
# Descubre, sugiere e instala skills usando vercel-labs/skills (npx skills)
|
|
5
|
+
# y escaneo local de ELSABRO skills.
|
|
3
6
|
#
|
|
4
7
|
# Uso: bash ./hooks/skill-discovery.sh "descripción de tarea" "complejidad"
|
|
5
|
-
# Output: JSON con skills descubiertos,
|
|
8
|
+
# Output: JSON con skills descubiertos, instalados y sugeridos
|
|
9
|
+
#
|
|
10
|
+
# Fuentes de descubrimiento:
|
|
11
|
+
# 1. Skills globales instalados (~/.claude/skills/)
|
|
12
|
+
# 2. Skills locales ELSABRO (./skills/*.md)
|
|
13
|
+
# 3. npx skills find <query> (vercel-labs/skills registry)
|
|
6
14
|
#
|
|
7
|
-
#
|
|
8
|
-
# 1. Extraer keywords de la descripción
|
|
9
|
-
# 2. Buscar skills en ELSABRO interno
|
|
10
|
-
# 3. Buscar skills en marketplace externo (skills.sh)
|
|
11
|
-
# 4. Validar y rankear resultados
|
|
12
|
-
# 5. Retornar JSON con estructura normalizada
|
|
15
|
+
# Requiere: bash 4+, jq, node/npm (para npx)
|
|
13
16
|
|
|
14
|
-
set -
|
|
17
|
+
set -euo pipefail
|
|
15
18
|
|
|
16
19
|
# ============================================================================
|
|
17
|
-
# CONFIGURACIÓN
|
|
20
|
+
# CONFIGURACIÓN
|
|
18
21
|
# ============================================================================
|
|
19
22
|
|
|
20
|
-
# Directorios
|
|
21
23
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
24
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
23
25
|
ELSABRO_SKILLS_DIR="${PROJECT_ROOT}/skills"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
GLOBAL_SKILLS_DIR="${HOME}/.claude/skills"
|
|
27
|
+
CACHE_DIR="${PROJECT_ROOT}/.cache"
|
|
28
|
+
CACHE_FILE="${CACHE_DIR}/skill-discovery-cache.json"
|
|
29
|
+
CACHE_TTL=3600 # 1 hora
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
MAX_SKILLS_RETURN=10
|
|
30
|
-
EXTERNAL_API_TIMEOUT=15
|
|
31
|
-
CACHE_TTL_SECONDS=3600
|
|
31
|
+
MAX_RESULTS=10
|
|
32
|
+
NPX_TIMEOUT=30 # segundos
|
|
32
33
|
|
|
33
|
-
# Colores para stderr
|
|
34
|
+
# Colores para stderr
|
|
34
35
|
RED='\033[0;31m'
|
|
35
36
|
GREEN='\033[0;32m'
|
|
36
37
|
YELLOW='\033[1;33m'
|
|
37
38
|
BLUE='\033[0;34m'
|
|
38
|
-
CYAN='\033[0;36m'
|
|
39
39
|
NC='\033[0m'
|
|
40
|
-
|
|
41
|
-
# Prefijo de logs
|
|
42
|
-
PREFIX="[ELSABRO:skill-discovery]"
|
|
40
|
+
PREFIX="[ELSABRO:skills]"
|
|
43
41
|
|
|
44
42
|
# ============================================================================
|
|
45
|
-
#
|
|
43
|
+
# LOGGING (todo a stderr, stdout es solo JSON)
|
|
46
44
|
# ============================================================================
|
|
47
45
|
|
|
48
|
-
log_info() {
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
log_success() {
|
|
53
|
-
echo -e "${GREEN}${PREFIX}${NC} ✓ $1" >&2
|
|
54
|
-
}
|
|
46
|
+
log_info() { echo -e "${BLUE}${PREFIX}${NC} $1" >&2; }
|
|
47
|
+
log_success() { echo -e "${GREEN}${PREFIX}${NC} ✓ $1" >&2; }
|
|
48
|
+
log_warn() { echo -e "${YELLOW}${PREFIX}${NC} ⚠ $1" >&2; }
|
|
49
|
+
log_error() { echo -e "${RED}${PREFIX}${NC} ✗ $1" >&2; }
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# UTILIDADES
|
|
53
|
+
# ============================================================================
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
ensure_dirs() {
|
|
56
|
+
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
|
57
|
+
mkdir -p "$GLOBAL_SKILLS_DIR" 2>/dev/null || true
|
|
62
58
|
}
|
|
63
59
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
mkdir -p "$(dirname "$SKILLS_CACHE_FILE")" 2>/dev/null || true
|
|
60
|
+
has_command() {
|
|
61
|
+
command -v "$1" &>/dev/null
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
# Verificar si caché es válido
|
|
64
|
+
# Verificar si caché es válido (no expirado)
|
|
70
65
|
is_cache_valid() {
|
|
71
|
-
local
|
|
72
|
-
|
|
73
|
-
if [ ! -f "$cache_file" ]; then
|
|
66
|
+
local cache_key="$1"
|
|
67
|
+
if [ ! -f "$CACHE_FILE" ]; then
|
|
74
68
|
return 1
|
|
75
69
|
fi
|
|
76
|
-
|
|
77
|
-
# Verificar edad del caché (en segundos)
|
|
78
|
-
local file_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
|
|
79
|
-
|
|
80
|
-
if [ "$file_age" -gt "$CACHE_TTL_SECONDS" ]; then
|
|
70
|
+
if ! has_command jq; then
|
|
81
71
|
return 1
|
|
82
72
|
fi
|
|
73
|
+
local cached_at
|
|
74
|
+
cached_at=$(jq -r --arg key "$cache_key" '.[$key].cached_at // 0' "$CACHE_FILE" 2>/dev/null || echo "0")
|
|
75
|
+
local now
|
|
76
|
+
now=$(date +%s)
|
|
77
|
+
local age=$(( now - cached_at ))
|
|
78
|
+
[ "$age" -lt "$CACHE_TTL" ]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get_from_cache() {
|
|
82
|
+
local cache_key="$1"
|
|
83
|
+
jq --arg key "$cache_key" '.[$key].data' "$CACHE_FILE" 2>/dev/null
|
|
84
|
+
}
|
|
83
85
|
|
|
84
|
-
|
|
86
|
+
save_to_cache() {
|
|
87
|
+
local cache_key="$1"
|
|
88
|
+
local data="$2"
|
|
89
|
+
ensure_dirs
|
|
90
|
+
local now
|
|
91
|
+
now=$(date +%s)
|
|
92
|
+
local tmp_file="${CACHE_FILE}.tmp.$$"
|
|
93
|
+
if [ -f "$CACHE_FILE" ] && jq empty "$CACHE_FILE" 2>/dev/null; then
|
|
94
|
+
jq --arg key "$cache_key" --argjson data "$data" --argjson ts "$now" \
|
|
95
|
+
'.[$key] = {"data": $data, "cached_at": $ts}' \
|
|
96
|
+
"$CACHE_FILE" > "$tmp_file" && mv "$tmp_file" "$CACHE_FILE"
|
|
97
|
+
else
|
|
98
|
+
jq -n --arg key "$cache_key" --argjson data "$data" --argjson ts "$now" \
|
|
99
|
+
'{($key): {"data": $data, "cached_at": $ts}}' > "$tmp_file" && mv "$tmp_file" "$CACHE_FILE"
|
|
100
|
+
fi
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
# ============================================================================
|
|
88
104
|
# 1. EXTRACCIÓN DE KEYWORDS
|
|
89
105
|
# ============================================================================
|
|
90
106
|
|
|
91
|
-
# Extrae palabras clave de la descripción de tarea
|
|
92
|
-
# Entrada: string (descripción)
|
|
93
|
-
# Salida: JSON array con keywords encontradas
|
|
94
107
|
extract_keywords() {
|
|
95
|
-
local
|
|
108
|
+
local task="$1"
|
|
109
|
+
local lower
|
|
110
|
+
lower=$(echo "$task" | tr '[:upper:]' '[:lower:]')
|
|
96
111
|
|
|
97
|
-
|
|
98
|
-
local lowercase=$(echo "$task_description" | tr '[:upper:]' '[:lower:]')
|
|
99
|
-
|
|
100
|
-
# Definir patrones de keywords por categoría
|
|
101
|
-
# Formato: categoria|sinónimo1|sinónimo2|sinónimo3
|
|
102
|
-
local keyword_patterns=(
|
|
112
|
+
local -a keyword_patterns=(
|
|
103
113
|
"api|api|backend|endpoint|rest|graphql|http|server"
|
|
104
|
-
"auth|auth|login|signin|oauth|jwt|session|password|signup
|
|
105
|
-
"db|database|db|postgres|
|
|
106
|
-
"payments|payment|stripe|paypal|billing|checkout|commerce
|
|
107
|
-
"mobile|mobile|expo|react-native|ios|android|
|
|
108
|
-
"web|website|web|nextjs|react|vue|angular|frontend|ui|
|
|
109
|
-
"devops|devops|docker|kubernetes|k8s|ci|cd|github|gitlab|deploy"
|
|
110
|
-
"testing|test|jest|vitest|mocha|pytest|coverage|
|
|
111
|
-
"security|security|encrypt|https|cors|ssl|tls|
|
|
112
|
-
"monitoring|monitor|sentry|logging|
|
|
113
|
-
"cli|cli|command|tool|script|
|
|
114
|
-
"ai|ai|machine-learning|ml|gpt|claude|llm|nlp|
|
|
114
|
+
"auth|auth|login|signin|oauth|jwt|session|password|signup"
|
|
115
|
+
"db|database|db|postgres|mysql|mongodb|firebase|sql|orm|prisma|drizzle"
|
|
116
|
+
"payments|payment|stripe|paypal|billing|checkout|commerce"
|
|
117
|
+
"mobile|mobile|expo|react-native|ios|android|swift|kotlin|flutter"
|
|
118
|
+
"web|website|web|nextjs|react|vue|angular|frontend|ui|html|css|tailwind"
|
|
119
|
+
"devops|devops|docker|kubernetes|k8s|ci|cd|github|gitlab|deploy|deploying|deployment|vercel"
|
|
120
|
+
"testing|test|testing|jest|vitest|mocha|pytest|coverage|e2e|playwright"
|
|
121
|
+
"security|security|encrypt|https|cors|ssl|tls|vulnerability"
|
|
122
|
+
"monitoring|monitor|monitoring|sentry|logging|error|analytics|observability"
|
|
123
|
+
"cli|cli|command|tool|script|terminal|bash|shell"
|
|
124
|
+
"ai|ai|machine-learning|ml|gpt|claude|llm|nlp|openai|anthropic"
|
|
115
125
|
)
|
|
116
126
|
|
|
117
|
-
|
|
118
|
-
local found_keywords=()
|
|
119
|
-
local found_categories=()
|
|
127
|
+
local -a found=()
|
|
120
128
|
|
|
121
|
-
# Buscar cada patrón en la descripción
|
|
122
129
|
for pattern in "${keyword_patterns[@]}"; do
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# Crear regex de sinónimos
|
|
126
|
-
local regex=$(echo "$synmap" | sed 's/|/\\|/g')
|
|
130
|
+
local category="${pattern%%|*}"
|
|
131
|
+
local synonyms="${pattern#*|}"
|
|
127
132
|
|
|
128
|
-
#
|
|
129
|
-
if echo "$
|
|
130
|
-
|
|
131
|
-
found_categories+=("$category")
|
|
133
|
+
# -E (extended regex) uses unescaped | for alternation
|
|
134
|
+
if echo "$lower" | grep -qiE "\b($synonyms)\b"; then
|
|
135
|
+
found+=("\"$category\"")
|
|
132
136
|
fi
|
|
133
137
|
done
|
|
134
138
|
|
|
135
|
-
#
|
|
136
|
-
# Si no encontró keywords, retornar array vacío
|
|
137
|
-
if [ ${#found_keywords[@]} -eq 0 ]; then
|
|
139
|
+
if [ ${#found[@]} -eq 0 ]; then
|
|
138
140
|
echo "[]"
|
|
139
141
|
else
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
printf ",$kw"
|
|
142
|
+
local result="[${found[0]}"
|
|
143
|
+
for kw in "${found[@]:1}"; do
|
|
144
|
+
result+=",$kw"
|
|
144
145
|
done
|
|
145
|
-
|
|
146
|
+
result+="]"
|
|
147
|
+
echo "$result"
|
|
146
148
|
fi
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
# ============================================================================
|
|
150
|
-
# 2.
|
|
152
|
+
# 2. ESCANEO DE SKILLS INSTALADOS GLOBALMENTE
|
|
151
153
|
# ============================================================================
|
|
152
154
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# Salida: JSON array con skills ELSABRO encontrados
|
|
156
|
-
discover_elsabro_skills() {
|
|
157
|
-
local keywords="$1"
|
|
158
|
-
|
|
159
|
-
local skills_found=()
|
|
155
|
+
scan_installed_skills() {
|
|
156
|
+
local -a skills=()
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
if [ ! -d "$ELSABRO_SKILLS_DIR" ]; then
|
|
158
|
+
if [ ! -d "$GLOBAL_SKILLS_DIR" ]; then
|
|
163
159
|
echo "[]"
|
|
164
160
|
return
|
|
165
161
|
fi
|
|
166
162
|
|
|
167
|
-
#
|
|
168
|
-
for
|
|
169
|
-
[ -
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
local
|
|
163
|
+
# Buscar SKILL.md en subdirectorios de ~/.claude/skills/
|
|
164
|
+
for skill_dir in "$GLOBAL_SKILLS_DIR"/*/; do
|
|
165
|
+
[ -d "$skill_dir" ] || continue
|
|
166
|
+
local skill_name
|
|
167
|
+
skill_name=$(basename "$skill_dir")
|
|
168
|
+
local skill_file="${skill_dir}SKILL.md"
|
|
169
|
+
|
|
170
|
+
if [ -f "$skill_file" ]; then
|
|
171
|
+
local description=""
|
|
172
|
+
description=$(sed -n '/^[^#]/p' "$skill_file" | head -1 | cut -c1-100)
|
|
173
|
+
|
|
174
|
+
# Use jq for safe JSON construction (avoids injection from special chars)
|
|
175
|
+
local skill_obj
|
|
176
|
+
skill_obj=$(jq -n --arg id "$skill_name" --arg desc "$description" --arg loc "$skill_dir" \
|
|
177
|
+
'{id: $id, source: "installed", status: "installed", location: $loc, description: $desc}')
|
|
178
|
+
skills+=("$skill_obj")
|
|
179
|
+
fi
|
|
180
|
+
done
|
|
173
181
|
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
if [ ${#skills[@]} -eq 0 ]; then
|
|
183
|
+
echo "[]"
|
|
184
|
+
else
|
|
185
|
+
printf '%s\n' "${skills[@]}" | jq -s '.'
|
|
186
|
+
fi
|
|
187
|
+
}
|
|
176
188
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
description="${description:0:80}" # Limitar a 80 caracteres
|
|
189
|
+
# ============================================================================
|
|
190
|
+
# 3. ESCANEO DE SKILLS LOCALES ELSABRO
|
|
191
|
+
# ============================================================================
|
|
181
192
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
difficulty="${difficulty:-intermediate}"
|
|
193
|
+
scan_elsabro_skills() {
|
|
194
|
+
local keywords_json="$1"
|
|
186
195
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
if [ ! -d "$ELSABRO_SKILLS_DIR" ]; then
|
|
197
|
+
echo "[]"
|
|
198
|
+
return
|
|
199
|
+
fi
|
|
190
200
|
|
|
191
|
-
|
|
192
|
-
local tags=$(
|
|
193
|
-
echo "$skill_content" |
|
|
194
|
-
sed -n '/^tags:/,/^[^ -]/p' |
|
|
195
|
-
grep "^ - " |
|
|
196
|
-
sed 's/^ - //' |
|
|
197
|
-
head -10
|
|
198
|
-
)
|
|
201
|
+
local -a skills=()
|
|
199
202
|
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
for skill_file in "$ELSABRO_SKILLS_DIR"/*.md; do
|
|
204
|
+
[ -f "$skill_file" ] || continue
|
|
202
205
|
|
|
203
|
-
local
|
|
206
|
+
local skill_name
|
|
207
|
+
skill_name=$(basename "$skill_file" .md)
|
|
204
208
|
|
|
205
|
-
#
|
|
206
|
-
local
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
kw_array+=("$kw")
|
|
210
|
-
done < <(echo "$keywords" | jq -r '.[]' 2>/dev/null)
|
|
211
|
-
fi
|
|
209
|
+
# Extraer metadata básica
|
|
210
|
+
local description=""
|
|
211
|
+
description=$(sed -n '/^description:/s/^description: *//p' "$skill_file" | head -1 | sed 's/"/\\"/g')
|
|
212
|
+
description="${description:0:100}"
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if echo "$tags" | grep -qi "$keyword"; then
|
|
216
|
-
((match_score++))
|
|
217
|
-
fi
|
|
218
|
-
if echo "$description" | grep -qi "$keyword"; then
|
|
219
|
-
((match_score++))
|
|
220
|
-
fi
|
|
221
|
-
done
|
|
214
|
+
local tags=""
|
|
215
|
+
tags=$(sed -n '/^tags:/,/^[^ -]/p' "$skill_file" | grep "^ - " | sed 's/^ - //' | head -10 | tr '\n' ',' | sed 's/,$//')
|
|
222
216
|
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
local
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"description": "$description",
|
|
235
|
-
"difficulty": "$difficulty",
|
|
236
|
-
"estimated_time": "$estimated_time",
|
|
237
|
-
"tags": $tags_json,
|
|
238
|
-
"rank": 0
|
|
239
|
-
}
|
|
240
|
-
EOF
|
|
241
|
-
)
|
|
217
|
+
# Calcular match score contra keywords
|
|
218
|
+
local match_score=0
|
|
219
|
+
if has_command jq; then
|
|
220
|
+
local kw_list
|
|
221
|
+
kw_list=$(echo "$keywords_json" | jq -r '.[]' 2>/dev/null)
|
|
222
|
+
for kw in $kw_list; do
|
|
223
|
+
if echo "$tags $description $skill_name" | grep -qi "$kw"; then
|
|
224
|
+
match_score=$((match_score + 1))
|
|
225
|
+
fi
|
|
226
|
+
done
|
|
227
|
+
fi
|
|
242
228
|
|
|
243
|
-
|
|
229
|
+
if [ "$match_score" -gt 0 ]; then
|
|
230
|
+
# Use jq for safe JSON construction
|
|
231
|
+
local skill_obj
|
|
232
|
+
skill_obj=$(jq -n --arg id "$skill_name" --arg desc "$description" --arg tags "$tags" --argjson score "$match_score" \
|
|
233
|
+
'{id: $id, source: "elsabro", status: "available", match_score: $score, description: $desc, tags: $tags}')
|
|
234
|
+
skills+=("$skill_obj")
|
|
244
235
|
fi
|
|
245
236
|
done
|
|
246
237
|
|
|
247
|
-
#
|
|
248
|
-
if [ ${#skills_found[@]} -eq 0 ]; then
|
|
238
|
+
if [ ${#skills[@]} -eq 0 ]; then
|
|
249
239
|
echo "[]"
|
|
250
240
|
else
|
|
251
|
-
|
|
252
|
-
printf '%s\n' "${skills_found[@]}" | jq -s '.'
|
|
241
|
+
printf '%s\n' "${skills[@]}" | jq -s 'sort_by(.match_score) | reverse'
|
|
253
242
|
fi
|
|
254
243
|
}
|
|
255
244
|
|
|
256
245
|
# ============================================================================
|
|
257
|
-
#
|
|
246
|
+
# 4. DESCUBRIMIENTO EXTERNO VIA npx skills find
|
|
258
247
|
# ============================================================================
|
|
259
248
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# Salida: JSON array con skills de marketplace
|
|
263
|
-
discover_external_skills() {
|
|
264
|
-
local keywords="$1"
|
|
249
|
+
discover_via_npx_skills() {
|
|
250
|
+
local query="$1"
|
|
265
251
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
log_warn "curl o jq no disponible, saltando external skills"
|
|
252
|
+
if ! has_command npx; then
|
|
253
|
+
log_warn "npx no disponible, saltando descubrimiento externo"
|
|
269
254
|
echo "[]"
|
|
270
255
|
return
|
|
271
256
|
fi
|
|
272
257
|
|
|
273
|
-
|
|
274
|
-
local api_url="https://api.skills.sh/v1/discover"
|
|
258
|
+
log_info "Ejecutando: npx skills find \"$query\""
|
|
275
259
|
|
|
276
|
-
|
|
277
|
-
|
|
260
|
+
local raw_output=""
|
|
261
|
+
# Ejecutar npx skills find con timeout
|
|
262
|
+
raw_output=$(timeout "$NPX_TIMEOUT" npx -y skills find "$query" 2>/dev/null) || {
|
|
263
|
+
log_warn "npx skills find falló o timeout"
|
|
264
|
+
echo "[]"
|
|
265
|
+
return
|
|
266
|
+
}
|
|
278
267
|
|
|
279
|
-
|
|
268
|
+
if [ -z "$raw_output" ]; then
|
|
269
|
+
echo "[]"
|
|
270
|
+
return
|
|
271
|
+
fi
|
|
280
272
|
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
273
|
+
# npx skills find retorna texto formateado, necesitamos parsearlo a JSON
|
|
274
|
+
# El formato típico es líneas con nombre y descripción
|
|
275
|
+
# Intentar parsear como JSON primero (por si devuelve JSON)
|
|
276
|
+
if echo "$raw_output" | jq empty 2>/dev/null; then
|
|
277
|
+
echo "$raw_output" | jq '[.[] | . + {"source": "skills-registry", "status": "available", "install_cmd": ("npx skills add -g " + .name)}]' 2>/dev/null || echo "[]"
|
|
278
|
+
return
|
|
279
|
+
fi
|
|
289
280
|
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
281
|
+
# Si no es JSON, parsear texto línea por línea
|
|
282
|
+
local -a skills=()
|
|
283
|
+
while IFS= read -r line; do
|
|
284
|
+
# Ignorar líneas vacías y headers
|
|
285
|
+
[ -z "$line" ] && continue
|
|
286
|
+
echo "$line" | grep -q "^─\|^━\|^=\|^Found\|^Searching\|^No " && continue
|
|
287
|
+
|
|
288
|
+
# Extraer nombre del skill (primera palabra que parece un nombre de skill)
|
|
289
|
+
local name
|
|
290
|
+
name=$(echo "$line" | grep -oE '[a-z][a-z0-9_-]+' | head -1)
|
|
291
|
+
[ -z "$name" ] && continue
|
|
292
|
+
|
|
293
|
+
local desc
|
|
294
|
+
desc=$(echo "$line" | sed "s/$name//" | sed 's/^[[:space:]-]*//' | cut -c1-100)
|
|
295
|
+
|
|
296
|
+
# Use jq for safe JSON construction
|
|
297
|
+
local skill_obj
|
|
298
|
+
skill_obj=$(jq -n --arg id "$name" --arg desc "$desc" --arg cmd "npx skills add -g $name" \
|
|
299
|
+
'{id: $id, source: "skills-registry", status: "available", description: $desc, install_cmd: $cmd}')
|
|
300
|
+
skills+=("$skill_obj")
|
|
301
|
+
done <<< "$raw_output"
|
|
302
|
+
|
|
303
|
+
if [ ${#skills[@]} -eq 0 ]; then
|
|
303
304
|
echo "[]"
|
|
305
|
+
else
|
|
306
|
+
printf '%s\n' "${skills[@]}" | jq -s '.' 2>/dev/null || echo "[]"
|
|
304
307
|
fi
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
# ============================================================================
|
|
308
|
-
#
|
|
311
|
+
# 5. CHECK SKILLS ACTUALIZACIONES
|
|
309
312
|
# ============================================================================
|
|
310
313
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
check_skill_updates() {
|
|
315
|
+
if ! has_command npx; then
|
|
316
|
+
echo "[]"
|
|
317
|
+
return
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
local check_output=""
|
|
321
|
+
check_output=$(timeout "$NPX_TIMEOUT" npx -y skills check 2>/dev/null) || {
|
|
322
|
+
echo "[]"
|
|
323
|
+
return
|
|
324
|
+
}
|
|
316
325
|
|
|
317
|
-
if
|
|
318
|
-
|
|
319
|
-
echo "$all_skills"
|
|
326
|
+
if [ -z "$check_output" ]; then
|
|
327
|
+
echo "[]"
|
|
320
328
|
return
|
|
321
329
|
fi
|
|
322
330
|
|
|
323
|
-
#
|
|
324
|
-
echo "$
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
(if .source == "elsabro" then 20 else 0 end) +
|
|
345
|
-
# Penalty por external que requiere instalación
|
|
346
|
-
(if (.install_command != null) then 0 else 5 end)
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
) |
|
|
350
|
-
# Ordenar por ranking descendente
|
|
351
|
-
sort_by(.rank) | reverse |
|
|
352
|
-
# Limitar a top 10
|
|
353
|
-
.[0:10]
|
|
354
|
-
'
|
|
331
|
+
# Si es JSON, retornar directamente
|
|
332
|
+
if echo "$check_output" | jq empty 2>/dev/null; then
|
|
333
|
+
echo "$check_output"
|
|
334
|
+
return
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
# Parsear texto de updates disponibles
|
|
338
|
+
local -a updates=()
|
|
339
|
+
while IFS= read -r line; do
|
|
340
|
+
[ -z "$line" ] && continue
|
|
341
|
+
local name
|
|
342
|
+
name=$(echo "$line" | grep -oE '[a-z][a-z0-9_-]+' | head -1)
|
|
343
|
+
[ -z "$name" ] && continue
|
|
344
|
+
updates+=("{\"id\":\"$name\",\"update_available\":true,\"update_cmd\":\"npx skills update $name\"}")
|
|
345
|
+
done <<< "$check_output"
|
|
346
|
+
|
|
347
|
+
if [ ${#updates[@]} -eq 0 ]; then
|
|
348
|
+
echo "[]"
|
|
349
|
+
else
|
|
350
|
+
printf '%s\n' "${updates[@]}" | jq -s '.' 2>/dev/null || echo "[]"
|
|
351
|
+
fi
|
|
355
352
|
}
|
|
356
353
|
|
|
357
354
|
# ============================================================================
|
|
358
|
-
#
|
|
355
|
+
# 6. RANKING Y RECOMENDACIONES
|
|
359
356
|
# ============================================================================
|
|
360
357
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
local keywords="$
|
|
366
|
-
local keywords_hash=$(echo -n "$keywords" | md5 2>/dev/null || echo "$keywords" | md5sum | awk '{print $1}')
|
|
358
|
+
rank_and_recommend() {
|
|
359
|
+
local installed="$1"
|
|
360
|
+
local elsabro="$2"
|
|
361
|
+
local external="$3"
|
|
362
|
+
local keywords="$4"
|
|
367
363
|
|
|
368
|
-
if !
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
# Buscar en caché con jq
|
|
373
|
-
if command -v jq &> /dev/null; then
|
|
374
|
-
local cached=$(
|
|
375
|
-
jq --arg hash "$keywords_hash" '.cache[$hash]?' "$SKILLS_CACHE_FILE" 2>/dev/null
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
if [ "$cached" != "null" ] && [ -n "$cached" ]; then
|
|
379
|
-
log_success "Skill cache hit"
|
|
380
|
-
echo "$cached"
|
|
381
|
-
return 0
|
|
382
|
-
fi
|
|
364
|
+
if ! has_command jq; then
|
|
365
|
+
echo "$external"
|
|
366
|
+
return
|
|
383
367
|
fi
|
|
384
368
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
369
|
+
# Combinar todas las fuentes, deduplicar por id, y rankear
|
|
370
|
+
jq -n \
|
|
371
|
+
--argjson installed "$installed" \
|
|
372
|
+
--argjson elsabro "$elsabro" \
|
|
373
|
+
--argjson external "$external" \
|
|
374
|
+
--argjson keywords "$keywords" \
|
|
375
|
+
'
|
|
376
|
+
# Lista de IDs instalados para filtrar
|
|
377
|
+
($installed | map(.id)) as $installed_ids |
|
|
378
|
+
|
|
379
|
+
# Combinar elsabro + external, excluir ya instalados
|
|
380
|
+
($elsabro + $external)
|
|
381
|
+
| map(select(.id as $id | $installed_ids | index($id) | not))
|
|
382
|
+
|
|
383
|
+
# Deduplicar por id (preferir elsabro sobre external)
|
|
384
|
+
| group_by(.id)
|
|
385
|
+
| map(
|
|
386
|
+
sort_by(if .source == "elsabro" then 0 else 1 end)
|
|
387
|
+
| first
|
|
388
|
+
)
|
|
393
389
|
|
|
394
|
-
|
|
390
|
+
# Rankear
|
|
391
|
+
| map(. + {
|
|
392
|
+
rank: (
|
|
393
|
+
((.match_score // 0) * 100) +
|
|
394
|
+
(if .source == "elsabro" then 20 else 0 end) +
|
|
395
|
+
(if .source == "skills-registry" then 10 else 0 end)
|
|
396
|
+
)
|
|
397
|
+
})
|
|
395
398
|
|
|
396
|
-
#
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
mv "$SKILLS_CACHE_FILE.tmp" "$SKILLS_CACHE_FILE"
|
|
401
|
-
else
|
|
402
|
-
# Crear caché nuevo
|
|
403
|
-
jq -n --arg hash "$keywords_hash" --argjson skills "$skills" \
|
|
404
|
-
'{cache: {($hash): $skills}}' > "$SKILLS_CACHE_FILE"
|
|
405
|
-
fi
|
|
399
|
+
# Ordenar por rank descendente y limitar
|
|
400
|
+
| sort_by(.rank) | reverse
|
|
401
|
+
| .[0:'"$MAX_RESULTS"']
|
|
402
|
+
'
|
|
406
403
|
}
|
|
407
404
|
|
|
408
405
|
# ============================================================================
|
|
409
|
-
#
|
|
406
|
+
# 7. GENERAR OUTPUT JSON
|
|
410
407
|
# ============================================================================
|
|
411
408
|
|
|
412
|
-
|
|
413
|
-
# Entrada: keywords, skills de cada fuente, skills rankeados
|
|
414
|
-
# Salida: JSON con estructura completa
|
|
415
|
-
format_discovery_output() {
|
|
409
|
+
generate_output() {
|
|
416
410
|
local keywords="$1"
|
|
417
|
-
local
|
|
418
|
-
local
|
|
419
|
-
local
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
"message": "jq not available"
|
|
428
|
-
}
|
|
429
|
-
EOF
|
|
411
|
+
local installed="$2"
|
|
412
|
+
local elsabro="$3"
|
|
413
|
+
local external="$4"
|
|
414
|
+
local recommended="$5"
|
|
415
|
+
local updates="$6"
|
|
416
|
+
|
|
417
|
+
if ! has_command jq; then
|
|
418
|
+
cat <<NOJQ
|
|
419
|
+
{"status":"error","message":"jq not available","timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
|
|
420
|
+
NOJQ
|
|
430
421
|
return 1
|
|
431
422
|
fi
|
|
432
423
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
424
|
+
jq -n \
|
|
425
|
+
--argjson keywords "$keywords" \
|
|
426
|
+
--argjson installed "$installed" \
|
|
427
|
+
--argjson elsabro "$elsabro" \
|
|
428
|
+
--argjson external "$external" \
|
|
429
|
+
--argjson recommended "$recommended" \
|
|
430
|
+
--argjson updates "$updates" \
|
|
431
|
+
'{
|
|
432
|
+
timestamp: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
|
|
433
|
+
status: "success",
|
|
434
|
+
keywords: $keywords,
|
|
435
|
+
installed: {
|
|
436
|
+
count: ($installed | length),
|
|
437
|
+
skills: $installed
|
|
438
|
+
},
|
|
439
|
+
discovered: {
|
|
440
|
+
elsabro: {
|
|
441
|
+
count: ($elsabro | length),
|
|
442
|
+
skills: $elsabro
|
|
443
|
+
},
|
|
444
|
+
registry: {
|
|
445
|
+
count: ($external | length),
|
|
446
|
+
skills: $external
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
recommended: {
|
|
450
|
+
count: ($recommended | length),
|
|
451
|
+
skills: $recommended,
|
|
452
|
+
install_commands: [
|
|
453
|
+
$recommended[]
|
|
454
|
+
| select(.install_cmd != null)
|
|
455
|
+
| {name: .id, command: .install_cmd}
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
updates: {
|
|
459
|
+
count: ($updates | length),
|
|
460
|
+
available: $updates
|
|
461
|
+
},
|
|
462
|
+
actions: {
|
|
463
|
+
install_all: (
|
|
464
|
+
if ($recommended | length) > 0
|
|
465
|
+
then "npx skills add -g " + ([$recommended[].id] | join(" "))
|
|
466
|
+
else null
|
|
467
|
+
end
|
|
468
|
+
),
|
|
469
|
+
update_all: (
|
|
470
|
+
if ($updates | length) > 0
|
|
471
|
+
then "npx skills update"
|
|
472
|
+
else null
|
|
473
|
+
end
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
}'
|
|
466
477
|
}
|
|
467
478
|
|
|
468
479
|
# ============================================================================
|
|
469
|
-
#
|
|
480
|
+
# 8. MAIN
|
|
470
481
|
# ============================================================================
|
|
471
482
|
|
|
472
483
|
main() {
|
|
473
|
-
local
|
|
484
|
+
local task="${1:-}"
|
|
474
485
|
local complexity="${2:-medium}"
|
|
475
486
|
|
|
476
|
-
#
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
EOF
|
|
487
|
+
# Validate complexity
|
|
488
|
+
case "$complexity" in
|
|
489
|
+
low|medium|high) ;;
|
|
490
|
+
*) complexity="medium" ;;
|
|
491
|
+
esac
|
|
492
|
+
|
|
493
|
+
if [ -z "$task" ]; then
|
|
494
|
+
jq -n '{"status":"error","message":"Task description required","timestamp":(now | strftime("%Y-%m-%dT%H:%M:%SZ"))}'
|
|
485
495
|
exit 1
|
|
486
496
|
fi
|
|
487
497
|
|
|
488
|
-
|
|
498
|
+
ensure_dirs
|
|
499
|
+
log_info "Skill Discovery para: $task (complejidad: $complexity)"
|
|
489
500
|
|
|
490
|
-
#
|
|
501
|
+
# --- Fase 1: Keywords ---
|
|
491
502
|
log_info "Extrayendo keywords..."
|
|
492
|
-
local keywords
|
|
493
|
-
|
|
503
|
+
local keywords
|
|
504
|
+
keywords=$(extract_keywords "$task")
|
|
505
|
+
log_success "Keywords: $(echo "$keywords" | jq -r 'join(", ")' 2>/dev/null || echo "$keywords")"
|
|
494
506
|
|
|
495
|
-
#
|
|
496
|
-
local
|
|
497
|
-
|
|
507
|
+
# --- Fase 2: Cache check ---
|
|
508
|
+
local cache_key
|
|
509
|
+
cache_key=$(echo -n "${task}|${complexity}" | md5 2>/dev/null || echo -n "${task}|${complexity}" | md5sum 2>/dev/null | awk '{print $1}' || echo "nocache")
|
|
498
510
|
|
|
499
|
-
if
|
|
500
|
-
log_success "
|
|
501
|
-
|
|
502
|
-
echo "$cached_result"
|
|
511
|
+
if is_cache_valid "$cache_key"; then
|
|
512
|
+
log_success "Cache hit"
|
|
513
|
+
get_from_cache "$cache_key"
|
|
503
514
|
exit 0
|
|
504
515
|
fi
|
|
505
516
|
|
|
506
|
-
#
|
|
507
|
-
log_info "
|
|
508
|
-
local
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
517
|
+
# --- Fase 3: Escanear skills instalados ---
|
|
518
|
+
log_info "Escaneando skills instalados..."
|
|
519
|
+
local installed
|
|
520
|
+
installed=$(scan_installed_skills)
|
|
521
|
+
log_success "Instalados: $(echo "$installed" | jq 'length' 2>/dev/null || echo 0)"
|
|
522
|
+
|
|
523
|
+
# --- Fase 4: Escanear skills ELSABRO locales ---
|
|
524
|
+
log_info "Escaneando skills ELSABRO..."
|
|
525
|
+
local elsabro
|
|
526
|
+
elsabro=$(scan_elsabro_skills "$keywords")
|
|
527
|
+
log_success "ELSABRO: $(echo "$elsabro" | jq 'length' 2>/dev/null || echo 0)"
|
|
528
|
+
|
|
529
|
+
# --- Fase 5: Descubrir skills externos ---
|
|
530
|
+
log_info "Buscando en skills registry..."
|
|
531
|
+
local query_str
|
|
532
|
+
query_str=$(echo "$keywords" | jq -r 'join(" ")' 2>/dev/null || echo "$task")
|
|
533
|
+
local external
|
|
534
|
+
external=$(discover_via_npx_skills "$query_str")
|
|
535
|
+
log_success "Registry: $(echo "$external" | jq 'length' 2>/dev/null || echo 0)"
|
|
536
|
+
|
|
537
|
+
# --- Fase 6: Check updates ---
|
|
538
|
+
log_info "Verificando actualizaciones..."
|
|
539
|
+
local updates
|
|
540
|
+
updates=$(check_skill_updates)
|
|
541
|
+
log_success "Updates: $(echo "$updates" | jq 'length' 2>/dev/null || echo 0)"
|
|
542
|
+
|
|
543
|
+
# --- Fase 7: Ranking ---
|
|
544
|
+
log_info "Rankeando recomendaciones..."
|
|
545
|
+
local recommended
|
|
546
|
+
recommended=$(rank_and_recommend "$installed" "$elsabro" "$external" "$keywords")
|
|
547
|
+
log_success "Recomendados: $(echo "$recommended" | jq 'length' 2>/dev/null || echo 0)"
|
|
548
|
+
|
|
549
|
+
# --- Fase 8: Output ---
|
|
550
|
+
local output
|
|
551
|
+
output=$(generate_output "$keywords" "$installed" "$elsabro" "$external" "$recommended" "$updates")
|
|
552
|
+
|
|
553
|
+
# Guardar en cache
|
|
554
|
+
save_to_cache "$cache_key" "$output" 2>/dev/null || true
|
|
555
|
+
|
|
556
|
+
# Output final a STDOUT (esto captura el Flow Engine)
|
|
529
557
|
echo "$output"
|
|
530
558
|
|
|
531
|
-
log_success "Discovery completado"
|
|
532
|
-
exit 0
|
|
559
|
+
log_success "Skill Discovery completado"
|
|
533
560
|
}
|
|
534
561
|
|
|
535
562
|
# ============================================================================
|