skill-seekers 2.7.3__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.
- skill_seekers/__init__.py +22 -0
- skill_seekers/cli/__init__.py +39 -0
- skill_seekers/cli/adaptors/__init__.py +120 -0
- skill_seekers/cli/adaptors/base.py +221 -0
- skill_seekers/cli/adaptors/claude.py +485 -0
- skill_seekers/cli/adaptors/gemini.py +453 -0
- skill_seekers/cli/adaptors/markdown.py +269 -0
- skill_seekers/cli/adaptors/openai.py +503 -0
- skill_seekers/cli/ai_enhancer.py +310 -0
- skill_seekers/cli/api_reference_builder.py +373 -0
- skill_seekers/cli/architectural_pattern_detector.py +525 -0
- skill_seekers/cli/code_analyzer.py +1462 -0
- skill_seekers/cli/codebase_scraper.py +1225 -0
- skill_seekers/cli/config_command.py +563 -0
- skill_seekers/cli/config_enhancer.py +431 -0
- skill_seekers/cli/config_extractor.py +871 -0
- skill_seekers/cli/config_manager.py +452 -0
- skill_seekers/cli/config_validator.py +394 -0
- skill_seekers/cli/conflict_detector.py +528 -0
- skill_seekers/cli/constants.py +72 -0
- skill_seekers/cli/dependency_analyzer.py +757 -0
- skill_seekers/cli/doc_scraper.py +2332 -0
- skill_seekers/cli/enhance_skill.py +488 -0
- skill_seekers/cli/enhance_skill_local.py +1096 -0
- skill_seekers/cli/enhance_status.py +194 -0
- skill_seekers/cli/estimate_pages.py +433 -0
- skill_seekers/cli/generate_router.py +1209 -0
- skill_seekers/cli/github_fetcher.py +534 -0
- skill_seekers/cli/github_scraper.py +1466 -0
- skill_seekers/cli/guide_enhancer.py +723 -0
- skill_seekers/cli/how_to_guide_builder.py +1267 -0
- skill_seekers/cli/install_agent.py +461 -0
- skill_seekers/cli/install_skill.py +178 -0
- skill_seekers/cli/language_detector.py +614 -0
- skill_seekers/cli/llms_txt_detector.py +60 -0
- skill_seekers/cli/llms_txt_downloader.py +104 -0
- skill_seekers/cli/llms_txt_parser.py +150 -0
- skill_seekers/cli/main.py +558 -0
- skill_seekers/cli/markdown_cleaner.py +132 -0
- skill_seekers/cli/merge_sources.py +806 -0
- skill_seekers/cli/package_multi.py +77 -0
- skill_seekers/cli/package_skill.py +241 -0
- skill_seekers/cli/pattern_recognizer.py +1825 -0
- skill_seekers/cli/pdf_extractor_poc.py +1166 -0
- skill_seekers/cli/pdf_scraper.py +617 -0
- skill_seekers/cli/quality_checker.py +519 -0
- skill_seekers/cli/rate_limit_handler.py +438 -0
- skill_seekers/cli/resume_command.py +160 -0
- skill_seekers/cli/run_tests.py +230 -0
- skill_seekers/cli/setup_wizard.py +93 -0
- skill_seekers/cli/split_config.py +390 -0
- skill_seekers/cli/swift_patterns.py +560 -0
- skill_seekers/cli/test_example_extractor.py +1081 -0
- skill_seekers/cli/test_unified_simple.py +179 -0
- skill_seekers/cli/unified_codebase_analyzer.py +572 -0
- skill_seekers/cli/unified_scraper.py +932 -0
- skill_seekers/cli/unified_skill_builder.py +1605 -0
- skill_seekers/cli/upload_skill.py +162 -0
- skill_seekers/cli/utils.py +432 -0
- skill_seekers/mcp/__init__.py +33 -0
- skill_seekers/mcp/agent_detector.py +316 -0
- skill_seekers/mcp/git_repo.py +273 -0
- skill_seekers/mcp/server.py +231 -0
- skill_seekers/mcp/server_fastmcp.py +1249 -0
- skill_seekers/mcp/server_legacy.py +2302 -0
- skill_seekers/mcp/source_manager.py +285 -0
- skill_seekers/mcp/tools/__init__.py +115 -0
- skill_seekers/mcp/tools/config_tools.py +251 -0
- skill_seekers/mcp/tools/packaging_tools.py +826 -0
- skill_seekers/mcp/tools/scraping_tools.py +842 -0
- skill_seekers/mcp/tools/source_tools.py +828 -0
- skill_seekers/mcp/tools/splitting_tools.py +212 -0
- skill_seekers/py.typed +0 -0
- skill_seekers-2.7.3.dist-info/METADATA +2027 -0
- skill_seekers-2.7.3.dist-info/RECORD +79 -0
- skill_seekers-2.7.3.dist-info/WHEEL +5 -0
- skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
- skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
- skill_seekers-2.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Router Skill Generator with GitHub Integration (Phase 4)
|
|
4
|
+
|
|
5
|
+
Creates a router/hub skill that intelligently directs queries to specialized sub-skills.
|
|
6
|
+
Integrates GitHub insights (issues, metadata) for enhanced topic detection and routing.
|
|
7
|
+
|
|
8
|
+
Phase 4 enhancements:
|
|
9
|
+
- Enhanced topic definition using GitHub issue labels
|
|
10
|
+
- Router template with repository stats and top issues
|
|
11
|
+
- Sub-skill templates with "Common Issues" section
|
|
12
|
+
- GitHub issue links for context
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
# Import three-stream data classes (Phase 1)
|
|
22
|
+
try:
|
|
23
|
+
from .github_fetcher import DocsStream, InsightsStream, ThreeStreamData
|
|
24
|
+
from .markdown_cleaner import MarkdownCleaner
|
|
25
|
+
from .merge_sources import categorize_issues_by_topic
|
|
26
|
+
except ImportError:
|
|
27
|
+
# Fallback if github_fetcher not available
|
|
28
|
+
ThreeStreamData = None
|
|
29
|
+
DocsStream = None
|
|
30
|
+
InsightsStream = None
|
|
31
|
+
categorize_issues_by_topic = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RouterGenerator:
|
|
35
|
+
"""Generates router skills that direct to specialized sub-skills with GitHub integration"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
config_paths: list[str],
|
|
40
|
+
router_name: str = None,
|
|
41
|
+
github_streams: Optional["ThreeStreamData"] = None,
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize router generator with optional GitHub streams.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
config_paths: Paths to sub-skill config files
|
|
48
|
+
router_name: Optional router skill name
|
|
49
|
+
github_streams: Optional ThreeStreamData with docs and insights
|
|
50
|
+
"""
|
|
51
|
+
self.config_paths = [Path(p) for p in config_paths]
|
|
52
|
+
self.configs = [self.load_config(p) for p in self.config_paths]
|
|
53
|
+
self.router_name = router_name or self.infer_router_name()
|
|
54
|
+
self.base_config = self.configs[0] # Use first as template
|
|
55
|
+
self.github_streams = github_streams
|
|
56
|
+
|
|
57
|
+
# Extract GitHub data if available
|
|
58
|
+
self.github_metadata = None
|
|
59
|
+
self.github_docs = None
|
|
60
|
+
self.github_issues = None
|
|
61
|
+
|
|
62
|
+
if github_streams and github_streams.insights_stream:
|
|
63
|
+
self.github_metadata = github_streams.insights_stream.metadata
|
|
64
|
+
self.github_issues = {
|
|
65
|
+
"common_problems": github_streams.insights_stream.common_problems,
|
|
66
|
+
"known_solutions": github_streams.insights_stream.known_solutions,
|
|
67
|
+
"top_labels": github_streams.insights_stream.top_labels,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if github_streams and github_streams.docs_stream:
|
|
71
|
+
self.github_docs = {
|
|
72
|
+
"readme": github_streams.docs_stream.readme,
|
|
73
|
+
"contributing": github_streams.docs_stream.contributing,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def load_config(self, path: Path) -> dict[str, Any]:
|
|
77
|
+
"""Load a config file"""
|
|
78
|
+
try:
|
|
79
|
+
with open(path) as f:
|
|
80
|
+
return json.load(f)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"❌ Error loading {path}: {e}")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
def infer_router_name(self) -> str:
|
|
86
|
+
"""Infer router name from sub-skill names"""
|
|
87
|
+
# Find common prefix
|
|
88
|
+
names = [cfg["name"] for cfg in self.configs]
|
|
89
|
+
if not names:
|
|
90
|
+
return "router"
|
|
91
|
+
|
|
92
|
+
# Get common prefix before first dash
|
|
93
|
+
first_name = names[0]
|
|
94
|
+
if "-" in first_name:
|
|
95
|
+
return first_name.split("-")[0]
|
|
96
|
+
return first_name
|
|
97
|
+
|
|
98
|
+
def extract_routing_keywords(self) -> dict[str, list[str]]:
|
|
99
|
+
"""
|
|
100
|
+
Extract keywords for routing to each skill (Phase 4 enhanced).
|
|
101
|
+
|
|
102
|
+
Enhancement: Weight GitHub issue labels 2x in topic scoring.
|
|
103
|
+
Uses C3.x patterns, examples, and GitHub insights for better routing.
|
|
104
|
+
"""
|
|
105
|
+
routing = {}
|
|
106
|
+
|
|
107
|
+
for config in self.configs:
|
|
108
|
+
name = config["name"]
|
|
109
|
+
keywords = []
|
|
110
|
+
|
|
111
|
+
# Extract from categories (base weight: 1x)
|
|
112
|
+
if "categories" in config:
|
|
113
|
+
keywords.extend(config["categories"].keys())
|
|
114
|
+
|
|
115
|
+
# Extract from name (part after dash)
|
|
116
|
+
if "-" in name:
|
|
117
|
+
skill_topic = name.split("-", 1)[1]
|
|
118
|
+
keywords.append(skill_topic)
|
|
119
|
+
|
|
120
|
+
# Phase 4: Add GitHub issue labels (weight 2x by including twice)
|
|
121
|
+
if self.github_issues:
|
|
122
|
+
# Get top labels related to this skill topic
|
|
123
|
+
top_labels = self.github_issues.get("top_labels", [])
|
|
124
|
+
skill_keywords = set(keywords)
|
|
125
|
+
|
|
126
|
+
for label_info in top_labels[:10]: # Top 10 labels
|
|
127
|
+
label = label_info["label"].lower()
|
|
128
|
+
|
|
129
|
+
# Check if label relates to any skill keyword
|
|
130
|
+
if any(
|
|
131
|
+
keyword.lower() in label or label in keyword.lower()
|
|
132
|
+
for keyword in skill_keywords
|
|
133
|
+
):
|
|
134
|
+
# Add twice for 2x weight
|
|
135
|
+
keywords.append(label)
|
|
136
|
+
keywords.append(label)
|
|
137
|
+
|
|
138
|
+
# NEW: Extract skill-specific labels from individual issues
|
|
139
|
+
skill_keywords_set = set(keywords)
|
|
140
|
+
skill_specific_labels = self._extract_skill_specific_labels(name, skill_keywords_set)
|
|
141
|
+
for label in skill_specific_labels:
|
|
142
|
+
keywords.append(label)
|
|
143
|
+
keywords.append(label) # 2x weight
|
|
144
|
+
|
|
145
|
+
routing[name] = keywords
|
|
146
|
+
|
|
147
|
+
return routing
|
|
148
|
+
|
|
149
|
+
def _extract_skill_specific_labels(self, _skill_name: str, skill_keywords: set) -> list[str]:
|
|
150
|
+
"""
|
|
151
|
+
Extract labels from GitHub issues that match this specific skill.
|
|
152
|
+
|
|
153
|
+
Scans all common_problems and known_solutions for issues whose labels
|
|
154
|
+
match the skill's keywords, then extracts ALL labels from those issues.
|
|
155
|
+
This provides richer, skill-specific routing keywords.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
skill_name: Name of the skill
|
|
159
|
+
skill_keywords: Set of keywords already associated with the skill
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of skill-specific labels (excluding generic ones)
|
|
163
|
+
"""
|
|
164
|
+
if not self.github_issues:
|
|
165
|
+
return []
|
|
166
|
+
|
|
167
|
+
common_problems = self.github_issues.get("common_problems", [])
|
|
168
|
+
known_solutions = self.github_issues.get("known_solutions", [])
|
|
169
|
+
all_issues = common_problems + known_solutions
|
|
170
|
+
|
|
171
|
+
matching_labels = set()
|
|
172
|
+
|
|
173
|
+
for issue in all_issues:
|
|
174
|
+
issue_labels = issue.get("labels", [])
|
|
175
|
+
issue_labels_lower = [label.lower() for label in issue_labels]
|
|
176
|
+
|
|
177
|
+
# Check if this issue relates to the skill
|
|
178
|
+
has_match = any(
|
|
179
|
+
keyword.lower() in label or label in keyword.lower()
|
|
180
|
+
for keyword in skill_keywords
|
|
181
|
+
for label in issue_labels_lower
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if has_match:
|
|
185
|
+
# Add ALL labels from this matching issue
|
|
186
|
+
for label in issue_labels_lower:
|
|
187
|
+
# Skip generic labels that don't add routing value
|
|
188
|
+
if label not in [
|
|
189
|
+
"bug",
|
|
190
|
+
"enhancement",
|
|
191
|
+
"question",
|
|
192
|
+
"help wanted",
|
|
193
|
+
"good first issue",
|
|
194
|
+
"documentation",
|
|
195
|
+
"duplicate",
|
|
196
|
+
]:
|
|
197
|
+
matching_labels.add(label)
|
|
198
|
+
|
|
199
|
+
return list(matching_labels)
|
|
200
|
+
|
|
201
|
+
def _generate_frontmatter(self, _routing_keywords: dict[str, list[str]]) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Generate YAML frontmatter compliant with agentskills.io spec.
|
|
204
|
+
|
|
205
|
+
Required fields:
|
|
206
|
+
- name: router name (1-64 chars, lowercase-hyphen)
|
|
207
|
+
- description: when to use (1-1024 chars, keyword-rich)
|
|
208
|
+
|
|
209
|
+
Optional fields:
|
|
210
|
+
- license: MIT (from config or default)
|
|
211
|
+
- compatibility: Python version, dependencies
|
|
212
|
+
"""
|
|
213
|
+
# Build comprehensive description from all sub-skills
|
|
214
|
+
all_topics = []
|
|
215
|
+
for config in self.configs:
|
|
216
|
+
desc = config.get("description", "")
|
|
217
|
+
# Extract key topics from description (simple extraction)
|
|
218
|
+
topics = [word.strip() for word in desc.split(",") if word.strip()]
|
|
219
|
+
all_topics.extend(topics[:2]) # Max 2 topics per skill
|
|
220
|
+
|
|
221
|
+
# Create keyword-rich description
|
|
222
|
+
unique_topics = list(dict.fromkeys(all_topics))[:7] # Top 7 unique topics
|
|
223
|
+
|
|
224
|
+
if unique_topics:
|
|
225
|
+
topics_str = ", ".join(unique_topics)
|
|
226
|
+
description = (
|
|
227
|
+
f"{self.router_name.title()} framework. Use when working with: {topics_str}"
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
description = (
|
|
231
|
+
f"Use when working with {self.router_name.title()} development and programming"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Truncate to 200 chars for performance (agentskills.io recommendation)
|
|
235
|
+
if len(description) > 200:
|
|
236
|
+
description = description[:197] + "..."
|
|
237
|
+
|
|
238
|
+
# Extract license and compatibility
|
|
239
|
+
license_info = "MIT"
|
|
240
|
+
compatibility = "See sub-skills for specific requirements"
|
|
241
|
+
|
|
242
|
+
# Try to get language-specific compatibility if GitHub metadata available
|
|
243
|
+
if self.github_metadata:
|
|
244
|
+
language = self.github_metadata.get("language", "")
|
|
245
|
+
compatibility_map = {
|
|
246
|
+
"Python": f"Python 3.10+, requires {self.router_name} package",
|
|
247
|
+
"JavaScript": f"Node.js 18+, requires {self.router_name} package",
|
|
248
|
+
"TypeScript": f"Node.js 18+, TypeScript 5+, requires {self.router_name} package",
|
|
249
|
+
"Go": f"Go 1.20+, requires {self.router_name} package",
|
|
250
|
+
"Rust": f"Rust 1.70+, requires {self.router_name} package",
|
|
251
|
+
"Java": f"Java 17+, requires {self.router_name} package",
|
|
252
|
+
}
|
|
253
|
+
if language in compatibility_map:
|
|
254
|
+
compatibility = compatibility_map[language]
|
|
255
|
+
|
|
256
|
+
# Try to extract license
|
|
257
|
+
if isinstance(self.github_metadata.get("license"), dict):
|
|
258
|
+
license_info = self.github_metadata["license"].get("name", "MIT")
|
|
259
|
+
|
|
260
|
+
frontmatter = f"""---
|
|
261
|
+
name: {self.router_name}
|
|
262
|
+
description: {description}
|
|
263
|
+
license: {license_info}
|
|
264
|
+
compatibility: {compatibility}
|
|
265
|
+
---"""
|
|
266
|
+
|
|
267
|
+
return frontmatter
|
|
268
|
+
|
|
269
|
+
def _extract_clean_readme_section(self, readme: str) -> str:
|
|
270
|
+
"""
|
|
271
|
+
Extract and clean README quick start section.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
readme: Full README content
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Cleaned quick start section (HTML removed, properly truncated)
|
|
278
|
+
"""
|
|
279
|
+
cleaner = MarkdownCleaner()
|
|
280
|
+
|
|
281
|
+
# Extract first meaningful section (1500 chars soft limit - extends for complete code blocks)
|
|
282
|
+
quick_start = cleaner.extract_first_section(readme, max_chars=1500)
|
|
283
|
+
|
|
284
|
+
# Additional validation
|
|
285
|
+
if len(quick_start) < 50: # Too short, probably just title
|
|
286
|
+
# Try to get more content
|
|
287
|
+
quick_start = cleaner.extract_first_section(readme, max_chars=2000)
|
|
288
|
+
|
|
289
|
+
return quick_start
|
|
290
|
+
|
|
291
|
+
def _extract_topic_from_skill(self, skill_name: str) -> str:
|
|
292
|
+
"""
|
|
293
|
+
Extract readable topic from skill name.
|
|
294
|
+
|
|
295
|
+
Examples:
|
|
296
|
+
- "fastmcp-oauth" -> "OAuth authentication"
|
|
297
|
+
- "react-hooks" -> "React hooks"
|
|
298
|
+
- "django-orm" -> "Django ORM"
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
skill_name: Skill name (e.g., "fastmcp-oauth")
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Readable topic string
|
|
305
|
+
"""
|
|
306
|
+
# Remove router name prefix
|
|
307
|
+
if skill_name.startswith(f"{self.router_name}-"):
|
|
308
|
+
topic = skill_name[len(self.router_name) + 1 :]
|
|
309
|
+
else:
|
|
310
|
+
topic = skill_name
|
|
311
|
+
|
|
312
|
+
# Capitalize and add context
|
|
313
|
+
topic = topic.replace("-", " ").title()
|
|
314
|
+
|
|
315
|
+
# Add common suffixes for context
|
|
316
|
+
topic_map = {
|
|
317
|
+
"oauth": "OAuth authentication",
|
|
318
|
+
"auth": "authentication",
|
|
319
|
+
"async": "async patterns",
|
|
320
|
+
"api": "API integration",
|
|
321
|
+
"orm": "ORM queries",
|
|
322
|
+
"hooks": "hooks",
|
|
323
|
+
"routing": "routing",
|
|
324
|
+
"testing": "testing",
|
|
325
|
+
"2d": "2D development",
|
|
326
|
+
"3d": "3D development",
|
|
327
|
+
"scripting": "scripting",
|
|
328
|
+
"physics": "physics",
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
topic_lower = topic.lower()
|
|
332
|
+
for key, value in topic_map.items():
|
|
333
|
+
if key in topic_lower:
|
|
334
|
+
return value
|
|
335
|
+
|
|
336
|
+
return topic
|
|
337
|
+
|
|
338
|
+
def _generate_dynamic_examples(self, routing_keywords: dict[str, list[str]]) -> str:
|
|
339
|
+
"""
|
|
340
|
+
Generate examples dynamically from actual sub-skill names and keywords.
|
|
341
|
+
|
|
342
|
+
Creates 2-3 realistic examples showing:
|
|
343
|
+
1. Single skill activation
|
|
344
|
+
2. Different skill activation
|
|
345
|
+
3. Complex query routing (if 2+ skills)
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
routing_keywords: Dictionary mapping skill names to keywords
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Formatted examples section
|
|
352
|
+
"""
|
|
353
|
+
examples = []
|
|
354
|
+
|
|
355
|
+
# Get list of sub-skills
|
|
356
|
+
skill_names = list(routing_keywords.keys())
|
|
357
|
+
|
|
358
|
+
if len(skill_names) == 0:
|
|
359
|
+
return ""
|
|
360
|
+
|
|
361
|
+
# Example 1: Single skill activation (first sub-skill)
|
|
362
|
+
if len(skill_names) >= 1:
|
|
363
|
+
first_skill = skill_names[0]
|
|
364
|
+
first_keywords = routing_keywords[first_skill][:2] # Top 2 keywords
|
|
365
|
+
|
|
366
|
+
# Extract topic from skill name
|
|
367
|
+
topic = self._extract_topic_from_skill(first_skill)
|
|
368
|
+
keyword = first_keywords[0] if first_keywords else topic
|
|
369
|
+
|
|
370
|
+
examples.append(
|
|
371
|
+
f'**Q:** "How do I implement {keyword}?"\n**A:** Activates {first_skill} skill'
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Example 2: Different skill (second sub-skill if available)
|
|
375
|
+
if len(skill_names) >= 2:
|
|
376
|
+
second_skill = skill_names[1]
|
|
377
|
+
second_keywords = routing_keywords[second_skill][:2]
|
|
378
|
+
|
|
379
|
+
topic = self._extract_topic_from_skill(second_skill)
|
|
380
|
+
keyword = second_keywords[0] if second_keywords else topic
|
|
381
|
+
|
|
382
|
+
examples.append(
|
|
383
|
+
f'**Q:** "Working with {keyword} in {self.router_name.title()}"\n**A:** Activates {second_skill} skill'
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Example 3: Multi-skill activation (if 2+ skills)
|
|
387
|
+
if len(skill_names) >= 2:
|
|
388
|
+
skill_1 = skill_names[0]
|
|
389
|
+
skill_2 = skill_names[1]
|
|
390
|
+
|
|
391
|
+
topic_1 = self._extract_topic_from_skill(skill_1)
|
|
392
|
+
topic_2 = self._extract_topic_from_skill(skill_2)
|
|
393
|
+
|
|
394
|
+
examples.append(
|
|
395
|
+
f'**Q:** "Combining {topic_1} with {topic_2}"\n**A:** Activates {skill_1} + {skill_2} skills'
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return "\n\n".join(examples)
|
|
399
|
+
|
|
400
|
+
def _generate_examples_from_github(self, routing_keywords: dict[str, list[str]]) -> str:
|
|
401
|
+
"""
|
|
402
|
+
Generate examples from real GitHub issue titles.
|
|
403
|
+
|
|
404
|
+
Uses actual user questions from GitHub issues to create realistic examples.
|
|
405
|
+
Matches issues to skills based on labels for relevance.
|
|
406
|
+
Fallback to keyword-based examples if no GitHub data available.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
routing_keywords: Dictionary mapping skill names to keywords
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Formatted examples section with real user questions
|
|
413
|
+
"""
|
|
414
|
+
if not self.github_issues:
|
|
415
|
+
return self._generate_dynamic_examples(routing_keywords)
|
|
416
|
+
|
|
417
|
+
examples = []
|
|
418
|
+
common_problems = self.github_issues.get("common_problems", [])
|
|
419
|
+
|
|
420
|
+
if not common_problems:
|
|
421
|
+
return self._generate_dynamic_examples(routing_keywords)
|
|
422
|
+
|
|
423
|
+
# Match issues to skills based on labels (generate up to 3 examples)
|
|
424
|
+
for skill_name, keywords in list(routing_keywords.items())[:3]:
|
|
425
|
+
skill_keywords_lower = [k.lower() for k in keywords]
|
|
426
|
+
matched_issue = None
|
|
427
|
+
|
|
428
|
+
# Find first issue matching this skill's keywords
|
|
429
|
+
for issue in common_problems:
|
|
430
|
+
issue_labels = [label.lower() for label in issue.get("labels", [])]
|
|
431
|
+
if any(label in skill_keywords_lower for label in issue_labels):
|
|
432
|
+
matched_issue = issue
|
|
433
|
+
common_problems.remove(issue) # Don't reuse same issue
|
|
434
|
+
break
|
|
435
|
+
|
|
436
|
+
if matched_issue:
|
|
437
|
+
title = matched_issue.get("title", "")
|
|
438
|
+
question = self._convert_issue_to_question(title)
|
|
439
|
+
examples.append(f'**Q:** "{question}"\n**A:** Activates {skill_name} skill')
|
|
440
|
+
else:
|
|
441
|
+
# Fallback to keyword-based example for this skill
|
|
442
|
+
topic = self._extract_topic_from_skill(skill_name)
|
|
443
|
+
keyword = keywords[0] if keywords else topic
|
|
444
|
+
examples.append(
|
|
445
|
+
f'**Q:** "Working with {keyword} in {self.router_name.title()}"\n'
|
|
446
|
+
f"**A:** Activates {skill_name} skill"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
"\n\n".join(examples) if examples else self._generate_dynamic_examples(routing_keywords)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def _convert_issue_to_question(self, issue_title: str) -> str:
|
|
454
|
+
"""
|
|
455
|
+
Convert GitHub issue title to natural question format.
|
|
456
|
+
|
|
457
|
+
Examples:
|
|
458
|
+
- "OAuth fails on redirect" → "How do I fix OAuth redirect failures?"
|
|
459
|
+
- "ApiKey Header documentation" → "How do I use ApiKey Header?"
|
|
460
|
+
- "Add WebSocket support" → "How do I handle WebSocket support?"
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
issue_title: Raw GitHub issue title
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Natural question format suitable for examples
|
|
467
|
+
"""
|
|
468
|
+
title_lower = issue_title.lower()
|
|
469
|
+
|
|
470
|
+
# Pattern 1: Error/Failure issues
|
|
471
|
+
if "fail" in title_lower or "error" in title_lower or "issue" in title_lower:
|
|
472
|
+
cleaned = issue_title.replace(" fails", "").replace(" errors", "").replace(" issue", "")
|
|
473
|
+
return f"How do I fix {cleaned.lower()}?"
|
|
474
|
+
|
|
475
|
+
# Pattern 2: Documentation requests
|
|
476
|
+
if "documentation" in title_lower or "docs" in title_lower:
|
|
477
|
+
cleaned = issue_title.replace(" documentation", "").replace(" docs", "")
|
|
478
|
+
return f"How do I use {cleaned.lower()}?"
|
|
479
|
+
|
|
480
|
+
# Pattern 3: Feature requests
|
|
481
|
+
if title_lower.startswith("add ") or title_lower.startswith("added "):
|
|
482
|
+
feature = issue_title.replace("Add ", "").replace("Added ", "")
|
|
483
|
+
return f"How do I implement {feature.lower()}?"
|
|
484
|
+
|
|
485
|
+
# Default: Generic question
|
|
486
|
+
return f"How do I handle {issue_title.lower()}?"
|
|
487
|
+
|
|
488
|
+
def _extract_common_patterns(self) -> list[dict[str, str]]:
|
|
489
|
+
"""
|
|
490
|
+
Extract problem-solution patterns from closed GitHub issues.
|
|
491
|
+
|
|
492
|
+
Analyzes closed issues (known_solutions) to identify common patterns
|
|
493
|
+
that users encountered and resolved. These patterns are shown in the
|
|
494
|
+
Common Patterns section of the router skill.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
List of pattern dicts with 'problem', 'solution', 'issue_number'
|
|
498
|
+
"""
|
|
499
|
+
if not self.github_issues:
|
|
500
|
+
return []
|
|
501
|
+
|
|
502
|
+
known_solutions = self.github_issues.get("known_solutions", [])
|
|
503
|
+
if not known_solutions:
|
|
504
|
+
return []
|
|
505
|
+
|
|
506
|
+
patterns = []
|
|
507
|
+
|
|
508
|
+
# Top 5 closed issues with most engagement (comments indicate usefulness)
|
|
509
|
+
top_solutions = sorted(known_solutions, key=lambda x: x.get("comments", 0), reverse=True)[
|
|
510
|
+
:5
|
|
511
|
+
]
|
|
512
|
+
|
|
513
|
+
for issue in top_solutions:
|
|
514
|
+
title = issue.get("title", "")
|
|
515
|
+
number = issue.get("number", 0)
|
|
516
|
+
problem, solution = self._parse_issue_pattern(title)
|
|
517
|
+
|
|
518
|
+
patterns.append({"problem": problem, "solution": solution, "issue_number": number})
|
|
519
|
+
|
|
520
|
+
return patterns
|
|
521
|
+
|
|
522
|
+
def _parse_issue_pattern(self, issue_title: str) -> tuple:
|
|
523
|
+
"""
|
|
524
|
+
Parse issue title to extract problem-solution pattern.
|
|
525
|
+
|
|
526
|
+
Analyzes the structure of closed issue titles to infer the problem
|
|
527
|
+
and solution pattern. Common patterns include fixes, additions, and resolutions.
|
|
528
|
+
|
|
529
|
+
Examples:
|
|
530
|
+
- "Fixed OAuth redirect" → ("OAuth redirect not working", "See fix implementation")
|
|
531
|
+
- "Added API key support" → ("Missing API key support", "Use API key support feature")
|
|
532
|
+
- "Resolved timeout errors" → ("Timeout errors issue", "See resolution approach")
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
issue_title: Title of closed GitHub issue
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Tuple of (problem_description, solution_hint)
|
|
539
|
+
"""
|
|
540
|
+
title_lower = issue_title.lower()
|
|
541
|
+
|
|
542
|
+
# Pattern 1: "Fixed X" → "X not working" / "See fix"
|
|
543
|
+
if title_lower.startswith("fixed ") or title_lower.startswith("fix "):
|
|
544
|
+
problem_text = issue_title.replace("Fixed ", "").replace("Fix ", "")
|
|
545
|
+
return (f"{problem_text} not working", "See fix implementation details")
|
|
546
|
+
|
|
547
|
+
# Pattern 2: "Resolved X" → "X issue" / "See resolution"
|
|
548
|
+
if title_lower.startswith("resolved ") or title_lower.startswith("resolve "):
|
|
549
|
+
problem_text = issue_title.replace("Resolved ", "").replace("Resolve ", "")
|
|
550
|
+
return (f"{problem_text} issue", "See resolution approach")
|
|
551
|
+
|
|
552
|
+
# Pattern 3: "Added X" → "Missing X" / "Use X"
|
|
553
|
+
if title_lower.startswith("added ") or title_lower.startswith("add "):
|
|
554
|
+
feature_text = issue_title.replace("Added ", "").replace("Add ", "")
|
|
555
|
+
return (f"Missing {feature_text}", f"Use {feature_text} feature")
|
|
556
|
+
|
|
557
|
+
# Default: Use title as-is
|
|
558
|
+
return (issue_title, "See issue for solution details")
|
|
559
|
+
|
|
560
|
+
def _detect_framework(self) -> str | None:
|
|
561
|
+
"""
|
|
562
|
+
Detect framework from router name and GitHub metadata.
|
|
563
|
+
|
|
564
|
+
Identifies common frameworks (fastapi, django, react, etc.) from
|
|
565
|
+
router name or repository description. Used to provide framework-specific
|
|
566
|
+
hello world templates when README lacks code examples.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
Framework identifier (e.g., 'fastapi', 'django') or None if unknown
|
|
570
|
+
"""
|
|
571
|
+
router_lower = self.router_name.lower()
|
|
572
|
+
|
|
573
|
+
framework_keywords = {
|
|
574
|
+
"fastapi": "fastapi",
|
|
575
|
+
"django": "django",
|
|
576
|
+
"flask": "flask",
|
|
577
|
+
"react": "react",
|
|
578
|
+
"vue": "vue",
|
|
579
|
+
"express": "express",
|
|
580
|
+
"fastmcp": "fastmcp",
|
|
581
|
+
"mcp": "fastmcp",
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Check router name first
|
|
585
|
+
for keyword, framework in framework_keywords.items():
|
|
586
|
+
if keyword in router_lower:
|
|
587
|
+
return framework
|
|
588
|
+
|
|
589
|
+
# Check GitHub description if available
|
|
590
|
+
if self.github_metadata:
|
|
591
|
+
description = self.github_metadata.get("description", "").lower()
|
|
592
|
+
for keyword, framework in framework_keywords.items():
|
|
593
|
+
if keyword in description:
|
|
594
|
+
return framework
|
|
595
|
+
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
def _get_framework_hello_world(self, framework: str) -> str:
|
|
599
|
+
"""
|
|
600
|
+
Get framework-specific hello world template.
|
|
601
|
+
|
|
602
|
+
Provides basic installation + hello world code for common frameworks.
|
|
603
|
+
Used as fallback when README doesn't contain code examples.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
framework: Framework identifier (e.g., 'fastapi', 'react')
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Formatted Quick Start section with install + hello world code
|
|
610
|
+
"""
|
|
611
|
+
templates = {
|
|
612
|
+
"fastapi": """## Quick Start
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
pip install fastapi uvicorn
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
```python
|
|
619
|
+
from fastapi import FastAPI
|
|
620
|
+
|
|
621
|
+
app = FastAPI()
|
|
622
|
+
|
|
623
|
+
@app.get("/")
|
|
624
|
+
def read_root():
|
|
625
|
+
return {"Hello": "World"}
|
|
626
|
+
|
|
627
|
+
# Run: uvicorn main:app --reload
|
|
628
|
+
```
|
|
629
|
+
""",
|
|
630
|
+
"fastmcp": """## Quick Start
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
pip install fastmcp
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
```python
|
|
637
|
+
from fastmcp import FastMCP
|
|
638
|
+
|
|
639
|
+
mcp = FastMCP("My Server")
|
|
640
|
+
|
|
641
|
+
@mcp.tool()
|
|
642
|
+
def greet(name: str) -> str:
|
|
643
|
+
return f"Hello, {name}!"
|
|
644
|
+
```
|
|
645
|
+
""",
|
|
646
|
+
"django": """## Quick Start
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
pip install django
|
|
650
|
+
django-admin startproject mysite
|
|
651
|
+
cd mysite
|
|
652
|
+
python manage.py runserver
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Visit http://127.0.0.1:8000/ to see your Django app.
|
|
656
|
+
""",
|
|
657
|
+
"react": """## Quick Start
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
npx create-react-app my-app
|
|
661
|
+
cd my-app
|
|
662
|
+
npm start
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
```jsx
|
|
666
|
+
function App() {
|
|
667
|
+
return <h1>Hello World</h1>;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export default App;
|
|
671
|
+
```
|
|
672
|
+
""",
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return templates.get(framework, "")
|
|
676
|
+
|
|
677
|
+
def _generate_comprehensive_description(self) -> str:
|
|
678
|
+
"""
|
|
679
|
+
Generate router description that covers all sub-skill topics.
|
|
680
|
+
|
|
681
|
+
Extracts key topics from all sub-skill descriptions and combines them
|
|
682
|
+
into a comprehensive "Use when working with:" list.
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
Comprehensive description string
|
|
686
|
+
"""
|
|
687
|
+
all_topics = []
|
|
688
|
+
|
|
689
|
+
for config in self.configs:
|
|
690
|
+
desc = config.get("description", "")
|
|
691
|
+
# Extract key topics from description (simple comma-separated extraction)
|
|
692
|
+
topics = [topic.strip() for topic in desc.split(",") if topic.strip()]
|
|
693
|
+
all_topics.extend(topics[:2]) # Max 2 topics per skill
|
|
694
|
+
|
|
695
|
+
# Deduplicate and take top 5-7 topics
|
|
696
|
+
unique_topics = list(dict.fromkeys(all_topics))[:7]
|
|
697
|
+
|
|
698
|
+
if not unique_topics:
|
|
699
|
+
return f"Use when working with {self.router_name} development and programming"
|
|
700
|
+
|
|
701
|
+
# Format as user-friendly bulleted list
|
|
702
|
+
description = f"""Use this skill when working with:
|
|
703
|
+
- {self.router_name.title()} framework (general questions)
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
for topic in unique_topics:
|
|
707
|
+
# Clean up topic text (remove "when working with" prefixes if present)
|
|
708
|
+
topic = topic.replace("when working with", "").strip()
|
|
709
|
+
topic = topic.replace("Use when", "").strip()
|
|
710
|
+
if topic:
|
|
711
|
+
description += f"- {topic}\n"
|
|
712
|
+
|
|
713
|
+
# Add comprehensive footer items
|
|
714
|
+
description += f"- {self.router_name.upper()} protocol implementation\n"
|
|
715
|
+
description += f"- {self.router_name.title()} configuration and setup"
|
|
716
|
+
|
|
717
|
+
return description
|
|
718
|
+
|
|
719
|
+
def generate_skill_md(self) -> str:
|
|
720
|
+
"""
|
|
721
|
+
Generate router SKILL.md content (Phase 4 enhanced).
|
|
722
|
+
|
|
723
|
+
Enhancement: Include repository stats, README quick start, and top 5 GitHub issues.
|
|
724
|
+
With YAML frontmatter for agentskills.io compliance.
|
|
725
|
+
"""
|
|
726
|
+
routing_keywords = self.extract_routing_keywords()
|
|
727
|
+
|
|
728
|
+
# NEW: Generate YAML frontmatter
|
|
729
|
+
frontmatter = self._generate_frontmatter(routing_keywords)
|
|
730
|
+
|
|
731
|
+
# NEW: Generate comprehensive description from all sub-skills
|
|
732
|
+
when_to_use = self._generate_comprehensive_description()
|
|
733
|
+
|
|
734
|
+
skill_md = (
|
|
735
|
+
frontmatter
|
|
736
|
+
+ "\n\n"
|
|
737
|
+
+ f"""# {self.router_name.replace("-", " ").title()} Documentation
|
|
738
|
+
|
|
739
|
+
## When to Use This Skill
|
|
740
|
+
|
|
741
|
+
{when_to_use}
|
|
742
|
+
|
|
743
|
+
This is a router skill that directs your questions to specialized sub-skills for efficient, focused assistance.
|
|
744
|
+
|
|
745
|
+
"""
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Phase 4: Add GitHub repository metadata
|
|
749
|
+
if self.github_metadata:
|
|
750
|
+
# NEW: Use html_url from GitHub metadata instead of base_url from config
|
|
751
|
+
repo_url = self.github_metadata.get("html_url", "")
|
|
752
|
+
stars = self.github_metadata.get("stars", 0)
|
|
753
|
+
language = self.github_metadata.get("language", "Unknown")
|
|
754
|
+
description = self.github_metadata.get("description", "")
|
|
755
|
+
|
|
756
|
+
skill_md += f"""## Repository Info
|
|
757
|
+
|
|
758
|
+
**Repository:** {repo_url}
|
|
759
|
+
**Stars:** ⭐ {stars:,} | **Language:** {language}
|
|
760
|
+
{f"**Description:** {description}" if description else ""}
|
|
761
|
+
|
|
762
|
+
"""
|
|
763
|
+
|
|
764
|
+
# Phase 4: Add Quick Start from README
|
|
765
|
+
if self.github_docs and self.github_docs.get("readme"):
|
|
766
|
+
readme = self.github_docs["readme"]
|
|
767
|
+
|
|
768
|
+
# NEW: Clean HTML and extract meaningful content
|
|
769
|
+
quick_start = self._extract_clean_readme_section(readme)
|
|
770
|
+
|
|
771
|
+
if quick_start:
|
|
772
|
+
skill_md += f"""## Quick Start
|
|
773
|
+
|
|
774
|
+
{quick_start}
|
|
775
|
+
|
|
776
|
+
*For detailed setup, see references/getting_started.md*
|
|
777
|
+
|
|
778
|
+
"""
|
|
779
|
+
else:
|
|
780
|
+
# NEW: Fallback to framework-specific hello world (Phase 2, Fix 5)
|
|
781
|
+
framework = self._detect_framework()
|
|
782
|
+
if framework:
|
|
783
|
+
hello_world = self._get_framework_hello_world(framework)
|
|
784
|
+
if hello_world:
|
|
785
|
+
skill_md += (
|
|
786
|
+
hello_world
|
|
787
|
+
+ "\n*Note: Generic template. See references/getting_started.md for project-specific setup.*\n\n"
|
|
788
|
+
)
|
|
789
|
+
else:
|
|
790
|
+
# No README available - try framework fallback
|
|
791
|
+
framework = self._detect_framework()
|
|
792
|
+
if framework:
|
|
793
|
+
hello_world = self._get_framework_hello_world(framework)
|
|
794
|
+
if hello_world:
|
|
795
|
+
skill_md += (
|
|
796
|
+
hello_world
|
|
797
|
+
+ "\n*Note: Generic template. Check repository for specific installation instructions.*\n\n"
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
skill_md += """## How It Works
|
|
801
|
+
|
|
802
|
+
This skill analyzes your question and activates the appropriate specialized skill(s):
|
|
803
|
+
|
|
804
|
+
"""
|
|
805
|
+
|
|
806
|
+
# List sub-skills
|
|
807
|
+
for config in self.configs:
|
|
808
|
+
name = config["name"]
|
|
809
|
+
desc = config.get("description", "")
|
|
810
|
+
# Remove router name prefix from description if present
|
|
811
|
+
if desc.startswith(f"{self.router_name.title()} -"):
|
|
812
|
+
desc = desc.split(" - ", 1)[1]
|
|
813
|
+
|
|
814
|
+
skill_md += f"### {name}\n{desc}\n\n"
|
|
815
|
+
|
|
816
|
+
# Routing logic
|
|
817
|
+
skill_md += """## Routing Logic
|
|
818
|
+
|
|
819
|
+
The router analyzes your question for topic keywords and activates relevant skills:
|
|
820
|
+
|
|
821
|
+
**Keywords → Skills:**
|
|
822
|
+
"""
|
|
823
|
+
|
|
824
|
+
for skill_name, keywords in routing_keywords.items():
|
|
825
|
+
# NEW: Deduplicate keywords for display while preserving order
|
|
826
|
+
unique_keywords = list(dict.fromkeys(keywords)) # Preserves order, removes duplicates
|
|
827
|
+
keyword_str = ", ".join(unique_keywords)
|
|
828
|
+
skill_md += f"- {keyword_str} → **{skill_name}**\n"
|
|
829
|
+
|
|
830
|
+
# Quick reference
|
|
831
|
+
skill_md += """
|
|
832
|
+
|
|
833
|
+
## Quick Reference
|
|
834
|
+
|
|
835
|
+
For quick answers, this router provides basic overview information. For detailed documentation, the specialized skills contain comprehensive references.
|
|
836
|
+
|
|
837
|
+
### Getting Started
|
|
838
|
+
|
|
839
|
+
1. Ask your question naturally - mention the topic area
|
|
840
|
+
2. The router will activate the appropriate skill(s)
|
|
841
|
+
3. You'll receive focused, detailed answers from specialized documentation
|
|
842
|
+
|
|
843
|
+
### Examples
|
|
844
|
+
|
|
845
|
+
"""
|
|
846
|
+
|
|
847
|
+
# NEW: Generate examples from GitHub issues (with fallback to keyword-based)
|
|
848
|
+
dynamic_examples = self._generate_examples_from_github(routing_keywords)
|
|
849
|
+
if dynamic_examples:
|
|
850
|
+
skill_md += dynamic_examples + "\n\n"
|
|
851
|
+
|
|
852
|
+
skill_md += """### All Available Skills
|
|
853
|
+
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
# List all skills
|
|
857
|
+
for config in self.configs:
|
|
858
|
+
skill_md += f"- **{config['name']}**\n"
|
|
859
|
+
|
|
860
|
+
# Phase 4: Add Common Issues from GitHub (Summary with Reference)
|
|
861
|
+
if self.github_issues:
|
|
862
|
+
common_problems = self.github_issues.get("common_problems", [])[:5] # Top 5
|
|
863
|
+
|
|
864
|
+
if common_problems:
|
|
865
|
+
skill_md += """
|
|
866
|
+
|
|
867
|
+
## Common Issues
|
|
868
|
+
|
|
869
|
+
Top 5 GitHub issues from the community:
|
|
870
|
+
|
|
871
|
+
"""
|
|
872
|
+
for i, issue in enumerate(common_problems, 1):
|
|
873
|
+
title = issue.get("title", "")
|
|
874
|
+
number = issue.get("number", 0)
|
|
875
|
+
comments = issue.get("comments", 0)
|
|
876
|
+
|
|
877
|
+
skill_md += f"{i}. **{title}** (Issue #{number}, {comments} comments)\n"
|
|
878
|
+
|
|
879
|
+
skill_md += "\n*For details and solutions, see references/github_issues.md*\n"
|
|
880
|
+
|
|
881
|
+
# NEW: Add Common Patterns section (Phase 2, Fix 4)
|
|
882
|
+
if self.github_issues:
|
|
883
|
+
patterns = self._extract_common_patterns()
|
|
884
|
+
|
|
885
|
+
if patterns:
|
|
886
|
+
skill_md += """
|
|
887
|
+
|
|
888
|
+
## Common Patterns
|
|
889
|
+
|
|
890
|
+
Problem-solution patterns from resolved GitHub issues:
|
|
891
|
+
|
|
892
|
+
"""
|
|
893
|
+
for i, pattern in enumerate(patterns, 1):
|
|
894
|
+
problem = pattern["problem"]
|
|
895
|
+
solution = pattern["solution"]
|
|
896
|
+
issue_num = pattern["issue_number"]
|
|
897
|
+
|
|
898
|
+
skill_md += f"**Pattern {i}**: {problem}\n"
|
|
899
|
+
skill_md += f"→ **Solution**: {solution} ([Issue #{issue_num}](references/github_issues.md))\n\n"
|
|
900
|
+
|
|
901
|
+
# NEW: Add References section
|
|
902
|
+
skill_md += """
|
|
903
|
+
|
|
904
|
+
## References
|
|
905
|
+
|
|
906
|
+
Detailed documentation available in:
|
|
907
|
+
|
|
908
|
+
"""
|
|
909
|
+
if self.github_issues:
|
|
910
|
+
skill_md += "- `references/github_issues.md` - Community problems and solutions\n"
|
|
911
|
+
if self.github_docs and self.github_docs.get("readme"):
|
|
912
|
+
skill_md += "- `references/getting_started.md` - Detailed setup guide\n"
|
|
913
|
+
|
|
914
|
+
skill_md += """
|
|
915
|
+
|
|
916
|
+
## Need Help?
|
|
917
|
+
|
|
918
|
+
Simply ask your question and mention the topic. The router will find the right specialized skill for you!
|
|
919
|
+
|
|
920
|
+
---
|
|
921
|
+
|
|
922
|
+
*This is a router skill. For complete documentation, see the specialized skills listed above.*
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
return skill_md
|
|
926
|
+
|
|
927
|
+
def generate_subskill_issues_section(self, _skill_name: str, topics: list[str]) -> str:
|
|
928
|
+
"""
|
|
929
|
+
Generate "Common Issues" section for a sub-skill (Phase 4).
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
skill_name: Name of the sub-skill
|
|
933
|
+
topics: List of topic keywords for this skill
|
|
934
|
+
|
|
935
|
+
Returns:
|
|
936
|
+
Markdown section with relevant GitHub issues
|
|
937
|
+
"""
|
|
938
|
+
if not self.github_issues or not categorize_issues_by_topic:
|
|
939
|
+
return ""
|
|
940
|
+
|
|
941
|
+
common_problems = self.github_issues.get("common_problems", [])
|
|
942
|
+
known_solutions = self.github_issues.get("known_solutions", [])
|
|
943
|
+
|
|
944
|
+
# Categorize issues by topic
|
|
945
|
+
categorized = categorize_issues_by_topic(common_problems, known_solutions, topics)
|
|
946
|
+
|
|
947
|
+
# Build issues section
|
|
948
|
+
issues_md = """
|
|
949
|
+
|
|
950
|
+
## Common Issues (from GitHub)
|
|
951
|
+
|
|
952
|
+
GitHub issues related to this topic:
|
|
953
|
+
|
|
954
|
+
"""
|
|
955
|
+
|
|
956
|
+
has_issues = False
|
|
957
|
+
|
|
958
|
+
# Add categorized issues
|
|
959
|
+
for topic, issues in categorized.items():
|
|
960
|
+
if not issues:
|
|
961
|
+
continue
|
|
962
|
+
|
|
963
|
+
has_issues = True
|
|
964
|
+
issues_md += f"\n### {topic.title()}\n\n"
|
|
965
|
+
|
|
966
|
+
for issue in issues[:3]: # Top 3 per topic
|
|
967
|
+
title = issue.get("title", "")
|
|
968
|
+
number = issue.get("number", 0)
|
|
969
|
+
state = issue.get("state", "unknown")
|
|
970
|
+
comments = issue.get("comments", 0)
|
|
971
|
+
labels = issue.get("labels", [])
|
|
972
|
+
|
|
973
|
+
# Format issue
|
|
974
|
+
state_icon = "🔴" if state == "open" else "✅"
|
|
975
|
+
issues_md += f"**{state_icon} Issue #{number}: {title}**\n"
|
|
976
|
+
issues_md += f"- Status: {state.title()}\n"
|
|
977
|
+
issues_md += f"- {comments} comments\n"
|
|
978
|
+
if labels:
|
|
979
|
+
issues_md += f"- Labels: {', '.join(labels)}\n"
|
|
980
|
+
issues_md += "\n"
|
|
981
|
+
|
|
982
|
+
if not has_issues:
|
|
983
|
+
return "" # No relevant issues for this skill
|
|
984
|
+
|
|
985
|
+
return issues_md
|
|
986
|
+
|
|
987
|
+
def create_router_config(self) -> dict[str, Any]:
|
|
988
|
+
"""Create router configuration"""
|
|
989
|
+
routing_keywords = self.extract_routing_keywords()
|
|
990
|
+
|
|
991
|
+
router_config = {
|
|
992
|
+
"name": self.router_name,
|
|
993
|
+
"description": self.base_config.get(
|
|
994
|
+
"description",
|
|
995
|
+
f"Use when working with {self.router_name} documentation (router for multiple sub-skills)",
|
|
996
|
+
),
|
|
997
|
+
"base_url": self.base_config["base_url"],
|
|
998
|
+
"selectors": self.base_config.get("selectors", {}),
|
|
999
|
+
"url_patterns": self.base_config.get("url_patterns", {}),
|
|
1000
|
+
"rate_limit": self.base_config.get("rate_limit", 0.5),
|
|
1001
|
+
"max_pages": 500, # Router only scrapes overview pages
|
|
1002
|
+
"_router": True,
|
|
1003
|
+
"_sub_skills": [cfg["name"] for cfg in self.configs],
|
|
1004
|
+
"_routing_keywords": routing_keywords,
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return router_config
|
|
1008
|
+
|
|
1009
|
+
def _generate_github_issues_reference(self) -> str:
|
|
1010
|
+
"""
|
|
1011
|
+
Generate detailed GitHub issues reference file.
|
|
1012
|
+
|
|
1013
|
+
Returns:
|
|
1014
|
+
Markdown content for github_issues.md
|
|
1015
|
+
"""
|
|
1016
|
+
md = "# Common GitHub Issues\n\n"
|
|
1017
|
+
md += "Top issues reported by the community:\n\n"
|
|
1018
|
+
|
|
1019
|
+
common_problems = (
|
|
1020
|
+
self.github_issues.get("common_problems", [])[:10] if self.github_issues else []
|
|
1021
|
+
)
|
|
1022
|
+
known_solutions = (
|
|
1023
|
+
self.github_issues.get("known_solutions", [])[:10] if self.github_issues else []
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
if common_problems:
|
|
1027
|
+
md += "## Open Issues (Common Problems)\n\n"
|
|
1028
|
+
for i, issue in enumerate(common_problems, 1):
|
|
1029
|
+
title = issue.get("title", "")
|
|
1030
|
+
number = issue.get("number", 0)
|
|
1031
|
+
comments = issue.get("comments", 0)
|
|
1032
|
+
labels = issue.get("labels", [])
|
|
1033
|
+
if isinstance(labels, list):
|
|
1034
|
+
labels_str = ", ".join(str(label) for label in labels)
|
|
1035
|
+
else:
|
|
1036
|
+
labels_str = str(labels) if labels else ""
|
|
1037
|
+
|
|
1038
|
+
md += f"### {i}. {title}\n\n"
|
|
1039
|
+
md += f"**Issue**: #{number}\n"
|
|
1040
|
+
md += f"**Comments**: {comments}\n"
|
|
1041
|
+
if labels_str:
|
|
1042
|
+
md += f"**Labels**: {labels_str}\n"
|
|
1043
|
+
md += (
|
|
1044
|
+
f"**Link**: https://github.com/{self.github_metadata.get('html_url', '').replace('https://github.com/', '')}/issues/{number}\n\n"
|
|
1045
|
+
if self.github_metadata
|
|
1046
|
+
else "\n\n"
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
if known_solutions:
|
|
1050
|
+
md += "\n## Closed Issues (Known Solutions)\n\n"
|
|
1051
|
+
for i, issue in enumerate(known_solutions, 1):
|
|
1052
|
+
title = issue.get("title", "")
|
|
1053
|
+
number = issue.get("number", 0)
|
|
1054
|
+
comments = issue.get("comments", 0)
|
|
1055
|
+
|
|
1056
|
+
md += f"### {i}. {title}\n\n"
|
|
1057
|
+
md += f"**Issue**: #{number} (Closed)\n"
|
|
1058
|
+
md += f"**Comments**: {comments}\n"
|
|
1059
|
+
if self.github_metadata:
|
|
1060
|
+
md += f"**Link**: https://github.com/{self.github_metadata.get('html_url', '').replace('https://github.com/', '')}/issues/{number}\n\n"
|
|
1061
|
+
else:
|
|
1062
|
+
md += "\n\n"
|
|
1063
|
+
|
|
1064
|
+
return md
|
|
1065
|
+
|
|
1066
|
+
def _generate_getting_started_reference(self) -> str:
|
|
1067
|
+
"""
|
|
1068
|
+
Generate getting started reference from README.
|
|
1069
|
+
|
|
1070
|
+
Returns:
|
|
1071
|
+
Markdown content for getting_started.md
|
|
1072
|
+
"""
|
|
1073
|
+
md = "# Getting Started\n\n"
|
|
1074
|
+
md += "*Extracted from project README*\n\n"
|
|
1075
|
+
|
|
1076
|
+
if self.github_docs and self.github_docs.get("readme"):
|
|
1077
|
+
readme = self.github_docs["readme"]
|
|
1078
|
+
|
|
1079
|
+
# Clean and extract full quick start section (up to 2000 chars)
|
|
1080
|
+
cleaner = MarkdownCleaner()
|
|
1081
|
+
content = cleaner.extract_first_section(readme, max_chars=2000)
|
|
1082
|
+
|
|
1083
|
+
md += content
|
|
1084
|
+
else:
|
|
1085
|
+
md += "No README content available.\n"
|
|
1086
|
+
|
|
1087
|
+
return md
|
|
1088
|
+
|
|
1089
|
+
def _generate_reference_files(self, references_dir: Path):
|
|
1090
|
+
"""
|
|
1091
|
+
Generate reference files for progressive disclosure.
|
|
1092
|
+
|
|
1093
|
+
Files created:
|
|
1094
|
+
- github_issues.md: Detailed GitHub issues with solutions
|
|
1095
|
+
- getting_started.md: Full README quick start
|
|
1096
|
+
|
|
1097
|
+
Args:
|
|
1098
|
+
references_dir: Path to references/ directory
|
|
1099
|
+
"""
|
|
1100
|
+
# 1. GitHub Issues Reference
|
|
1101
|
+
if self.github_issues:
|
|
1102
|
+
issues_md = self._generate_github_issues_reference()
|
|
1103
|
+
with open(references_dir / "github_issues.md", "w") as f:
|
|
1104
|
+
f.write(issues_md)
|
|
1105
|
+
|
|
1106
|
+
# 2. Getting Started Reference
|
|
1107
|
+
if self.github_docs and self.github_docs.get("readme"):
|
|
1108
|
+
getting_started_md = self._generate_getting_started_reference()
|
|
1109
|
+
with open(references_dir / "getting_started.md", "w") as f:
|
|
1110
|
+
f.write(getting_started_md)
|
|
1111
|
+
|
|
1112
|
+
def generate(self, output_dir: Path = None) -> tuple[Path, Path]:
|
|
1113
|
+
"""Generate router skill and config with progressive disclosure"""
|
|
1114
|
+
if output_dir is None:
|
|
1115
|
+
output_dir = self.config_paths[0].parent
|
|
1116
|
+
|
|
1117
|
+
output_dir = Path(output_dir)
|
|
1118
|
+
|
|
1119
|
+
# Generate SKILL.md
|
|
1120
|
+
skill_md = self.generate_skill_md()
|
|
1121
|
+
skill_path = output_dir.parent / f"output/{self.router_name}/SKILL.md"
|
|
1122
|
+
skill_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1123
|
+
|
|
1124
|
+
with open(skill_path, "w") as f:
|
|
1125
|
+
f.write(skill_md)
|
|
1126
|
+
|
|
1127
|
+
# NEW: Create references/ directory and generate reference files
|
|
1128
|
+
references_dir = skill_path.parent / "references"
|
|
1129
|
+
references_dir.mkdir(parents=True, exist_ok=True)
|
|
1130
|
+
self._generate_reference_files(references_dir)
|
|
1131
|
+
|
|
1132
|
+
# Generate config
|
|
1133
|
+
router_config = self.create_router_config()
|
|
1134
|
+
config_path = output_dir / f"{self.router_name}.json"
|
|
1135
|
+
|
|
1136
|
+
with open(config_path, "w") as f:
|
|
1137
|
+
json.dump(router_config, f, indent=2)
|
|
1138
|
+
|
|
1139
|
+
return config_path, skill_path
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def main():
|
|
1143
|
+
parser = argparse.ArgumentParser(
|
|
1144
|
+
description="Generate router/hub skill for split documentation",
|
|
1145
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1146
|
+
epilog="""
|
|
1147
|
+
Examples:
|
|
1148
|
+
# Generate router from multiple configs
|
|
1149
|
+
python3 generate_router.py configs/godot-2d.json configs/godot-3d.json configs/godot-scripting.json
|
|
1150
|
+
|
|
1151
|
+
# Use glob pattern
|
|
1152
|
+
python3 generate_router.py configs/godot-*.json
|
|
1153
|
+
|
|
1154
|
+
# Custom router name
|
|
1155
|
+
python3 generate_router.py configs/godot-*.json --name godot-hub
|
|
1156
|
+
|
|
1157
|
+
# Custom output directory
|
|
1158
|
+
python3 generate_router.py configs/godot-*.json --output-dir configs/routers/
|
|
1159
|
+
""",
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
parser.add_argument("configs", nargs="+", help="Sub-skill config files")
|
|
1163
|
+
|
|
1164
|
+
parser.add_argument("--name", help="Router skill name (default: inferred from sub-skills)")
|
|
1165
|
+
|
|
1166
|
+
parser.add_argument("--output-dir", help="Output directory (default: same as input configs)")
|
|
1167
|
+
|
|
1168
|
+
args = parser.parse_args()
|
|
1169
|
+
|
|
1170
|
+
# Filter out router configs (avoid recursion)
|
|
1171
|
+
config_files = []
|
|
1172
|
+
for path_str in args.configs:
|
|
1173
|
+
path = Path(path_str)
|
|
1174
|
+
if path.exists() and not path.stem.endswith("-router"):
|
|
1175
|
+
config_files.append(path_str)
|
|
1176
|
+
|
|
1177
|
+
if not config_files:
|
|
1178
|
+
print("❌ Error: No valid config files provided")
|
|
1179
|
+
sys.exit(1)
|
|
1180
|
+
|
|
1181
|
+
print(f"\n{'=' * 60}")
|
|
1182
|
+
print("ROUTER SKILL GENERATOR")
|
|
1183
|
+
print(f"{'=' * 60}")
|
|
1184
|
+
print(f"Sub-skills: {len(config_files)}")
|
|
1185
|
+
for cfg in config_files:
|
|
1186
|
+
print(f" - {Path(cfg).stem}")
|
|
1187
|
+
print("")
|
|
1188
|
+
|
|
1189
|
+
# Generate router
|
|
1190
|
+
generator = RouterGenerator(config_files, args.name)
|
|
1191
|
+
config_path, skill_path = generator.generate(args.output_dir)
|
|
1192
|
+
|
|
1193
|
+
print(f"✅ Router config created: {config_path}")
|
|
1194
|
+
print(f"✅ Router SKILL.md created: {skill_path}")
|
|
1195
|
+
print("")
|
|
1196
|
+
print(f"{'=' * 60}")
|
|
1197
|
+
print("NEXT STEPS")
|
|
1198
|
+
print(f"{'=' * 60}")
|
|
1199
|
+
print(f"1. Review router SKILL.md: {skill_path}")
|
|
1200
|
+
print("2. Optionally scrape router (for overview pages):")
|
|
1201
|
+
print(f" skill-seekers scrape --config {config_path}")
|
|
1202
|
+
print("3. Package router skill:")
|
|
1203
|
+
print(f" skill-seekers package output/{generator.router_name}/")
|
|
1204
|
+
print("4. Upload router + all sub-skills to Claude")
|
|
1205
|
+
print("")
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
if __name__ == "__main__":
|
|
1209
|
+
main()
|