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,828 @@
1
+ """
2
+ Source management tools for MCP server.
3
+
4
+ This module contains tools for managing config sources:
5
+ - fetch_config: Fetch configs from API, git URL, or named sources
6
+ - submit_config: Submit configs to the community repository
7
+ - add_config_source: Register a git repository as a config source
8
+ - list_config_sources: List all registered config sources
9
+ - remove_config_source: Remove a registered config source
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import re
15
+ from pathlib import Path
16
+
17
+ # MCP types (imported conditionally)
18
+ try:
19
+ from mcp.types import TextContent
20
+
21
+ MCP_AVAILABLE = True
22
+ except ImportError:
23
+ # Graceful degradation: Create a simple fallback class for testing
24
+ class TextContent:
25
+ """Fallback TextContent for when MCP is not installed"""
26
+
27
+ def __init__(self, type: str, text: str):
28
+ self.type = type
29
+ self.text = text
30
+
31
+ MCP_AVAILABLE = False
32
+
33
+ import httpx
34
+
35
+
36
+ async def fetch_config_tool(args: dict) -> list[TextContent]:
37
+ """
38
+ Fetch config from API, git URL, or named source.
39
+
40
+ Supports three modes:
41
+ 1. Named source from registry (highest priority)
42
+ 2. Direct git URL
43
+ 3. API (default, backward compatible)
44
+
45
+ Args:
46
+ args: Dictionary containing:
47
+ - config_name: Name of config to download (optional for API list mode)
48
+ - destination: Directory to save config file (default: "configs")
49
+ - list_available: List all available configs from API (default: false)
50
+ - category: Filter configs by category when listing (optional)
51
+ - git_url: Git repository URL (enables git mode)
52
+ - source: Named source from registry (enables named source mode)
53
+ - branch: Git branch to use (default: "main")
54
+ - token: Authentication token for private repos (optional)
55
+ - refresh: Force refresh cached git repository (default: false)
56
+
57
+ Returns:
58
+ List of TextContent with fetch results or config list
59
+ """
60
+ from skill_seekers.mcp.git_repo import GitConfigRepo
61
+ from skill_seekers.mcp.source_manager import SourceManager
62
+
63
+ config_name = args.get("config_name")
64
+ destination = args.get("destination", "configs")
65
+ list_available = args.get("list_available", False)
66
+ category = args.get("category")
67
+
68
+ # Git mode parameters
69
+ source_name = args.get("source")
70
+ git_url = args.get("git_url")
71
+ branch = args.get("branch", "main")
72
+ token = args.get("token")
73
+ force_refresh = args.get("refresh", False)
74
+
75
+ try:
76
+ # MODE 1: Named Source (highest priority)
77
+ if source_name:
78
+ if not config_name:
79
+ return [
80
+ TextContent(
81
+ type="text",
82
+ text="❌ Error: config_name is required when using source parameter",
83
+ )
84
+ ]
85
+
86
+ # Get source from registry
87
+ source_manager = SourceManager()
88
+ try:
89
+ source = source_manager.get_source(source_name)
90
+ except KeyError as e:
91
+ return [TextContent(type="text", text=f"❌ {str(e)}")]
92
+
93
+ git_url = source["git_url"]
94
+ branch = source.get("branch", branch)
95
+ token_env = source.get("token_env")
96
+
97
+ # Get token from environment if not provided
98
+ if not token and token_env:
99
+ token = os.environ.get(token_env)
100
+
101
+ # Clone/pull repository
102
+ git_repo = GitConfigRepo()
103
+ try:
104
+ repo_path = git_repo.clone_or_pull(
105
+ source_name=source_name,
106
+ git_url=git_url,
107
+ branch=branch,
108
+ token=token,
109
+ force_refresh=force_refresh,
110
+ )
111
+ except Exception as e:
112
+ return [TextContent(type="text", text=f"❌ Git error: {str(e)}")]
113
+
114
+ # Load config from repository
115
+ try:
116
+ config_data = git_repo.get_config(repo_path, config_name)
117
+ except FileNotFoundError as e:
118
+ return [TextContent(type="text", text=f"❌ {str(e)}")]
119
+ except ValueError as e:
120
+ return [TextContent(type="text", text=f"❌ {str(e)}")]
121
+
122
+ # Save to destination
123
+ dest_path = Path(destination)
124
+ dest_path.mkdir(parents=True, exist_ok=True)
125
+ config_file = dest_path / f"{config_name}.json"
126
+
127
+ with open(config_file, "w") as f:
128
+ json.dump(config_data, f, indent=2)
129
+
130
+ result = f"""✅ Config fetched from git source successfully!
131
+
132
+ 📦 Config: {config_name}
133
+ 📂 Saved to: {config_file}
134
+ 🔗 Source: {source_name}
135
+ 🌿 Branch: {branch}
136
+ 📁 Repository: {git_url}
137
+ 🔄 Refreshed: {"Yes (forced)" if force_refresh else "No (used cache)"}
138
+
139
+ Next steps:
140
+ 1. Review config: cat {config_file}
141
+ 2. Estimate pages: Use estimate_pages tool
142
+ 3. Scrape docs: Use scrape_docs tool
143
+
144
+ 💡 Manage sources: Use add_config_source, list_config_sources, remove_config_source tools
145
+ """
146
+ return [TextContent(type="text", text=result)]
147
+
148
+ # MODE 2: Direct Git URL
149
+ elif git_url:
150
+ if not config_name:
151
+ return [
152
+ TextContent(
153
+ type="text",
154
+ text="❌ Error: config_name is required when using git_url parameter",
155
+ )
156
+ ]
157
+
158
+ # Clone/pull repository
159
+ git_repo = GitConfigRepo()
160
+ source_name_temp = f"temp_{config_name}"
161
+
162
+ try:
163
+ repo_path = git_repo.clone_or_pull(
164
+ source_name=source_name_temp,
165
+ git_url=git_url,
166
+ branch=branch,
167
+ token=token,
168
+ force_refresh=force_refresh,
169
+ )
170
+ except ValueError as e:
171
+ return [TextContent(type="text", text=f"❌ Invalid git URL: {str(e)}")]
172
+ except Exception as e:
173
+ return [TextContent(type="text", text=f"❌ Git error: {str(e)}")]
174
+
175
+ # Load config from repository
176
+ try:
177
+ config_data = git_repo.get_config(repo_path, config_name)
178
+ except FileNotFoundError as e:
179
+ return [TextContent(type="text", text=f"❌ {str(e)}")]
180
+ except ValueError as e:
181
+ return [TextContent(type="text", text=f"❌ {str(e)}")]
182
+
183
+ # Save to destination
184
+ dest_path = Path(destination)
185
+ dest_path.mkdir(parents=True, exist_ok=True)
186
+ config_file = dest_path / f"{config_name}.json"
187
+
188
+ with open(config_file, "w") as f:
189
+ json.dump(config_data, f, indent=2)
190
+
191
+ result = f"""✅ Config fetched from git URL successfully!
192
+
193
+ 📦 Config: {config_name}
194
+ 📂 Saved to: {config_file}
195
+ 📁 Repository: {git_url}
196
+ 🌿 Branch: {branch}
197
+ 🔄 Refreshed: {"Yes (forced)" if force_refresh else "No (used cache)"}
198
+
199
+ Next steps:
200
+ 1. Review config: cat {config_file}
201
+ 2. Estimate pages: Use estimate_pages tool
202
+ 3. Scrape docs: Use scrape_docs tool
203
+
204
+ 💡 Register this source: Use add_config_source to save for future use
205
+ """
206
+ return [TextContent(type="text", text=result)]
207
+
208
+ # MODE 3: API (existing, backward compatible)
209
+ else:
210
+ API_BASE_URL = "https://api.skillseekersweb.com"
211
+
212
+ async with httpx.AsyncClient(timeout=30.0) as client:
213
+ # List available configs if requested or no config_name provided
214
+ if list_available or not config_name:
215
+ # Build API URL with optional category filter
216
+ list_url = f"{API_BASE_URL}/api/configs"
217
+ params = {}
218
+ if category:
219
+ params["category"] = category
220
+
221
+ response = await client.get(list_url, params=params)
222
+ response.raise_for_status()
223
+ data = response.json()
224
+
225
+ configs = data.get("configs", [])
226
+ total = data.get("total", 0)
227
+ filters = data.get("filters")
228
+
229
+ # Format list output
230
+ result = f"📋 Available Configs ({total} total)\n"
231
+ if filters:
232
+ result += f"🔍 Filters: {filters}\n"
233
+ result += "\n"
234
+
235
+ # Group by category
236
+ by_category = {}
237
+ for config in configs:
238
+ cat = config.get("category", "uncategorized")
239
+ if cat not in by_category:
240
+ by_category[cat] = []
241
+ by_category[cat].append(config)
242
+
243
+ for cat, cat_configs in sorted(by_category.items()):
244
+ result += f"\n**{cat.upper()}** ({len(cat_configs)} configs):\n"
245
+ for cfg in cat_configs:
246
+ name = cfg.get("name")
247
+ desc = cfg.get("description", "")[:60]
248
+ config_type = cfg.get("type", "unknown")
249
+ tags = ", ".join(cfg.get("tags", [])[:3])
250
+ result += f" • {name} [{config_type}] - {desc}{'...' if len(cfg.get('description', '')) > 60 else ''}\n"
251
+ if tags:
252
+ result += f" Tags: {tags}\n"
253
+
254
+ result += (
255
+ "\n💡 To download a config, use: fetch_config with config_name='<name>'\n"
256
+ )
257
+ result += f"📚 API Docs: {API_BASE_URL}/docs\n"
258
+
259
+ return [TextContent(type="text", text=result)]
260
+
261
+ # Download specific config
262
+ if not config_name:
263
+ return [
264
+ TextContent(
265
+ type="text",
266
+ text="❌ Error: Please provide config_name or set list_available=true",
267
+ )
268
+ ]
269
+
270
+ # Get config details first
271
+ detail_url = f"{API_BASE_URL}/api/configs/{config_name}"
272
+ detail_response = await client.get(detail_url)
273
+
274
+ if detail_response.status_code == 404:
275
+ return [
276
+ TextContent(
277
+ type="text",
278
+ text=f"❌ Config '{config_name}' not found. Use list_available=true to see available configs.",
279
+ )
280
+ ]
281
+
282
+ detail_response.raise_for_status()
283
+ config_info = detail_response.json()
284
+
285
+ # Download the actual config file using the download_url from API response
286
+ download_url = config_info.get("download_url")
287
+ if not download_url:
288
+ return [
289
+ TextContent(
290
+ type="text",
291
+ text=f"❌ Config '{config_name}' has no download_url. Contact support.",
292
+ )
293
+ ]
294
+
295
+ download_response = await client.get(download_url)
296
+ download_response.raise_for_status()
297
+ config_data = download_response.json()
298
+
299
+ # Save to destination
300
+ dest_path = Path(destination)
301
+ dest_path.mkdir(parents=True, exist_ok=True)
302
+ config_file = dest_path / f"{config_name}.json"
303
+
304
+ with open(config_file, "w") as f:
305
+ json.dump(config_data, f, indent=2)
306
+
307
+ # Build result message
308
+ result = f"""✅ Config downloaded successfully!
309
+
310
+ 📦 Config: {config_name}
311
+ 📂 Saved to: {config_file}
312
+ 📊 Category: {config_info.get("category", "uncategorized")}
313
+ 🏷️ Tags: {", ".join(config_info.get("tags", []))}
314
+ 📄 Type: {config_info.get("type", "unknown")}
315
+ 📝 Description: {config_info.get("description", "No description")}
316
+
317
+ 🔗 Source: {config_info.get("primary_source", "N/A")}
318
+ 📏 Max pages: {config_info.get("max_pages", "N/A")}
319
+ 📦 File size: {config_info.get("file_size", "N/A")} bytes
320
+ 🕒 Last updated: {config_info.get("last_updated", "N/A")}
321
+
322
+ Next steps:
323
+ 1. Review config: cat {config_file}
324
+ 2. Estimate pages: Use estimate_pages tool
325
+ 3. Scrape docs: Use scrape_docs tool
326
+
327
+ 💡 More configs: Use list_available=true to see all available configs
328
+ """
329
+
330
+ return [TextContent(type="text", text=result)]
331
+
332
+ except httpx.HTTPError as e:
333
+ return [
334
+ TextContent(
335
+ type="text",
336
+ text=f"❌ HTTP Error: {str(e)}\n\nCheck your internet connection or try again later.",
337
+ )
338
+ ]
339
+ except json.JSONDecodeError as e:
340
+ return [
341
+ TextContent(type="text", text=f"❌ JSON Error: Invalid response from API: {str(e)}")
342
+ ]
343
+ except Exception as e:
344
+ return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
345
+
346
+
347
+ async def submit_config_tool(args: dict) -> list[TextContent]:
348
+ """
349
+ Submit a custom config to skill-seekers-configs repository via GitHub issue.
350
+
351
+ Validates the config (both legacy and unified formats) and creates a GitHub
352
+ issue for community review.
353
+
354
+ Args:
355
+ args: Dictionary containing:
356
+ - config_path: Path to config JSON file (optional)
357
+ - config_json: Config JSON as string (optional, alternative to config_path)
358
+ - testing_notes: Notes about testing (optional)
359
+ - github_token: GitHub personal access token (optional, can use GITHUB_TOKEN env var)
360
+
361
+ Returns:
362
+ List of TextContent with submission results
363
+ """
364
+ try:
365
+ from github import Github, GithubException
366
+ except ImportError:
367
+ return [
368
+ TextContent(
369
+ type="text",
370
+ text="❌ Error: PyGithub not installed.\n\nInstall with: pip install PyGithub",
371
+ )
372
+ ]
373
+
374
+ # Import config validator
375
+ try:
376
+ import sys
377
+ from pathlib import Path
378
+
379
+ CLI_DIR = Path(__file__).parent.parent.parent / "cli"
380
+ sys.path.insert(0, str(CLI_DIR))
381
+ from config_validator import ConfigValidator
382
+ except ImportError:
383
+ ConfigValidator = None
384
+
385
+ config_path = args.get("config_path")
386
+ config_json_str = args.get("config_json")
387
+ testing_notes = args.get("testing_notes", "")
388
+ github_token = args.get("github_token") or os.environ.get("GITHUB_TOKEN")
389
+
390
+ try:
391
+ # Load config data
392
+ if config_path:
393
+ config_file = Path(config_path)
394
+ if not config_file.exists():
395
+ return [
396
+ TextContent(type="text", text=f"❌ Error: Config file not found: {config_path}")
397
+ ]
398
+
399
+ with open(config_file) as f:
400
+ config_data = json.load(f)
401
+ config_json_str = json.dumps(config_data, indent=2)
402
+ config_name = config_data.get("name", config_file.stem)
403
+
404
+ elif config_json_str:
405
+ try:
406
+ config_data = json.loads(config_json_str)
407
+ config_name = config_data.get("name", "unnamed")
408
+ except json.JSONDecodeError as e:
409
+ return [TextContent(type="text", text=f"❌ Error: Invalid JSON: {str(e)}")]
410
+
411
+ else:
412
+ return [
413
+ TextContent(
414
+ type="text", text="❌ Error: Must provide either config_path or config_json"
415
+ )
416
+ ]
417
+
418
+ # Use ConfigValidator for comprehensive validation
419
+ if ConfigValidator is None:
420
+ return [
421
+ TextContent(
422
+ type="text",
423
+ text="❌ Error: ConfigValidator not available. Please ensure config_validator.py is in the CLI directory.",
424
+ )
425
+ ]
426
+
427
+ try:
428
+ validator = ConfigValidator(config_data)
429
+ validator.validate()
430
+
431
+ # Get format info
432
+ is_unified = validator.is_unified
433
+ config_name = config_data.get("name", "unnamed")
434
+
435
+ # Additional format validation (ConfigValidator only checks structure)
436
+ # Validate name format (alphanumeric, hyphens, underscores only)
437
+ if not re.match(r"^[a-zA-Z0-9_-]+$", config_name):
438
+ raise ValueError(
439
+ f"Invalid name format: '{config_name}'\nNames must contain only alphanumeric characters, hyphens, and underscores"
440
+ )
441
+
442
+ # Validate URL formats
443
+ if not is_unified:
444
+ # Legacy config - check base_url
445
+ base_url = config_data.get("base_url", "")
446
+ if base_url and not (
447
+ base_url.startswith("http://") or base_url.startswith("https://")
448
+ ):
449
+ raise ValueError(
450
+ f"Invalid base_url format: '{base_url}'\nURLs must start with http:// or https://"
451
+ )
452
+ else:
453
+ # Unified config - check URLs in sources
454
+ for idx, source in enumerate(config_data.get("sources", [])):
455
+ if source.get("type") == "documentation":
456
+ source_url = source.get("base_url", "")
457
+ if source_url and not (
458
+ source_url.startswith("http://") or source_url.startswith("https://")
459
+ ):
460
+ raise ValueError(
461
+ f"Source {idx} (documentation): Invalid base_url format: '{source_url}'\nURLs must start with http:// or https://"
462
+ )
463
+
464
+ except ValueError as validation_error:
465
+ # Provide detailed validation feedback
466
+ error_msg = f"""❌ Config validation failed:
467
+
468
+ {str(validation_error)}
469
+
470
+ Please fix these issues and try again.
471
+
472
+ 💡 Validation help:
473
+ - Names: alphanumeric, hyphens, underscores only (e.g., "my-framework", "react_docs")
474
+ - URLs: must start with http:// or https://
475
+ - Selectors: should be a dict with keys like 'main_content', 'title', 'code_blocks'
476
+ - Rate limit: non-negative number (default: 0.5)
477
+ - Max pages: positive integer or -1 for unlimited
478
+
479
+ 📚 Example configs: https://github.com/yusufkaraaslan/skill-seekers-configs/tree/main/official
480
+ """
481
+ return [TextContent(type="text", text=error_msg)]
482
+
483
+ # Detect category based on config format and content
484
+ if is_unified:
485
+ # For unified configs, look at source types
486
+ source_types = [src.get("type") for src in config_data.get("sources", [])]
487
+ if (
488
+ "documentation" in source_types
489
+ and "github" in source_types
490
+ or "documentation" in source_types
491
+ and "pdf" in source_types
492
+ or len(source_types) > 1
493
+ ):
494
+ category = "multi-source"
495
+ else:
496
+ category = "unified"
497
+ else:
498
+ # For legacy configs, use name-based detection
499
+ name_lower = config_name.lower()
500
+ category = "other"
501
+ if any(
502
+ x in name_lower
503
+ for x in ["react", "vue", "django", "laravel", "fastapi", "astro", "hono"]
504
+ ):
505
+ category = "web-frameworks"
506
+ elif any(x in name_lower for x in ["godot", "unity", "unreal"]):
507
+ category = "game-engines"
508
+ elif any(x in name_lower for x in ["kubernetes", "ansible", "docker"]):
509
+ category = "devops"
510
+ elif any(x in name_lower for x in ["tailwind", "bootstrap", "bulma"]):
511
+ category = "css-frameworks"
512
+
513
+ # Collect validation warnings
514
+ warnings = []
515
+ if not is_unified:
516
+ # Legacy config warnings
517
+ if "max_pages" not in config_data:
518
+ warnings.append("⚠️ No max_pages set - will use default (100)")
519
+ elif config_data.get("max_pages") in (None, -1):
520
+ warnings.append(
521
+ "⚠️ Unlimited scraping enabled - may scrape thousands of pages and take hours"
522
+ )
523
+ else:
524
+ # Unified config warnings
525
+ for src in config_data.get("sources", []):
526
+ if src.get("type") == "documentation" and "max_pages" not in src:
527
+ warnings.append(
528
+ "⚠️ No max_pages set for documentation source - will use default (100)"
529
+ )
530
+ elif src.get("type") == "documentation" and src.get("max_pages") in (None, -1):
531
+ warnings.append("⚠️ Unlimited scraping enabled for documentation source")
532
+
533
+ # Check for GitHub token
534
+ if not github_token:
535
+ return [
536
+ TextContent(
537
+ type="text",
538
+ text="❌ Error: GitHub token required.\n\nProvide github_token parameter or set GITHUB_TOKEN environment variable.\n\nCreate token at: https://github.com/settings/tokens",
539
+ )
540
+ ]
541
+
542
+ # Create GitHub issue
543
+ try:
544
+ gh = Github(github_token)
545
+ repo = gh.get_repo("yusufkaraaslan/skill-seekers-configs")
546
+
547
+ # Build issue body
548
+ issue_body = f"""## Config Submission
549
+
550
+ ### Framework/Tool Name
551
+ {config_name}
552
+
553
+ ### Category
554
+ {category}
555
+
556
+ ### Config Format
557
+ {"Unified (multi-source)" if is_unified else "Legacy (single-source)"}
558
+
559
+ ### Configuration JSON
560
+ ```json
561
+ {config_json_str}
562
+ ```
563
+
564
+ ### Testing Results
565
+ {testing_notes if testing_notes else "Not provided"}
566
+
567
+ ### Documentation URL
568
+ {config_data.get("base_url") if not is_unified else "See sources in config"}
569
+
570
+ {"### Validation Warnings" if warnings else ""}
571
+ {chr(10).join(f"- {w}" for w in warnings) if warnings else ""}
572
+
573
+ ---
574
+
575
+ ### Checklist
576
+ - [x] Config validated with ConfigValidator
577
+ - [ ] Test scraping completed
578
+ - [ ] Added to appropriate category
579
+ - [ ] API updated
580
+ """
581
+
582
+ # Create issue
583
+ issue = repo.create_issue(
584
+ title=f"[CONFIG] {config_name}",
585
+ body=issue_body,
586
+ labels=["config-submission", "needs-review"],
587
+ )
588
+
589
+ result = f"""✅ Config submitted successfully!
590
+
591
+ 📝 Issue created: {issue.html_url}
592
+ 🏷️ Issue #{issue.number}
593
+ 📦 Config: {config_name}
594
+ 📊 Category: {category}
595
+ 🏷️ Labels: config-submission, needs-review
596
+
597
+ What happens next:
598
+ 1. Maintainers will review your config
599
+ 2. They'll test it with the actual documentation
600
+ 3. If approved, it will be added to official/{category}/
601
+ 4. The API will auto-update and your config becomes available!
602
+
603
+ 💡 Track your submission: {issue.html_url}
604
+ 📚 All configs: https://github.com/yusufkaraaslan/skill-seekers-configs
605
+ """
606
+
607
+ return [TextContent(type="text", text=result)]
608
+
609
+ except GithubException as e:
610
+ return [
611
+ TextContent(
612
+ type="text",
613
+ text=f"❌ GitHub Error: {str(e)}\n\nCheck your token permissions (needs 'repo' or 'public_repo' scope).",
614
+ )
615
+ ]
616
+
617
+ except Exception as e:
618
+ return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
619
+
620
+
621
+ async def add_config_source_tool(args: dict) -> list[TextContent]:
622
+ """
623
+ Register a git repository as a config source.
624
+
625
+ Allows fetching configs from private/team repos. Use this to set up named
626
+ sources that can be referenced by fetch_config.
627
+
628
+ Args:
629
+ args: Dictionary containing:
630
+ - name: Source identifier (required)
631
+ - git_url: Git repository URL (required)
632
+ - source_type: Source type (default: "github")
633
+ - token_env: Environment variable name for auth token (optional)
634
+ - branch: Git branch to use (default: "main")
635
+ - priority: Source priority (default: 100, lower = higher priority)
636
+ - enabled: Whether source is enabled (default: true)
637
+
638
+ Returns:
639
+ List of TextContent with registration results
640
+ """
641
+ from skill_seekers.mcp.source_manager import SourceManager
642
+
643
+ name = args.get("name")
644
+ git_url = args.get("git_url")
645
+ source_type = args.get("source_type", "github")
646
+ token_env = args.get("token_env")
647
+ branch = args.get("branch", "main")
648
+ priority = args.get("priority", 100)
649
+ enabled = args.get("enabled", True)
650
+
651
+ try:
652
+ # Validate required parameters
653
+ if not name:
654
+ return [TextContent(type="text", text="❌ Error: 'name' parameter is required")]
655
+ if not git_url:
656
+ return [TextContent(type="text", text="❌ Error: 'git_url' parameter is required")]
657
+
658
+ # Add source
659
+ source_manager = SourceManager()
660
+ source = source_manager.add_source(
661
+ name=name,
662
+ git_url=git_url,
663
+ source_type=source_type,
664
+ token_env=token_env,
665
+ branch=branch,
666
+ priority=priority,
667
+ enabled=enabled,
668
+ )
669
+
670
+ # Check if this is an update
671
+ is_update = "updated_at" in source and source["added_at"] != source["updated_at"]
672
+
673
+ result = f"""✅ Config source {"updated" if is_update else "registered"} successfully!
674
+
675
+ 📛 Name: {source["name"]}
676
+ 📁 Repository: {source["git_url"]}
677
+ 🔖 Type: {source["type"]}
678
+ 🌿 Branch: {source["branch"]}
679
+ 🔑 Token env: {source.get("token_env", "None")}
680
+ ⚡ Priority: {source["priority"]} (lower = higher priority)
681
+ ✓ Enabled: {source["enabled"]}
682
+ 🕒 Added: {source["added_at"][:19]}
683
+
684
+ Usage:
685
+ # Fetch config from this source
686
+ fetch_config(source="{source["name"]}", config_name="your-config")
687
+
688
+ # List all sources
689
+ list_config_sources()
690
+
691
+ # Remove this source
692
+ remove_config_source(name="{source["name"]}")
693
+
694
+ 💡 Make sure to set {source.get("token_env", "GIT_TOKEN")} environment variable for private repos
695
+ """
696
+
697
+ return [TextContent(type="text", text=result)]
698
+
699
+ except ValueError as e:
700
+ return [TextContent(type="text", text=f"❌ Validation Error: {str(e)}")]
701
+ except Exception as e:
702
+ return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
703
+
704
+
705
+ async def list_config_sources_tool(args: dict) -> list[TextContent]:
706
+ """
707
+ List all registered config sources.
708
+
709
+ Shows git repositories that have been registered with add_config_source.
710
+
711
+ Args:
712
+ args: Dictionary containing:
713
+ - enabled_only: Only show enabled sources (default: false)
714
+
715
+ Returns:
716
+ List of TextContent with source list
717
+ """
718
+ from skill_seekers.mcp.source_manager import SourceManager
719
+
720
+ enabled_only = args.get("enabled_only", False)
721
+
722
+ try:
723
+ source_manager = SourceManager()
724
+ sources = source_manager.list_sources(enabled_only=enabled_only)
725
+
726
+ if not sources:
727
+ result = """📋 No config sources registered
728
+
729
+ To add a source:
730
+ add_config_source(
731
+ name="team",
732
+ git_url="https://github.com/myorg/configs.git"
733
+ )
734
+
735
+ 💡 Once added, use: fetch_config(source="team", config_name="...")
736
+ """
737
+ return [TextContent(type="text", text=result)]
738
+
739
+ # Format sources list
740
+ result = f"📋 Config Sources ({len(sources)} total"
741
+ if enabled_only:
742
+ result += ", enabled only"
743
+ result += ")\n\n"
744
+
745
+ for source in sources:
746
+ status_icon = "✓" if source.get("enabled", True) else "✗"
747
+ result += f"{status_icon} **{source['name']}**\n"
748
+ result += f" 📁 {source['git_url']}\n"
749
+ result += f" 🔖 Type: {source['type']} | 🌿 Branch: {source['branch']}\n"
750
+ result += f" 🔑 Token: {source.get('token_env', 'None')} | ⚡ Priority: {source['priority']}\n"
751
+ result += f" 🕒 Added: {source['added_at'][:19]}\n"
752
+ result += "\n"
753
+
754
+ result += """Usage:
755
+ # Fetch config from a source
756
+ fetch_config(source="SOURCE_NAME", config_name="CONFIG_NAME")
757
+
758
+ # Add new source
759
+ add_config_source(name="...", git_url="...")
760
+
761
+ # Remove source
762
+ remove_config_source(name="SOURCE_NAME")
763
+ """
764
+
765
+ return [TextContent(type="text", text=result)]
766
+
767
+ except Exception as e:
768
+ return [TextContent(type="text", text=f"❌ Error: {str(e)}")]
769
+
770
+
771
+ async def remove_config_source_tool(args: dict) -> list[TextContent]:
772
+ """
773
+ Remove a registered config source.
774
+
775
+ Deletes the source from the registry. Does not delete cached git repository data.
776
+
777
+ Args:
778
+ args: Dictionary containing:
779
+ - name: Source identifier to remove (required)
780
+
781
+ Returns:
782
+ List of TextContent with removal results
783
+ """
784
+ from skill_seekers.mcp.source_manager import SourceManager
785
+
786
+ name = args.get("name")
787
+
788
+ try:
789
+ # Validate required parameter
790
+ if not name:
791
+ return [TextContent(type="text", text="❌ Error: 'name' parameter is required")]
792
+
793
+ # Remove source
794
+ source_manager = SourceManager()
795
+ removed = source_manager.remove_source(name)
796
+
797
+ if removed:
798
+ result = f"""✅ Config source removed successfully!
799
+
800
+ 📛 Removed: {name}
801
+
802
+ ⚠️ Note: Cached git repository data is NOT deleted
803
+ To free up disk space, manually delete: ~/.skill-seekers/cache/{name}/
804
+
805
+ Next steps:
806
+ # List remaining sources
807
+ list_config_sources()
808
+
809
+ # Add a different source
810
+ add_config_source(name="...", git_url="...")
811
+ """
812
+ return [TextContent(type="text", text=result)]
813
+ else:
814
+ # Not found - show available sources
815
+ sources = source_manager.list_sources()
816
+ available = [s["name"] for s in sources]
817
+
818
+ result = f"""❌ Source '{name}' not found
819
+
820
+ Available sources: {", ".join(available) if available else "none"}
821
+
822
+ To see all sources:
823
+ list_config_sources()
824
+ """
825
+ return [TextContent(type="text", text=result)]
826
+
827
+ except Exception as e:
828
+ return [TextContent(type="text", text=f"❌ Error: {str(e)}")]