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,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Manager for Skill Seekers
|
|
3
|
+
|
|
4
|
+
Handles multi-profile GitHub tokens, API keys, and application settings.
|
|
5
|
+
Provides secure storage with file permissions and auto-detection capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigManager:
|
|
18
|
+
"""Manages Skill Seekers configuration with multi-token support."""
|
|
19
|
+
|
|
20
|
+
# Default paths
|
|
21
|
+
CONFIG_DIR = Path.home() / ".config" / "skill-seekers"
|
|
22
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
23
|
+
WELCOME_FLAG = CONFIG_DIR / ".welcomed"
|
|
24
|
+
PROGRESS_DIR = Path.home() / ".local" / "share" / "skill-seekers" / "progress"
|
|
25
|
+
|
|
26
|
+
# Default configuration
|
|
27
|
+
DEFAULT_CONFIG = {
|
|
28
|
+
"version": "1.0",
|
|
29
|
+
"github": {"default_profile": None, "profiles": {}},
|
|
30
|
+
"rate_limit": {
|
|
31
|
+
"default_timeout_minutes": 30,
|
|
32
|
+
"auto_switch_profiles": True,
|
|
33
|
+
"show_countdown": True,
|
|
34
|
+
},
|
|
35
|
+
"resume": {"auto_save_interval_seconds": 60, "keep_progress_days": 7},
|
|
36
|
+
"api_keys": {"anthropic": None, "google": None, "openai": None},
|
|
37
|
+
"first_run": {"completed": False, "version": "2.7.0"},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
"""Initialize configuration manager."""
|
|
42
|
+
self.config_dir = self.CONFIG_DIR
|
|
43
|
+
self.config_file = self.CONFIG_FILE
|
|
44
|
+
self.progress_dir = self.PROGRESS_DIR
|
|
45
|
+
self._ensure_directories()
|
|
46
|
+
self.config = self._load_config()
|
|
47
|
+
|
|
48
|
+
def _ensure_directories(self):
|
|
49
|
+
"""Ensure configuration and progress directories exist with secure permissions."""
|
|
50
|
+
for directory in [self.config_dir, self.progress_dir]:
|
|
51
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
# Set directory permissions to 700 (rwx------)
|
|
53
|
+
directory.chmod(stat.S_IRWXU)
|
|
54
|
+
|
|
55
|
+
def _load_config(self) -> dict[str, Any]:
|
|
56
|
+
"""Load configuration from file or create default."""
|
|
57
|
+
if not self.config_file.exists():
|
|
58
|
+
return self.DEFAULT_CONFIG.copy()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
with open(self.config_file) as f:
|
|
62
|
+
config = json.load(f)
|
|
63
|
+
|
|
64
|
+
# Merge with defaults for any missing keys
|
|
65
|
+
config = self._merge_with_defaults(config)
|
|
66
|
+
return config
|
|
67
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
68
|
+
print(f"ā ļø Warning: Could not load config file: {e}")
|
|
69
|
+
print(" Using default configuration.")
|
|
70
|
+
return self.DEFAULT_CONFIG.copy()
|
|
71
|
+
|
|
72
|
+
def _merge_with_defaults(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
73
|
+
"""Merge loaded config with defaults to ensure all keys exist."""
|
|
74
|
+
|
|
75
|
+
def deep_merge(default: dict, custom: dict) -> dict:
|
|
76
|
+
result = default.copy()
|
|
77
|
+
for key, value in custom.items():
|
|
78
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
79
|
+
result[key] = deep_merge(result[key], value)
|
|
80
|
+
else:
|
|
81
|
+
result[key] = value
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
return deep_merge(self.DEFAULT_CONFIG, config)
|
|
85
|
+
|
|
86
|
+
def save_config(self):
|
|
87
|
+
"""Save configuration to file with secure permissions."""
|
|
88
|
+
try:
|
|
89
|
+
with open(self.config_file, "w") as f:
|
|
90
|
+
json.dump(self.config, f, indent=2)
|
|
91
|
+
|
|
92
|
+
# Set file permissions to 600 (rw-------)
|
|
93
|
+
self.config_file.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
94
|
+
|
|
95
|
+
except OSError as e:
|
|
96
|
+
print(f"ā Error saving config: {e}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
# GitHub Token Management
|
|
100
|
+
|
|
101
|
+
def add_github_profile(
|
|
102
|
+
self,
|
|
103
|
+
name: str,
|
|
104
|
+
token: str,
|
|
105
|
+
description: str = "",
|
|
106
|
+
rate_limit_strategy: str = "prompt",
|
|
107
|
+
timeout_minutes: int = 30,
|
|
108
|
+
set_as_default: bool = False,
|
|
109
|
+
):
|
|
110
|
+
"""Add a new GitHub profile."""
|
|
111
|
+
if not name:
|
|
112
|
+
raise ValueError("Profile name cannot be empty")
|
|
113
|
+
|
|
114
|
+
if not token.startswith("ghp_") and not token.startswith("github_pat_"):
|
|
115
|
+
print("ā ļø Warning: Token doesn't match GitHub format (ghp_* or github_pat_*)")
|
|
116
|
+
|
|
117
|
+
profile = {
|
|
118
|
+
"token": token,
|
|
119
|
+
"description": description,
|
|
120
|
+
"rate_limit_strategy": rate_limit_strategy,
|
|
121
|
+
"timeout_minutes": timeout_minutes,
|
|
122
|
+
"added_at": datetime.now().isoformat(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
self.config["github"]["profiles"][name] = profile
|
|
126
|
+
|
|
127
|
+
if set_as_default or not self.config["github"]["default_profile"]:
|
|
128
|
+
self.config["github"]["default_profile"] = name
|
|
129
|
+
|
|
130
|
+
self.save_config()
|
|
131
|
+
print(f"ā
Added GitHub profile: {name}")
|
|
132
|
+
if set_as_default:
|
|
133
|
+
print("ā
Set as default profile")
|
|
134
|
+
|
|
135
|
+
def remove_github_profile(self, name: str):
|
|
136
|
+
"""Remove a GitHub profile."""
|
|
137
|
+
if name not in self.config["github"]["profiles"]:
|
|
138
|
+
raise ValueError(f"Profile '{name}' not found")
|
|
139
|
+
|
|
140
|
+
del self.config["github"]["profiles"][name]
|
|
141
|
+
|
|
142
|
+
# Update default if we removed it
|
|
143
|
+
if self.config["github"]["default_profile"] == name:
|
|
144
|
+
remaining = list(self.config["github"]["profiles"].keys())
|
|
145
|
+
self.config["github"]["default_profile"] = remaining[0] if remaining else None
|
|
146
|
+
|
|
147
|
+
self.save_config()
|
|
148
|
+
print(f"ā
Removed GitHub profile: {name}")
|
|
149
|
+
|
|
150
|
+
def list_github_profiles(self) -> list[dict[str, Any]]:
|
|
151
|
+
"""List all GitHub profiles."""
|
|
152
|
+
profiles = []
|
|
153
|
+
default = self.config["github"]["default_profile"]
|
|
154
|
+
|
|
155
|
+
for name, data in self.config["github"]["profiles"].items():
|
|
156
|
+
profile_info = {
|
|
157
|
+
"name": name,
|
|
158
|
+
"description": data.get("description", ""),
|
|
159
|
+
"strategy": data.get("rate_limit_strategy", "prompt"),
|
|
160
|
+
"timeout": data.get("timeout_minutes", 30),
|
|
161
|
+
"is_default": name == default,
|
|
162
|
+
"added_at": data.get("added_at", "Unknown"),
|
|
163
|
+
}
|
|
164
|
+
profiles.append(profile_info)
|
|
165
|
+
|
|
166
|
+
return profiles
|
|
167
|
+
|
|
168
|
+
def get_github_token(
|
|
169
|
+
self, profile_name: str | None = None, _repo_url: str | None = None
|
|
170
|
+
) -> str | None:
|
|
171
|
+
"""
|
|
172
|
+
Get GitHub token with smart fallback chain.
|
|
173
|
+
|
|
174
|
+
Priority:
|
|
175
|
+
1. Specified profile_name
|
|
176
|
+
2. Environment variable GITHUB_TOKEN
|
|
177
|
+
3. Default profile from config
|
|
178
|
+
4. None (will use 60/hour unauthenticated)
|
|
179
|
+
"""
|
|
180
|
+
# 1. Check specified profile
|
|
181
|
+
if profile_name:
|
|
182
|
+
profile = self.config["github"]["profiles"].get(profile_name)
|
|
183
|
+
if profile:
|
|
184
|
+
return profile["token"]
|
|
185
|
+
else:
|
|
186
|
+
print(f"ā ļø Warning: Profile '{profile_name}' not found")
|
|
187
|
+
|
|
188
|
+
# 2. Check environment variable
|
|
189
|
+
env_token = os.getenv("GITHUB_TOKEN")
|
|
190
|
+
if env_token:
|
|
191
|
+
return env_token
|
|
192
|
+
|
|
193
|
+
# 3. Check default profile
|
|
194
|
+
default_profile = self.config["github"]["default_profile"]
|
|
195
|
+
if default_profile:
|
|
196
|
+
profile = self.config["github"]["profiles"].get(default_profile)
|
|
197
|
+
if profile:
|
|
198
|
+
return profile["token"]
|
|
199
|
+
|
|
200
|
+
# 4. No token available
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def get_profile_for_token(self, token: str) -> str | None:
|
|
204
|
+
"""Get profile name for a given token."""
|
|
205
|
+
for name, profile in self.config["github"]["profiles"].items():
|
|
206
|
+
if profile["token"] == token:
|
|
207
|
+
return name
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def get_next_profile(self, current_token: str) -> tuple | None:
|
|
211
|
+
"""
|
|
212
|
+
Get next available profile for rate limit switching.
|
|
213
|
+
|
|
214
|
+
Returns: (profile_name, token) or None
|
|
215
|
+
"""
|
|
216
|
+
profiles = list(self.config["github"]["profiles"].items())
|
|
217
|
+
if len(profiles) <= 1:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
# Find current profile index
|
|
221
|
+
current_idx = None
|
|
222
|
+
for idx, (_name, profile) in enumerate(profiles):
|
|
223
|
+
if profile["token"] == current_token:
|
|
224
|
+
current_idx = idx
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
if current_idx is None:
|
|
228
|
+
# Current token not in profiles, return first profile
|
|
229
|
+
name, profile = profiles[0]
|
|
230
|
+
return (name, profile["token"])
|
|
231
|
+
|
|
232
|
+
# Return next profile (circular)
|
|
233
|
+
next_idx = (current_idx + 1) % len(profiles)
|
|
234
|
+
name, profile = profiles[next_idx]
|
|
235
|
+
return (name, profile["token"])
|
|
236
|
+
|
|
237
|
+
def get_rate_limit_strategy(self, token: str | None = None) -> str:
|
|
238
|
+
"""Get rate limit strategy for a token (or default)."""
|
|
239
|
+
if token:
|
|
240
|
+
profile_name = self.get_profile_for_token(token)
|
|
241
|
+
if profile_name:
|
|
242
|
+
profile = self.config["github"]["profiles"][profile_name]
|
|
243
|
+
return profile.get("rate_limit_strategy", "prompt")
|
|
244
|
+
|
|
245
|
+
# Default strategy
|
|
246
|
+
return "prompt"
|
|
247
|
+
|
|
248
|
+
def get_timeout_minutes(self, token: str | None = None) -> int:
|
|
249
|
+
"""Get timeout minutes for a token (or default)."""
|
|
250
|
+
if token:
|
|
251
|
+
profile_name = self.get_profile_for_token(token)
|
|
252
|
+
if profile_name:
|
|
253
|
+
profile = self.config["github"]["profiles"][profile_name]
|
|
254
|
+
return profile.get("timeout_minutes", 30)
|
|
255
|
+
|
|
256
|
+
return self.config["rate_limit"]["default_timeout_minutes"]
|
|
257
|
+
|
|
258
|
+
# API Keys Management
|
|
259
|
+
|
|
260
|
+
def set_api_key(self, provider: str, key: str):
|
|
261
|
+
"""Set API key for a provider (anthropic, google, openai)."""
|
|
262
|
+
if provider not in self.config["api_keys"]:
|
|
263
|
+
raise ValueError(f"Unknown provider: {provider}. Use: anthropic, google, openai")
|
|
264
|
+
|
|
265
|
+
self.config["api_keys"][provider] = key
|
|
266
|
+
self.save_config()
|
|
267
|
+
print(f"ā
Set {provider.capitalize()} API key")
|
|
268
|
+
|
|
269
|
+
def get_api_key(self, provider: str) -> str | None:
|
|
270
|
+
"""
|
|
271
|
+
Get API key with environment variable fallback.
|
|
272
|
+
|
|
273
|
+
Priority:
|
|
274
|
+
1. Environment variable
|
|
275
|
+
2. Config file
|
|
276
|
+
"""
|
|
277
|
+
# Check environment first
|
|
278
|
+
env_map = {
|
|
279
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
280
|
+
"google": "GOOGLE_API_KEY",
|
|
281
|
+
"openai": "OPENAI_API_KEY",
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
env_var = env_map.get(provider)
|
|
285
|
+
if env_var:
|
|
286
|
+
env_key = os.getenv(env_var)
|
|
287
|
+
if env_key:
|
|
288
|
+
return env_key
|
|
289
|
+
|
|
290
|
+
# Check config file
|
|
291
|
+
return self.config["api_keys"].get(provider)
|
|
292
|
+
|
|
293
|
+
# Progress Management
|
|
294
|
+
|
|
295
|
+
def save_progress(self, job_id: str, progress_data: dict[str, Any]):
|
|
296
|
+
"""Save progress for a job."""
|
|
297
|
+
progress_file = self.progress_dir / f"{job_id}.json"
|
|
298
|
+
|
|
299
|
+
progress_data["last_updated"] = datetime.now().isoformat()
|
|
300
|
+
|
|
301
|
+
with open(progress_file, "w") as f:
|
|
302
|
+
json.dump(progress_data, f, indent=2)
|
|
303
|
+
|
|
304
|
+
# Set file permissions to 600
|
|
305
|
+
progress_file.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
306
|
+
|
|
307
|
+
def load_progress(self, job_id: str) -> dict[str, Any] | None:
|
|
308
|
+
"""Load progress for a job."""
|
|
309
|
+
progress_file = self.progress_dir / f"{job_id}.json"
|
|
310
|
+
|
|
311
|
+
if not progress_file.exists():
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
with open(progress_file) as f:
|
|
316
|
+
return json.load(f)
|
|
317
|
+
except (OSError, json.JSONDecodeError):
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
def list_resumable_jobs(self) -> list[dict[str, Any]]:
|
|
321
|
+
"""List all resumable jobs."""
|
|
322
|
+
jobs = []
|
|
323
|
+
|
|
324
|
+
for progress_file in self.progress_dir.glob("*.json"):
|
|
325
|
+
try:
|
|
326
|
+
with open(progress_file) as f:
|
|
327
|
+
data = json.load(f)
|
|
328
|
+
|
|
329
|
+
if data.get("can_resume", False):
|
|
330
|
+
jobs.append(
|
|
331
|
+
{
|
|
332
|
+
"job_id": data.get("job_id", progress_file.stem),
|
|
333
|
+
"started_at": data.get("started_at"),
|
|
334
|
+
"command": data.get("command"),
|
|
335
|
+
"progress": data.get("progress", {}),
|
|
336
|
+
"last_updated": data.get("last_updated"),
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
except (OSError, json.JSONDecodeError):
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# Sort by last updated (newest first)
|
|
343
|
+
jobs.sort(key=lambda x: x.get("last_updated", ""), reverse=True)
|
|
344
|
+
return jobs
|
|
345
|
+
|
|
346
|
+
def delete_progress(self, job_id: str):
|
|
347
|
+
"""Delete progress file for a job."""
|
|
348
|
+
progress_file = self.progress_dir / f"{job_id}.json"
|
|
349
|
+
if progress_file.exists():
|
|
350
|
+
progress_file.unlink()
|
|
351
|
+
|
|
352
|
+
def cleanup_old_progress(self):
|
|
353
|
+
"""Delete progress files older than configured days."""
|
|
354
|
+
keep_days = self.config["resume"]["keep_progress_days"]
|
|
355
|
+
cutoff_date = datetime.now() - timedelta(days=keep_days)
|
|
356
|
+
|
|
357
|
+
deleted_count = 0
|
|
358
|
+
for progress_file in self.progress_dir.glob("*.json"):
|
|
359
|
+
# Check file modification time
|
|
360
|
+
mtime = datetime.fromtimestamp(progress_file.stat().st_mtime)
|
|
361
|
+
if mtime < cutoff_date:
|
|
362
|
+
progress_file.unlink()
|
|
363
|
+
deleted_count += 1
|
|
364
|
+
|
|
365
|
+
if deleted_count > 0:
|
|
366
|
+
print(f"š§¹ Cleaned up {deleted_count} old progress file(s)")
|
|
367
|
+
|
|
368
|
+
# First Run Experience
|
|
369
|
+
|
|
370
|
+
def is_first_run(self) -> bool:
|
|
371
|
+
"""Check if this is the first run."""
|
|
372
|
+
return not self.config["first_run"]["completed"]
|
|
373
|
+
|
|
374
|
+
def mark_first_run_complete(self):
|
|
375
|
+
"""Mark first run as completed."""
|
|
376
|
+
self.config["first_run"]["completed"] = True
|
|
377
|
+
self.save_config()
|
|
378
|
+
|
|
379
|
+
def should_show_welcome(self) -> bool:
|
|
380
|
+
"""Check if we should show welcome message."""
|
|
381
|
+
return not self.WELCOME_FLAG.exists()
|
|
382
|
+
|
|
383
|
+
def mark_welcome_shown(self):
|
|
384
|
+
"""Mark welcome message as shown."""
|
|
385
|
+
self.WELCOME_FLAG.touch()
|
|
386
|
+
self.WELCOME_FLAG.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
387
|
+
|
|
388
|
+
# Display Helpers
|
|
389
|
+
|
|
390
|
+
def display_config_summary(self):
|
|
391
|
+
"""Display current configuration summary."""
|
|
392
|
+
print("\nš Skill Seekers Configuration\n")
|
|
393
|
+
print(f"Config file: {self.config_file}")
|
|
394
|
+
print(f"Progress dir: {self.progress_dir}\n")
|
|
395
|
+
|
|
396
|
+
# GitHub profiles
|
|
397
|
+
profiles = self.list_github_profiles()
|
|
398
|
+
print(f"GitHub Profiles: {len(profiles)}")
|
|
399
|
+
if profiles:
|
|
400
|
+
for p in profiles:
|
|
401
|
+
default_marker = " (default)" if p["is_default"] else ""
|
|
402
|
+
print(f" ⢠{p['name']}{default_marker}")
|
|
403
|
+
if p["description"]:
|
|
404
|
+
print(f" {p['description']}")
|
|
405
|
+
print(f" Strategy: {p['strategy']}, Timeout: {p['timeout']}m")
|
|
406
|
+
else:
|
|
407
|
+
print(" (none configured)")
|
|
408
|
+
|
|
409
|
+
print()
|
|
410
|
+
|
|
411
|
+
# API Keys
|
|
412
|
+
print("API Keys:")
|
|
413
|
+
for provider in ["anthropic", "google", "openai"]:
|
|
414
|
+
key = self.get_api_key(provider)
|
|
415
|
+
status = "ā
Set" if key else "ā Not set"
|
|
416
|
+
source = ""
|
|
417
|
+
if key:
|
|
418
|
+
if os.getenv(provider.upper() + "_API_KEY"):
|
|
419
|
+
source = " (from environment)"
|
|
420
|
+
else:
|
|
421
|
+
source = " (from config)"
|
|
422
|
+
print(f" ⢠{provider.capitalize()}: {status}{source}")
|
|
423
|
+
|
|
424
|
+
print()
|
|
425
|
+
|
|
426
|
+
# Settings
|
|
427
|
+
print("Settings:")
|
|
428
|
+
print(f" ⢠Rate limit timeout: {self.config['rate_limit']['default_timeout_minutes']}m")
|
|
429
|
+
print(f" ⢠Auto-switch profiles: {self.config['rate_limit']['auto_switch_profiles']}")
|
|
430
|
+
print(f" ⢠Keep progress for: {self.config['resume']['keep_progress_days']} days")
|
|
431
|
+
|
|
432
|
+
# Resumable jobs
|
|
433
|
+
jobs = self.list_resumable_jobs()
|
|
434
|
+
if jobs:
|
|
435
|
+
print(f"\nš¦ Resumable Jobs: {len(jobs)}")
|
|
436
|
+
for job in jobs[:5]: # Show max 5
|
|
437
|
+
print(f" ⢠{job['job_id']}")
|
|
438
|
+
if job.get("progress"):
|
|
439
|
+
phase = job["progress"].get("phase", "unknown")
|
|
440
|
+
print(f" Phase: {phase}, Last: {job['last_updated']}")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# Global instance
|
|
444
|
+
_config_manager = None
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def get_config_manager() -> ConfigManager:
|
|
448
|
+
"""Get singleton config manager instance."""
|
|
449
|
+
global _config_manager
|
|
450
|
+
if _config_manager is None:
|
|
451
|
+
_config_manager = ConfigManager()
|
|
452
|
+
return _config_manager
|