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,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)}")]
|