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.
Files changed (79) hide show
  1. skill_seekers/__init__.py +22 -0
  2. skill_seekers/cli/__init__.py +39 -0
  3. skill_seekers/cli/adaptors/__init__.py +120 -0
  4. skill_seekers/cli/adaptors/base.py +221 -0
  5. skill_seekers/cli/adaptors/claude.py +485 -0
  6. skill_seekers/cli/adaptors/gemini.py +453 -0
  7. skill_seekers/cli/adaptors/markdown.py +269 -0
  8. skill_seekers/cli/adaptors/openai.py +503 -0
  9. skill_seekers/cli/ai_enhancer.py +310 -0
  10. skill_seekers/cli/api_reference_builder.py +373 -0
  11. skill_seekers/cli/architectural_pattern_detector.py +525 -0
  12. skill_seekers/cli/code_analyzer.py +1462 -0
  13. skill_seekers/cli/codebase_scraper.py +1225 -0
  14. skill_seekers/cli/config_command.py +563 -0
  15. skill_seekers/cli/config_enhancer.py +431 -0
  16. skill_seekers/cli/config_extractor.py +871 -0
  17. skill_seekers/cli/config_manager.py +452 -0
  18. skill_seekers/cli/config_validator.py +394 -0
  19. skill_seekers/cli/conflict_detector.py +528 -0
  20. skill_seekers/cli/constants.py +72 -0
  21. skill_seekers/cli/dependency_analyzer.py +757 -0
  22. skill_seekers/cli/doc_scraper.py +2332 -0
  23. skill_seekers/cli/enhance_skill.py +488 -0
  24. skill_seekers/cli/enhance_skill_local.py +1096 -0
  25. skill_seekers/cli/enhance_status.py +194 -0
  26. skill_seekers/cli/estimate_pages.py +433 -0
  27. skill_seekers/cli/generate_router.py +1209 -0
  28. skill_seekers/cli/github_fetcher.py +534 -0
  29. skill_seekers/cli/github_scraper.py +1466 -0
  30. skill_seekers/cli/guide_enhancer.py +723 -0
  31. skill_seekers/cli/how_to_guide_builder.py +1267 -0
  32. skill_seekers/cli/install_agent.py +461 -0
  33. skill_seekers/cli/install_skill.py +178 -0
  34. skill_seekers/cli/language_detector.py +614 -0
  35. skill_seekers/cli/llms_txt_detector.py +60 -0
  36. skill_seekers/cli/llms_txt_downloader.py +104 -0
  37. skill_seekers/cli/llms_txt_parser.py +150 -0
  38. skill_seekers/cli/main.py +558 -0
  39. skill_seekers/cli/markdown_cleaner.py +132 -0
  40. skill_seekers/cli/merge_sources.py +806 -0
  41. skill_seekers/cli/package_multi.py +77 -0
  42. skill_seekers/cli/package_skill.py +241 -0
  43. skill_seekers/cli/pattern_recognizer.py +1825 -0
  44. skill_seekers/cli/pdf_extractor_poc.py +1166 -0
  45. skill_seekers/cli/pdf_scraper.py +617 -0
  46. skill_seekers/cli/quality_checker.py +519 -0
  47. skill_seekers/cli/rate_limit_handler.py +438 -0
  48. skill_seekers/cli/resume_command.py +160 -0
  49. skill_seekers/cli/run_tests.py +230 -0
  50. skill_seekers/cli/setup_wizard.py +93 -0
  51. skill_seekers/cli/split_config.py +390 -0
  52. skill_seekers/cli/swift_patterns.py +560 -0
  53. skill_seekers/cli/test_example_extractor.py +1081 -0
  54. skill_seekers/cli/test_unified_simple.py +179 -0
  55. skill_seekers/cli/unified_codebase_analyzer.py +572 -0
  56. skill_seekers/cli/unified_scraper.py +932 -0
  57. skill_seekers/cli/unified_skill_builder.py +1605 -0
  58. skill_seekers/cli/upload_skill.py +162 -0
  59. skill_seekers/cli/utils.py +432 -0
  60. skill_seekers/mcp/__init__.py +33 -0
  61. skill_seekers/mcp/agent_detector.py +316 -0
  62. skill_seekers/mcp/git_repo.py +273 -0
  63. skill_seekers/mcp/server.py +231 -0
  64. skill_seekers/mcp/server_fastmcp.py +1249 -0
  65. skill_seekers/mcp/server_legacy.py +2302 -0
  66. skill_seekers/mcp/source_manager.py +285 -0
  67. skill_seekers/mcp/tools/__init__.py +115 -0
  68. skill_seekers/mcp/tools/config_tools.py +251 -0
  69. skill_seekers/mcp/tools/packaging_tools.py +826 -0
  70. skill_seekers/mcp/tools/scraping_tools.py +842 -0
  71. skill_seekers/mcp/tools/source_tools.py +828 -0
  72. skill_seekers/mcp/tools/splitting_tools.py +212 -0
  73. skill_seekers/py.typed +0 -0
  74. skill_seekers-2.7.3.dist-info/METADATA +2027 -0
  75. skill_seekers-2.7.3.dist-info/RECORD +79 -0
  76. skill_seekers-2.7.3.dist-info/WHEEL +5 -0
  77. skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
  78. skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
  79. 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()