monol-plugin-scout 2.1.3 → 4.1.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.
Files changed (87) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +244 -0
  3. package/monol-plugin-scout-pkg/CLAUDE.md +125 -8
  4. package/monol-plugin-scout-pkg/api/mock/categories.json +81 -0
  5. package/monol-plugin-scout-pkg/api/mock/insights.json +111 -0
  6. package/monol-plugin-scout-pkg/api/mock/marketplace.json +501 -0
  7. package/monol-plugin-scout-pkg/api/mock/trending.json +96 -0
  8. package/monol-plugin-scout-pkg/commands/console.md +79 -0
  9. package/monol-plugin-scout-pkg/commands/frequency.md +32 -0
  10. package/monol-plugin-scout-pkg/commands/install.md +32 -0
  11. package/monol-plugin-scout-pkg/commands/priority.md +30 -0
  12. package/monol-plugin-scout-pkg/commands/quiet.md +32 -0
  13. package/monol-plugin-scout-pkg/commands/schedule.md +32 -0
  14. package/monol-plugin-scout-pkg/commands/scout.md +8 -5
  15. package/monol-plugin-scout-pkg/commands/skills.md +160 -0
  16. package/monol-plugin-scout-pkg/commands/team.md +32 -0
  17. package/monol-plugin-scout-pkg/commands/timing.md +32 -0
  18. package/monol-plugin-scout-pkg/commands/trust.md +85 -0
  19. package/monol-plugin-scout-pkg/commands/trusted-install.md +98 -0
  20. package/monol-plugin-scout-pkg/commands/uninstall.md +31 -0
  21. package/monol-plugin-scout-pkg/config.yaml +57 -0
  22. package/monol-plugin-scout-pkg/data/.cache/676d5ab664292155e5f509c69d4edae1 +10 -0
  23. package/monol-plugin-scout-pkg/data/.session +8 -0
  24. package/monol-plugin-scout-pkg/data/analytics.json +102 -0
  25. package/monol-plugin-scout-pkg/data/history.json +67 -11
  26. package/monol-plugin-scout-pkg/data/logs/scout.log +1 -0
  27. package/monol-plugin-scout-pkg/data/schedules.json +17 -0
  28. package/monol-plugin-scout-pkg/data/team.json +53 -0
  29. package/monol-plugin-scout-pkg/data/usage.json +100 -7
  30. package/monol-plugin-scout-pkg/docs/ARCHITECTURE.md +201 -0
  31. package/monol-plugin-scout-pkg/hooks/generate-insights.sh +102 -0
  32. package/monol-plugin-scout-pkg/hooks/hooks.json +36 -1
  33. package/monol-plugin-scout-pkg/hooks/on-session-end.sh +136 -0
  34. package/monol-plugin-scout-pkg/hooks/on-session-start.sh +43 -0
  35. package/monol-plugin-scout-pkg/hooks/on-stop.md +79 -0
  36. package/monol-plugin-scout-pkg/hooks/open-console.sh +111 -0
  37. package/monol-plugin-scout-pkg/hooks/open-dashboard.sh +61 -0
  38. package/monol-plugin-scout-pkg/hooks/track-usage.sh +59 -0
  39. package/monol-plugin-scout-pkg/lib/ai-recommender.sh +505 -0
  40. package/monol-plugin-scout-pkg/lib/cache.sh +194 -0
  41. package/monol-plugin-scout-pkg/lib/data-validator.sh +360 -0
  42. package/monol-plugin-scout-pkg/lib/error-handler.sh +296 -0
  43. package/monol-plugin-scout-pkg/lib/logger.sh +263 -0
  44. package/monol-plugin-scout-pkg/lib/plugin-manager.sh +239 -0
  45. package/monol-plugin-scout-pkg/lib/priority-scorer.sh +262 -0
  46. package/monol-plugin-scout-pkg/lib/profile-learner.sh +339 -0
  47. package/monol-plugin-scout-pkg/lib/project-analyzer.sh +281 -0
  48. package/monol-plugin-scout-pkg/lib/recommendation-controller.sh +290 -0
  49. package/monol-plugin-scout-pkg/lib/rejection-learner.sh +232 -0
  50. package/monol-plugin-scout-pkg/lib/scheduler.sh +275 -0
  51. package/monol-plugin-scout-pkg/lib/skill-scout.sh +729 -0
  52. package/monol-plugin-scout-pkg/lib/sync.sh +221 -0
  53. package/monol-plugin-scout-pkg/lib/team-learner.sh +450 -0
  54. package/monol-plugin-scout-pkg/lib/team-manager.sh +369 -0
  55. package/monol-plugin-scout-pkg/lib/trend-learner.sh +428 -0
  56. package/monol-plugin-scout-pkg/lib/trust-manager.sh +261 -0
  57. package/monol-plugin-scout-pkg/lib/trusted-installer.sh +738 -0
  58. package/monol-plugin-scout-pkg/plugin.json +3 -2
  59. package/monol-plugin-scout-pkg/skills/audit.md +6 -0
  60. package/monol-plugin-scout-pkg/skills/cleanup.md +6 -0
  61. package/monol-plugin-scout-pkg/skills/compare.md +6 -0
  62. package/monol-plugin-scout-pkg/skills/console.md +315 -0
  63. package/monol-plugin-scout-pkg/skills/explore.md +6 -0
  64. package/monol-plugin-scout-pkg/skills/fork.md +6 -0
  65. package/monol-plugin-scout-pkg/skills/frequency.md +93 -0
  66. package/monol-plugin-scout-pkg/skills/install.md +127 -0
  67. package/monol-plugin-scout-pkg/skills/priority.md +77 -0
  68. package/monol-plugin-scout-pkg/skills/quiet.md +73 -0
  69. package/monol-plugin-scout-pkg/skills/schedule.md +95 -0
  70. package/monol-plugin-scout-pkg/skills/scout.md +27 -17
  71. package/monol-plugin-scout-pkg/skills/skills.md +230 -0
  72. package/monol-plugin-scout-pkg/skills/team.md +117 -0
  73. package/monol-plugin-scout-pkg/skills/timing.md +97 -0
  74. package/monol-plugin-scout-pkg/skills/trust.md +120 -0
  75. package/monol-plugin-scout-pkg/skills/trusted-install.md +264 -0
  76. package/monol-plugin-scout-pkg/skills/uninstall.md +100 -0
  77. package/monol-plugin-scout-pkg/web/components/activity-chart.js +208 -0
  78. package/monol-plugin-scout-pkg/web/components/index.js +27 -0
  79. package/monol-plugin-scout-pkg/web/components/insight-card.js +365 -0
  80. package/monol-plugin-scout-pkg/web/components/overview.js +154 -0
  81. package/monol-plugin-scout-pkg/web/components/plugin-list.js +242 -0
  82. package/monol-plugin-scout-pkg/web/components/stats-card.js +126 -0
  83. package/monol-plugin-scout-pkg/web/components/team-list.js +346 -0
  84. package/monol-plugin-scout-pkg/web/console.html +2098 -0
  85. package/monol-plugin-scout-pkg/web/dashboard.html +2106 -0
  86. package/monol-plugin-scout-pkg/web/manifest.json +29 -0
  87. package/package.json +1 -1
@@ -0,0 +1,729 @@
1
+ #!/bin/bash
2
+ # Plugin Scout - skills.sh 통합 모듈
3
+ # skills.sh 마켓플레이스에서 스킬 탐색, 추천, 설치
4
+
5
+ PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$0")/..}"
6
+ DATA_DIR="$PLUGIN_ROOT/data"
7
+
8
+ SKILLS_API="https://skills.sh/api"
9
+ CACHE_DIR="$DATA_DIR/.cache"
10
+ SKILLS_CACHE="$CACHE_DIR/skills-catalog.json"
11
+ SKILLS_DIR="$HOME/.claude/skills"
12
+ HISTORY_FILE="$DATA_DIR/history.json"
13
+ CONFIG_FILE="$PLUGIN_ROOT/config.yaml"
14
+ INSTALL_LOG="$DATA_DIR/.last-skill-install.json"
15
+
16
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
17
+
18
+ # ============================================================================
19
+ # Config reader
20
+ # ============================================================================
21
+
22
+ # config.yaml의 skills_sh 섹션에서 값 읽기
23
+ # Usage: read_config <key> <default>
24
+ read_config() {
25
+ local key="$1"
26
+ local default="$2"
27
+
28
+ if [ ! -f "$CONFIG_FILE" ]; then
29
+ echo "$default"
30
+ return
31
+ fi
32
+
33
+ # skills_sh 섹션에서 key 추출 (grep + awk)
34
+ local value
35
+ value=$(grep -A 20 "^skills_sh:" "$CONFIG_FILE" | grep "^ ${key}:" | awk '{print $2}' | tr -d '"' | tr -d "'")
36
+
37
+ if [ -z "$value" ]; then
38
+ echo "$default"
39
+ else
40
+ echo "$value"
41
+ fi
42
+ }
43
+
44
+ # ============================================================================
45
+ # npx 확인
46
+ # ============================================================================
47
+
48
+ check_npx() {
49
+ if command -v npx &> /dev/null; then
50
+ echo "npx available: $(npx --version 2>/dev/null)"
51
+ return 0
52
+ else
53
+ echo "Error: npx not found."
54
+ echo ""
55
+ echo "npx is required for skills.sh integration."
56
+ echo "Install Node.js (v18+) to get npx:"
57
+ echo " - macOS: brew install node"
58
+ echo " - Linux: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
59
+ echo " - Windows: https://nodejs.org/"
60
+ echo " - nvm: nvm install --lts"
61
+ return 1
62
+ fi
63
+ }
64
+
65
+ # ============================================================================
66
+ # 카탈로그 가져오기
67
+ # ============================================================================
68
+
69
+ fetch_catalog() {
70
+ mkdir -p "$CACHE_DIR"
71
+
72
+ # 캐시 TTL 확인
73
+ local cache_ttl
74
+ cache_ttl=$(read_config "cache_ttl" "3600")
75
+
76
+ if [ -f "$SKILLS_CACHE" ]; then
77
+ local cache_age=0
78
+ if [[ "$OSTYPE" == "darwin"* ]]; then
79
+ local cache_mtime
80
+ cache_mtime=$(stat -f "%m" "$SKILLS_CACHE" 2>/dev/null || echo 0)
81
+ local now
82
+ now=$(date +%s)
83
+ cache_age=$((now - cache_mtime))
84
+ else
85
+ local cache_mtime
86
+ cache_mtime=$(stat -c "%Y" "$SKILLS_CACHE" 2>/dev/null || echo 0)
87
+ local now
88
+ now=$(date +%s)
89
+ cache_age=$((now - cache_mtime))
90
+ fi
91
+
92
+ if [ "$cache_age" -lt "$cache_ttl" ]; then
93
+ cat "$SKILLS_CACHE"
94
+ return 0
95
+ fi
96
+ fi
97
+
98
+ # API에서 가져오기 (페이지네이션)
99
+ local api_base
100
+ api_base=$(read_config "api_base" "$SKILLS_API")
101
+
102
+ local all_skills="[]"
103
+ local offset=0
104
+ local limit=500
105
+ local has_more=true
106
+
107
+ while [ "$has_more" = "true" ]; do
108
+ local response
109
+ response=$(curl -s --max-time 10 "${api_base}/skills?limit=${limit}&offset=${offset}" 2>/dev/null)
110
+
111
+ if [ $? -ne 0 ] || [ -z "$response" ]; then
112
+ # 네트워크 실패 시 캐시 폴백
113
+ if [ -f "$SKILLS_CACHE" ]; then
114
+ cat "$SKILLS_CACHE"
115
+ return 0
116
+ fi
117
+ echo "[]"
118
+ return 1
119
+ fi
120
+
121
+ # 응답에서 스킬 추출하여 병합
122
+ local skills_page
123
+ skills_page=$(echo "$response" | jq -c '.skills // .data // []' 2>/dev/null)
124
+
125
+ if [ -z "$skills_page" ] || [ "$skills_page" = "null" ] || [ "$skills_page" = "[]" ]; then
126
+ break
127
+ fi
128
+
129
+ all_skills=$(echo "$all_skills" "$skills_page" | jq -s '.[0] + .[1]' 2>/dev/null)
130
+
131
+ # hasMore 확인
132
+ has_more=$(echo "$response" | jq -r '.hasMore // false' 2>/dev/null)
133
+ offset=$((offset + limit))
134
+
135
+ # Rate limit
136
+ sleep 0.1
137
+ done
138
+
139
+ # 캐시 저장
140
+ echo "$all_skills" > "$SKILLS_CACHE"
141
+ echo "$all_skills"
142
+ }
143
+
144
+ # ============================================================================
145
+ # Anthropic 공식 스킬 체크
146
+ # ============================================================================
147
+
148
+ is_anthropic_skill() {
149
+ local skill_json="$1"
150
+ local top_source
151
+ top_source=$(echo "$skill_json" | jq -r '.topSource // ""' 2>/dev/null)
152
+
153
+ if [ "$top_source" = "anthropics/skills" ]; then
154
+ return 0
155
+ else
156
+ return 1
157
+ fi
158
+ }
159
+
160
+ # ============================================================================
161
+ # 설치된 스킬 목록
162
+ # ============================================================================
163
+
164
+ get_installed_skills() {
165
+ local installed=()
166
+
167
+ # 글로벌 스킬 (~/.claude/skills/)
168
+ if [ -d "$SKILLS_DIR" ]; then
169
+ for skill_dir in "$SKILLS_DIR"/*/; do
170
+ if [ -d "$skill_dir" ]; then
171
+ local name
172
+ name=$(basename "$skill_dir")
173
+ installed+=("$name")
174
+ fi
175
+ done
176
+ fi
177
+
178
+ # 플러그인 내부 스킬
179
+ if [ -d "$PLUGIN_ROOT/skills" ]; then
180
+ for skill_file in "$PLUGIN_ROOT/skills"/*.md; do
181
+ if [ -f "$skill_file" ]; then
182
+ local name
183
+ name=$(basename "$skill_file" .md)
184
+ installed+=("$name")
185
+ fi
186
+ done
187
+ fi
188
+
189
+ printf '%s\n' "${installed[@]}" | jq -R -s -c 'split("\n") | map(select(. != "")) | unique'
190
+ }
191
+
192
+ # ============================================================================
193
+ # Anthropic 공식 스킬 자동 설치
194
+ # ============================================================================
195
+
196
+ auto_install_official() {
197
+ local auto_enabled
198
+ auto_enabled=$(read_config "auto_install_official" "true")
199
+
200
+ if [ "$auto_enabled" != "true" ]; then
201
+ echo '{"skipped": true, "reason": "auto_install_official disabled"}'
202
+ return 0
203
+ fi
204
+
205
+ # npx 확인
206
+ if ! check_npx > /dev/null 2>&1; then
207
+ echo '{"skipped": true, "reason": "npx not available"}'
208
+ return 1
209
+ fi
210
+
211
+ local catalog
212
+ catalog=$(fetch_catalog)
213
+
214
+ if [ "$catalog" = "[]" ] || [ -z "$catalog" ]; then
215
+ echo '{"skipped": true, "reason": "empty catalog"}'
216
+ return 0
217
+ fi
218
+
219
+ local installed
220
+ installed=$(get_installed_skills)
221
+
222
+ local results=()
223
+ local install_count=0
224
+ local skip_count=0
225
+ local fail_count=0
226
+
227
+ # Anthropic 공식 스킬 필터링 및 설치
228
+ local anthropic_skills
229
+ anthropic_skills=$(echo "$catalog" | jq -c '[.[] | select(.topSource == "anthropics/skills")]' 2>/dev/null)
230
+
231
+ local skill_count
232
+ skill_count=$(echo "$anthropic_skills" | jq 'length' 2>/dev/null)
233
+
234
+ for i in $(seq 0 $((skill_count - 1))); do
235
+ local skill
236
+ skill=$(echo "$anthropic_skills" | jq -c ".[$i]" 2>/dev/null)
237
+
238
+ local skill_id
239
+ skill_id=$(echo "$skill" | jq -r '.id // .name // ""' 2>/dev/null)
240
+
241
+ if [ -z "$skill_id" ]; then
242
+ continue
243
+ fi
244
+
245
+ # 이미 설치되었는지 확인
246
+ local is_installed
247
+ is_installed=$(echo "$installed" | jq -r --arg id "$skill_id" 'map(select(. == $id)) | length' 2>/dev/null)
248
+
249
+ if [ "$is_installed" -gt 0 ] 2>/dev/null; then
250
+ skip_count=$((skip_count + 1))
251
+ continue
252
+ fi
253
+
254
+ # 설치 시도
255
+ local install_result
256
+ install_result=$(npx skills add "$skill_id" 2>&1)
257
+
258
+ if [ $? -eq 0 ]; then
259
+ install_count=$((install_count + 1))
260
+ results+=("{\"id\": \"$skill_id\", \"status\": \"installed\"}")
261
+
262
+ # history.json에 기록
263
+ if [ -f "$HISTORY_FILE" ]; then
264
+ local timestamp
265
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
266
+ jq --arg skill "$skill_id" --arg ts "$timestamp" '
267
+ .lastUpdated = $ts |
268
+ .skills.installed[$skill] = {
269
+ installedAt: $ts,
270
+ source: "anthropics/skills",
271
+ autoInstalled: true
272
+ }
273
+ ' "$HISTORY_FILE" > "$HISTORY_FILE.tmp" && mv "$HISTORY_FILE.tmp" "$HISTORY_FILE"
274
+ fi
275
+ else
276
+ fail_count=$((fail_count + 1))
277
+ results+=("{\"id\": \"$skill_id\", \"status\": \"failed\", \"error\": \"$(echo "$install_result" | head -1)\"}")
278
+ fi
279
+ done
280
+
281
+ # 결과 기록
282
+ local timestamp
283
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
284
+ local results_json
285
+ results_json=$(printf '%s\n' "${results[@]}" | jq -s -c '.' 2>/dev/null || echo "[]")
286
+
287
+ cat > "$INSTALL_LOG" <<EOF
288
+ {
289
+ "timestamp": "$timestamp",
290
+ "type": "auto_install_official",
291
+ "installed": $install_count,
292
+ "skipped": $skip_count,
293
+ "failed": $fail_count,
294
+ "details": $results_json
295
+ }
296
+ EOF
297
+
298
+ cat "$INSTALL_LOG"
299
+ }
300
+
301
+ # ============================================================================
302
+ # 스킬 점수 계산
303
+ # ============================================================================
304
+
305
+ score_skill() {
306
+ local skill_json="$1"
307
+ local project_types="$2"
308
+
309
+ local score=0
310
+
311
+ # --- installs popularity: 50% ---
312
+ local installs
313
+ installs=$(echo "$skill_json" | jq -r '.installs // .downloads // 0' 2>/dev/null)
314
+
315
+ # 정규화: 상위 기준 1000 설치를 100점으로 가정
316
+ local pop_score=0
317
+ if [ "$installs" -ge 1000 ] 2>/dev/null; then
318
+ pop_score=100
319
+ elif [ "$installs" -ge 500 ] 2>/dev/null; then
320
+ pop_score=80
321
+ elif [ "$installs" -ge 100 ] 2>/dev/null; then
322
+ pop_score=60
323
+ elif [ "$installs" -ge 50 ] 2>/dev/null; then
324
+ pop_score=40
325
+ elif [ "$installs" -ge 10 ] 2>/dev/null; then
326
+ pop_score=20
327
+ else
328
+ pop_score=10
329
+ fi
330
+
331
+ # --- project matching: 30% ---
332
+ local match_score=0
333
+ local skill_name
334
+ skill_name=$(echo "$skill_json" | jq -r '.name // ""' 2>/dev/null | tr '[:upper:]' '[:lower:]')
335
+ local skill_tags
336
+ skill_tags=$(echo "$skill_json" | jq -r '(.tags // []) | join(" ")' 2>/dev/null | tr '[:upper:]' '[:lower:]')
337
+ local skill_desc
338
+ skill_desc=$(echo "$skill_json" | jq -r '.description // ""' 2>/dev/null | tr '[:upper:]' '[:lower:]')
339
+
340
+ local search_text="$skill_name $skill_tags $skill_desc"
341
+
342
+ # project_types가 JSON 배열인 경우 파싱
343
+ if echo "$project_types" | jq -e '.' > /dev/null 2>&1; then
344
+ local type_count
345
+ type_count=$(echo "$project_types" | jq -r '.[]' 2>/dev/null | while read -r ptype; do
346
+ if echo "$search_text" | grep -qi "$ptype"; then
347
+ echo "match"
348
+ fi
349
+ done | wc -l | tr -d ' ')
350
+
351
+ if [ "$type_count" -gt 0 ] 2>/dev/null; then
352
+ match_score=$((type_count * 33))
353
+ [ "$match_score" -gt 100 ] && match_score=100
354
+ fi
355
+ fi
356
+
357
+ # --- source trust: 20% ---
358
+ local trust_score=0
359
+ local top_source
360
+ top_source=$(echo "$skill_json" | jq -r '.topSource // ""' 2>/dev/null)
361
+
362
+ case "$top_source" in
363
+ anthropics/*) trust_score=100 ;;
364
+ verified/*|official/*) trust_score=70 ;;
365
+ *) trust_score=40 ;;
366
+ esac
367
+
368
+ # 가중 합산
369
+ score=$(( (pop_score * 50 + match_score * 30 + trust_score * 20) / 100 ))
370
+
371
+ # 0-100 범위 제한
372
+ [ "$score" -gt 100 ] && score=100
373
+ [ "$score" -lt 0 ] && score=0
374
+
375
+ echo "$score"
376
+ }
377
+
378
+ # ============================================================================
379
+ # 추천
380
+ # ============================================================================
381
+
382
+ recommend() {
383
+ local count="${1:-5}"
384
+ local max_count
385
+ max_count=$(read_config "max_recommendations" "5")
386
+
387
+ if [ "$count" -gt "$max_count" ] 2>/dev/null; then
388
+ count="$max_count"
389
+ fi
390
+
391
+ local catalog
392
+ catalog=$(fetch_catalog)
393
+
394
+ if [ "$catalog" = "[]" ] || [ -z "$catalog" ]; then
395
+ echo "[]"
396
+ return 0
397
+ fi
398
+
399
+ local installed
400
+ installed=$(get_installed_skills)
401
+
402
+ # 프로젝트 타입 가져오기
403
+ local project_types="[]"
404
+ if [ -f "$PLUGIN_ROOT/lib/project-analyzer.sh" ]; then
405
+ project_types=$(bash "$PLUGIN_ROOT/lib/project-analyzer.sh" types 2>/dev/null || echo "[]")
406
+ fi
407
+
408
+ # 각 스킬 점수 계산 (설치된 것 제외)
409
+ local skill_count
410
+ skill_count=$(echo "$catalog" | jq 'length' 2>/dev/null)
411
+
412
+ local scored_skills="[]"
413
+
414
+ for i in $(seq 0 $((skill_count - 1))); do
415
+ local skill
416
+ skill=$(echo "$catalog" | jq -c ".[$i]" 2>/dev/null)
417
+
418
+ local skill_id
419
+ skill_id=$(echo "$skill" | jq -r '.id // .name // ""' 2>/dev/null)
420
+
421
+ if [ -z "$skill_id" ]; then
422
+ continue
423
+ fi
424
+
425
+ # 이미 설치된 스킬 제외
426
+ local is_installed
427
+ is_installed=$(echo "$installed" | jq -r --arg id "$skill_id" 'map(select(. == $id)) | length' 2>/dev/null)
428
+
429
+ if [ "$is_installed" -gt 0 ] 2>/dev/null; then
430
+ continue
431
+ fi
432
+
433
+ local skill_score
434
+ skill_score=$(score_skill "$skill" "$project_types")
435
+
436
+ scored_skills=$(echo "$scored_skills" | jq --argjson skill "$skill" --argjson score "$skill_score" '. + [$skill + {"_score": $score}]' 2>/dev/null)
437
+ done
438
+
439
+ # 점수 내림차순 정렬, 상위 N개
440
+ echo "$scored_skills" | jq -c --argjson n "$count" '[sort_by(-._score)][:$n]' 2>/dev/null || echo "[]"
441
+ }
442
+
443
+ # ============================================================================
444
+ # 키워드 검색
445
+ # ============================================================================
446
+
447
+ search() {
448
+ local query="$1"
449
+
450
+ if [ -z "$query" ]; then
451
+ echo "Error: search query required"
452
+ echo "Usage: $0 search <query>"
453
+ return 1
454
+ fi
455
+
456
+ local catalog
457
+ catalog=$(fetch_catalog)
458
+
459
+ if [ "$catalog" = "[]" ] || [ -z "$catalog" ]; then
460
+ echo "[]"
461
+ return 0
462
+ fi
463
+
464
+ local query_lower
465
+ query_lower=$(echo "$query" | tr '[:upper:]' '[:lower:]')
466
+
467
+ # 프로젝트 타입
468
+ local project_types="[]"
469
+ if [ -f "$PLUGIN_ROOT/lib/project-analyzer.sh" ]; then
470
+ project_types=$(bash "$PLUGIN_ROOT/lib/project-analyzer.sh" types 2>/dev/null || echo "[]")
471
+ fi
472
+
473
+ # 이름, 태그, 설명에서 검색 (대소문자 무시)
474
+ local matches
475
+ matches=$(echo "$catalog" | jq -c --arg q "$query_lower" '[.[] | select(
476
+ (.name // "" | ascii_downcase | contains($q)) or
477
+ ((.tags // []) | map(ascii_downcase) | any(contains($q))) or
478
+ (.description // "" | ascii_downcase | contains($q))
479
+ )]' 2>/dev/null)
480
+
481
+ if [ "$matches" = "[]" ] || [ -z "$matches" ]; then
482
+ echo "[]"
483
+ return 0
484
+ fi
485
+
486
+ # 매치된 스킬에 점수 추가
487
+ local match_count
488
+ match_count=$(echo "$matches" | jq 'length' 2>/dev/null)
489
+
490
+ local scored_matches="[]"
491
+
492
+ for i in $(seq 0 $((match_count - 1))); do
493
+ local skill
494
+ skill=$(echo "$matches" | jq -c ".[$i]" 2>/dev/null)
495
+
496
+ local skill_score
497
+ skill_score=$(score_skill "$skill" "$project_types")
498
+
499
+ scored_matches=$(echo "$scored_matches" | jq --argjson skill "$skill" --argjson score "$skill_score" '. + [$skill + {"_score": $score}]' 2>/dev/null)
500
+ done
501
+
502
+ # 점수순 정렬
503
+ echo "$scored_matches" | jq -c '[sort_by(-._score)]' 2>/dev/null || echo "[]"
504
+ }
505
+
506
+ # ============================================================================
507
+ # 시너지 콤보 추천
508
+ # ============================================================================
509
+
510
+ suggest_combos() {
511
+ local combo_enabled
512
+ combo_enabled=$(read_config "combo_suggestions" "true")
513
+
514
+ if [ "$combo_enabled" != "true" ]; then
515
+ echo '{"skipped": true, "reason": "combo_suggestions disabled"}'
516
+ return 0
517
+ fi
518
+
519
+ # 설치된 플러그인 조회
520
+ local installed_plugins="[]"
521
+ if [ -f "$CLAUDE_SETTINGS" ] && command -v jq &> /dev/null; then
522
+ installed_plugins=$(jq -r '[.enabledPlugins // {} | to_entries[] | select(.value == true) | .key]' "$CLAUDE_SETTINGS" 2>/dev/null || echo "[]")
523
+ fi
524
+
525
+ local catalog
526
+ catalog=$(fetch_catalog)
527
+
528
+ # 콤보 매핑: 플러그인 키워드 → 보완 스킬 키워드
529
+ local combos='{
530
+ "typescript-lsp": ["typescript-strict", "type-coverage", "ts-refactor"],
531
+ "code-review": ["pr-description", "review-checklist"],
532
+ "commit-commands": ["conventional-commits", "changelog-gen"],
533
+ "sentry": ["error-tracking", "alert-config"],
534
+ "react": ["react-patterns", "component-gen"],
535
+ "testing": ["test-gen", "coverage-report"]
536
+ }'
537
+
538
+ local recommendations="[]"
539
+
540
+ # 각 설치된 플러그인에 대해 콤보 검색
541
+ echo "$installed_plugins" | jq -r '.[]' 2>/dev/null | while read -r plugin; do
542
+ local plugin_lower
543
+ plugin_lower=$(echo "$plugin" | tr '[:upper:]' '[:lower:]')
544
+
545
+ # 콤보 맵에서 매칭 키워드 찾기
546
+ echo "$combos" | jq -r 'to_entries[] | .key' 2>/dev/null | while read -r combo_key; do
547
+ if echo "$plugin_lower" | grep -qi "$combo_key"; then
548
+ local skill_keywords
549
+ skill_keywords=$(echo "$combos" | jq -r --arg k "$combo_key" '.[$k][]' 2>/dev/null)
550
+
551
+ echo "$skill_keywords" | while read -r keyword; do
552
+ # 카탈로그에서 매칭 스킬 검색
553
+ local matched
554
+ matched=$(echo "$catalog" | jq -c --arg kw "$keyword" '[.[] | select(
555
+ (.name // "" | ascii_downcase | contains($kw)) or
556
+ ((.tags // []) | map(ascii_downcase) | any(contains($kw)))
557
+ )] | .[0] // empty' 2>/dev/null)
558
+
559
+ if [ -n "$matched" ] && [ "$matched" != "null" ]; then
560
+ echo "$matched" | jq -c --arg plugin "$plugin" --arg combo "$combo_key" '. + {"_reason": ("synergy with " + $plugin), "_combo": $combo}'
561
+ fi
562
+ done
563
+ fi
564
+ done
565
+ done | jq -s -c 'unique_by(.name // .id)' 2>/dev/null || echo "[]"
566
+ }
567
+
568
+ # ============================================================================
569
+ # 스킬 설치
570
+ # ============================================================================
571
+
572
+ install_skill() {
573
+ local skill_id="$1"
574
+
575
+ if [ -z "$skill_id" ]; then
576
+ echo "Error: skill ID required"
577
+ echo "Usage: $0 install <skill-id>"
578
+ return 1
579
+ fi
580
+
581
+ # npx 확인
582
+ if ! check_npx > /dev/null 2>&1; then
583
+ check_npx
584
+ return 1
585
+ fi
586
+
587
+ echo "Installing skill: $skill_id"
588
+
589
+ local install_result
590
+ install_result=$(npx skills add "$skill_id" 2>&1)
591
+ local exit_code=$?
592
+
593
+ if [ $exit_code -eq 0 ]; then
594
+ # history.json에 기록
595
+ if [ -f "$HISTORY_FILE" ]; then
596
+ local timestamp
597
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
598
+ jq --arg skill "$skill_id" --arg ts "$timestamp" '
599
+ .lastUpdated = $ts |
600
+ .skills.installed[$skill] = {
601
+ installedAt: $ts,
602
+ source: "skills.sh",
603
+ autoInstalled: false
604
+ }
605
+ ' "$HISTORY_FILE" > "$HISTORY_FILE.tmp" && mv "$HISTORY_FILE.tmp" "$HISTORY_FILE"
606
+ fi
607
+
608
+ echo "Installed: $skill_id"
609
+ echo "Skill is now available in Claude Code."
610
+ return 0
611
+ else
612
+ echo "Error installing $skill_id:"
613
+ echo "$install_result"
614
+ return 1
615
+ fi
616
+ }
617
+
618
+ # ============================================================================
619
+ # 상태 표시
620
+ # ============================================================================
621
+
622
+ show_status() {
623
+ echo "=== Skill Scout Status ==="
624
+ echo ""
625
+
626
+ # 설치된 스킬
627
+ local installed
628
+ installed=$(get_installed_skills)
629
+ local installed_count
630
+ installed_count=$(echo "$installed" | jq 'length' 2>/dev/null || echo 0)
631
+
632
+ echo "Installed Skills: $installed_count"
633
+ if [ "$installed_count" -gt 0 ] 2>/dev/null; then
634
+ echo "$installed" | jq -r '.[] | " - " + .' 2>/dev/null
635
+ fi
636
+ echo ""
637
+
638
+ # 마지막 동기화 시각
639
+ if [ -f "$SKILLS_CACHE" ]; then
640
+ local cache_mtime
641
+ if [[ "$OSTYPE" == "darwin"* ]]; then
642
+ cache_mtime=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$SKILLS_CACHE" 2>/dev/null)
643
+ else
644
+ cache_mtime=$(stat -c "%y" "$SKILLS_CACHE" 2>/dev/null | cut -d'.' -f1)
645
+ fi
646
+ echo "Last Sync: $cache_mtime"
647
+ else
648
+ echo "Last Sync: never"
649
+ fi
650
+ echo ""
651
+
652
+ # skills_sh 설정 요약
653
+ local sh_enabled
654
+ sh_enabled=$(read_config "enabled" "true")
655
+ local auto_official
656
+ auto_official=$(read_config "auto_install_official" "true")
657
+ local cache_ttl
658
+ cache_ttl=$(read_config "cache_ttl" "3600")
659
+ local max_rec
660
+ max_rec=$(read_config "max_recommendations" "5")
661
+
662
+ echo "Config (skills_sh):"
663
+ echo " enabled: $sh_enabled"
664
+ echo " auto_install_official: $auto_official"
665
+ echo " cache_ttl: ${cache_ttl}s"
666
+ echo " max_recommendations: $max_rec"
667
+ echo ""
668
+
669
+ # 마지막 자동 설치 결과
670
+ if [ -f "$INSTALL_LOG" ]; then
671
+ echo "Last Auto-Install:"
672
+ jq -r '
673
+ " timestamp: " + .timestamp,
674
+ " installed: " + (.installed | tostring),
675
+ " skipped: " + (.skipped | tostring),
676
+ " failed: " + (.failed | tostring)
677
+ ' "$INSTALL_LOG" 2>/dev/null
678
+ else
679
+ echo "Last Auto-Install: none"
680
+ fi
681
+ }
682
+
683
+ # ============================================================================
684
+ # CLI 인터페이스
685
+ # ============================================================================
686
+
687
+ case "$1" in
688
+ fetch)
689
+ fetch_catalog
690
+ ;;
691
+ auto)
692
+ auto_install_official
693
+ ;;
694
+ recommend)
695
+ recommend "${2:-5}"
696
+ ;;
697
+ search)
698
+ search "$2"
699
+ ;;
700
+ combos)
701
+ suggest_combos
702
+ ;;
703
+ install)
704
+ install_skill "$2"
705
+ ;;
706
+ status)
707
+ show_status
708
+ ;;
709
+ check-npx)
710
+ check_npx
711
+ ;;
712
+ installed)
713
+ get_installed_skills
714
+ ;;
715
+ *)
716
+ echo "Usage: $0 {fetch|auto|recommend|search|combos|install|status|check-npx|installed}"
717
+ echo ""
718
+ echo "Commands:"
719
+ echo " fetch - Fetch skills catalog from skills.sh API"
720
+ echo " auto - Auto-install Anthropic official skills"
721
+ echo " recommend [count] - Get top N recommended skills (default: 5)"
722
+ echo " search <query> - Search skills by keyword"
723
+ echo " combos - Suggest synergy skills based on installed plugins"
724
+ echo " install <skill-id> - Install a skill via npx"
725
+ echo " status - Show skill scout status"
726
+ echo " check-npx - Verify npx availability"
727
+ echo " installed - List installed skills"
728
+ ;;
729
+ esac