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,438 @@
1
+ """
2
+ Rate Limit Handler for GitHub API
3
+
4
+ Handles GitHub API rate limits with smart strategies:
5
+ - Upfront warnings about token status
6
+ - Real-time countdown timers
7
+ - Profile switching for multi-token setups
8
+ - Progress auto-save on interruption
9
+ - Non-interactive mode for CI/CD
10
+ """
11
+
12
+ import sys
13
+ import time
14
+ from datetime import datetime
15
+ from typing import Any
16
+
17
+ import requests
18
+
19
+ from .config_manager import get_config_manager
20
+
21
+
22
+ class RateLimitError(Exception):
23
+ """Raised when rate limit is exceeded and cannot be handled."""
24
+
25
+ pass
26
+
27
+
28
+ class RateLimitHandler:
29
+ """
30
+ Handles GitHub API rate limits with multiple strategies.
31
+
32
+ Usage:
33
+ handler = RateLimitHandler(
34
+ token=github_token,
35
+ interactive=True,
36
+ profile_name="personal"
37
+ )
38
+
39
+ # Before starting
40
+ handler.check_upfront()
41
+
42
+ # Around requests
43
+ response = requests.get(url, headers=headers)
44
+ handler.check_response(response)
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ token: str | None = None,
50
+ interactive: bool = True,
51
+ profile_name: str | None = None,
52
+ auto_switch: bool = True,
53
+ ):
54
+ """
55
+ Initialize rate limit handler.
56
+
57
+ Args:
58
+ token: GitHub token (or None for unauthenticated)
59
+ interactive: Whether to show prompts (False for CI/CD)
60
+ profile_name: Name of the profile being used
61
+ auto_switch: Whether to auto-switch profiles when rate limited
62
+ """
63
+ self.token = token
64
+ self.interactive = interactive
65
+ self.profile_name = profile_name
66
+ self.config = get_config_manager()
67
+
68
+ # Get settings from config
69
+ self.auto_switch = auto_switch and self.config.config["rate_limit"]["auto_switch_profiles"]
70
+ self.show_countdown = self.config.config["rate_limit"]["show_countdown"]
71
+ self.default_timeout = self.config.config["rate_limit"]["default_timeout_minutes"]
72
+
73
+ # Get profile-specific settings if available
74
+ if token:
75
+ self.strategy = self.config.get_rate_limit_strategy(token)
76
+ self.timeout_minutes = self.config.get_timeout_minutes(token)
77
+ else:
78
+ self.strategy = "prompt"
79
+ self.timeout_minutes = self.default_timeout
80
+
81
+ def check_upfront(self) -> bool:
82
+ """
83
+ Check rate limit status before starting.
84
+ Shows non-intrusive warning if no token configured.
85
+
86
+ Returns:
87
+ True if check passed, False if should abort
88
+ """
89
+ if not self.token:
90
+ print("\n๐Ÿ’ก Tip: GitHub API limit is 60 requests/hour without a token.")
91
+ print(" Set up a GitHub token for 5000 requests/hour:")
92
+ print(" $ skill-seekers config --github")
93
+ print()
94
+
95
+ if self.interactive:
96
+ response = input("Continue without token? [Y/n]: ").strip().lower()
97
+ if response in ["n", "no"]:
98
+ print("\nโœ… Run 'skill-seekers config --github' to set up a token.\n")
99
+ return False
100
+
101
+ return True
102
+
103
+ # Check current rate limit status
104
+ try:
105
+ rate_info = self.get_rate_limit_info()
106
+ remaining = rate_info.get("remaining", 0)
107
+ limit = rate_info.get("limit", 5000)
108
+
109
+ if remaining == 0:
110
+ print(f"\nโš ๏ธ Warning: GitHub rate limit already exhausted (0/{limit})")
111
+ reset_time = rate_info.get("reset_time")
112
+ if reset_time:
113
+ wait_minutes = (reset_time - datetime.now()).total_seconds() / 60
114
+ print(f" Resets in {int(wait_minutes)} minutes")
115
+
116
+ if self.interactive:
117
+ return self.handle_rate_limit(rate_info)
118
+ else:
119
+ print("\nโŒ Cannot proceed: Rate limit exhausted (non-interactive mode)\n")
120
+ return False
121
+
122
+ # Show friendly status
123
+ if remaining < 100:
124
+ print(f"โš ๏ธ GitHub API: {remaining}/{limit} requests remaining")
125
+ else:
126
+ print(f"โœ… GitHub API: {remaining}/{limit} requests available")
127
+
128
+ return True
129
+
130
+ except Exception as e:
131
+ print(f"โš ๏ธ Could not check rate limit status: {e}")
132
+ print(" Proceeding anyway...")
133
+ return True
134
+
135
+ def check_response(self, response: requests.Response) -> bool:
136
+ """
137
+ Check if response indicates rate limit and handle it.
138
+
139
+ Args:
140
+ response: requests.Response object
141
+
142
+ Returns:
143
+ True if handled successfully, False if should abort
144
+
145
+ Raises:
146
+ RateLimitError: If rate limit cannot be handled
147
+ """
148
+ # Check for rate limit (403 with specific message)
149
+ if response.status_code == 403:
150
+ try:
151
+ error_data = response.json()
152
+ message = error_data.get("message", "")
153
+
154
+ if "rate limit" in message.lower() or "api rate limit exceeded" in message.lower():
155
+ # Extract rate limit info from headers
156
+ rate_info = self.extract_rate_limit_info(response)
157
+ return self.handle_rate_limit(rate_info)
158
+
159
+ except Exception:
160
+ pass # Not a rate limit error
161
+
162
+ return True
163
+
164
+ def extract_rate_limit_info(self, response: requests.Response) -> dict[str, Any]:
165
+ """
166
+ Extract rate limit information from response headers.
167
+
168
+ Args:
169
+ response: requests.Response with rate limit headers
170
+
171
+ Returns:
172
+ Dict with rate limit info
173
+ """
174
+ headers = response.headers
175
+
176
+ limit = int(headers.get("X-RateLimit-Limit", 0))
177
+ remaining = int(headers.get("X-RateLimit-Remaining", 0))
178
+ reset_timestamp = int(headers.get("X-RateLimit-Reset", 0))
179
+
180
+ reset_time = datetime.fromtimestamp(reset_timestamp) if reset_timestamp else None
181
+
182
+ return {
183
+ "limit": limit,
184
+ "remaining": remaining,
185
+ "reset_timestamp": reset_timestamp,
186
+ "reset_time": reset_time,
187
+ }
188
+
189
+ def get_rate_limit_info(self) -> dict[str, Any]:
190
+ """
191
+ Get current rate limit status from GitHub API.
192
+
193
+ Returns:
194
+ Dict with rate limit info
195
+ """
196
+ url = "https://api.github.com/rate_limit"
197
+ headers = {}
198
+ if self.token:
199
+ headers["Authorization"] = f"token {self.token}"
200
+
201
+ response = requests.get(url, headers=headers, timeout=5)
202
+ response.raise_for_status()
203
+
204
+ data = response.json()
205
+ core = data.get("rate", {})
206
+
207
+ reset_timestamp = core.get("reset", 0)
208
+ reset_time = datetime.fromtimestamp(reset_timestamp) if reset_timestamp else None
209
+
210
+ return {
211
+ "limit": core.get("limit", 0),
212
+ "remaining": core.get("remaining", 0),
213
+ "reset_timestamp": reset_timestamp,
214
+ "reset_time": reset_time,
215
+ }
216
+
217
+ def handle_rate_limit(self, rate_info: dict[str, Any]) -> bool:
218
+ """
219
+ Handle rate limit based on strategy.
220
+
221
+ Args:
222
+ rate_info: Dict with rate limit information
223
+
224
+ Returns:
225
+ True if handled (can continue), False if should abort
226
+
227
+ Raises:
228
+ RateLimitError: If cannot handle in non-interactive mode
229
+ """
230
+ reset_time = rate_info.get("reset_time")
231
+ remaining = rate_info.get("remaining", 0)
232
+ limit = rate_info.get("limit", 0)
233
+
234
+ print("\nโš ๏ธ GitHub Rate Limit Reached")
235
+ print(f" Profile: {self.profile_name or 'default'}")
236
+ print(f" Limit: {remaining}/{limit} requests")
237
+
238
+ if reset_time:
239
+ wait_seconds = (reset_time - datetime.now()).total_seconds()
240
+ wait_minutes = int(wait_seconds / 60)
241
+ print(f" Resets at: {reset_time.strftime('%H:%M:%S')} ({wait_minutes} minutes)")
242
+ else:
243
+ wait_seconds = 0
244
+ wait_minutes = 0
245
+
246
+ print()
247
+
248
+ # Strategy-based handling
249
+ if self.strategy == "fail":
250
+ print("โŒ Strategy: fail - Aborting immediately")
251
+ if not self.interactive:
252
+ raise RateLimitError("Rate limit exceeded (fail strategy)")
253
+ return False
254
+
255
+ if self.strategy == "switch" and self.auto_switch:
256
+ # Try switching to another profile
257
+ new_profile = self.try_switch_profile()
258
+ if new_profile:
259
+ return True
260
+ else:
261
+ print("โš ๏ธ No alternative profiles available")
262
+ # Fall through to other strategies
263
+
264
+ if self.strategy == "wait":
265
+ # Auto-wait with countdown
266
+ return self.wait_for_reset(wait_seconds, wait_minutes)
267
+
268
+ # Default: prompt user (if interactive)
269
+ if self.interactive:
270
+ return self.prompt_user_action(wait_seconds, wait_minutes)
271
+ else:
272
+ # Non-interactive mode: fail
273
+ raise RateLimitError("Rate limit exceeded (non-interactive mode)")
274
+
275
+ def try_switch_profile(self) -> bool:
276
+ """
277
+ Try to switch to another GitHub profile.
278
+
279
+ Returns:
280
+ True if switched successfully, False otherwise
281
+ """
282
+ if not self.token:
283
+ return False
284
+
285
+ next_profile_data = self.config.get_next_profile(self.token)
286
+
287
+ if not next_profile_data:
288
+ return False
289
+
290
+ next_name, next_token = next_profile_data
291
+
292
+ print(f"๐Ÿ”„ Switching to profile: {next_name}")
293
+
294
+ # Check if new profile has quota
295
+ try:
296
+ old_token = self.token
297
+ self.token = next_token
298
+
299
+ rate_info = self.get_rate_limit_info()
300
+ remaining = rate_info.get("remaining", 0)
301
+ limit = rate_info.get("limit", 0)
302
+
303
+ if remaining > 0:
304
+ print(f"โœ… Profile '{next_name}' has {remaining}/{limit} requests available")
305
+ self.profile_name = next_name
306
+ return True
307
+ else:
308
+ print(f"โš ๏ธ Profile '{next_name}' also exhausted ({remaining}/{limit})")
309
+ self.token = old_token # Restore old token
310
+ return False
311
+
312
+ except Exception as e:
313
+ print(f"โŒ Failed to switch profiles: {e}")
314
+ self.token = old_token # Restore old token
315
+ return False
316
+
317
+ def wait_for_reset(self, wait_seconds: float, wait_minutes: int) -> bool:
318
+ """
319
+ Wait for rate limit to reset with countdown.
320
+
321
+ Args:
322
+ wait_seconds: Seconds to wait
323
+ wait_minutes: Minutes to wait (for display)
324
+
325
+ Returns:
326
+ True if waited successfully, False if aborted
327
+ """
328
+ # Check timeout
329
+ if wait_minutes > self.timeout_minutes:
330
+ print(f"โš ๏ธ Wait time ({wait_minutes}m) exceeds timeout ({self.timeout_minutes}m)")
331
+ return False
332
+
333
+ if wait_seconds <= 0:
334
+ print("โœ… Rate limit should be reset now")
335
+ return True
336
+
337
+ print(f"โณ Waiting {wait_minutes} minutes for rate limit reset...")
338
+ print(" Press Ctrl+C to cancel\n")
339
+
340
+ try:
341
+ if self.show_countdown:
342
+ self.show_countdown_timer(wait_seconds)
343
+ else:
344
+ time.sleep(wait_seconds)
345
+
346
+ print("\nโœ… Rate limit reset! Continuing...\n")
347
+ return True
348
+
349
+ except KeyboardInterrupt:
350
+ print("\n\nโธ๏ธ Wait interrupted by user")
351
+ return False
352
+
353
+ def show_countdown_timer(self, total_seconds: float):
354
+ """
355
+ Show a live countdown timer.
356
+
357
+ Args:
358
+ total_seconds: Total seconds to count down
359
+ """
360
+ end_time = time.time() + total_seconds
361
+
362
+ while time.time() < end_time:
363
+ remaining = int(end_time - time.time())
364
+ minutes, seconds = divmod(remaining, 60)
365
+
366
+ # Print countdown on same line
367
+ sys.stdout.write(f"\rโฑ๏ธ Resuming in {minutes:02d}:{seconds:02d}...")
368
+ sys.stdout.flush()
369
+
370
+ time.sleep(1)
371
+
372
+ sys.stdout.write("\r" + " " * 50 + "\r") # Clear line
373
+ sys.stdout.flush()
374
+
375
+ def prompt_user_action(self, wait_seconds: float, wait_minutes: int) -> bool:
376
+ """
377
+ Prompt user for action when rate limited.
378
+
379
+ Args:
380
+ wait_seconds: Seconds until reset
381
+ wait_minutes: Minutes until reset
382
+
383
+ Returns:
384
+ True if user chooses to continue, False to abort
385
+ """
386
+ print("Options:")
387
+ print(f" [w] Wait {wait_minutes} minutes (auto-continues)")
388
+
389
+ # Check if profile switching is available
390
+ if self.token and self.config.get_next_profile(self.token):
391
+ print(" [s] Switch to another GitHub profile")
392
+
393
+ print(" [t] Set up new GitHub token")
394
+ print(" [c] Cancel")
395
+ print()
396
+
397
+ while True:
398
+ choice = input("Select an option [w/s/t/c]: ").strip().lower()
399
+
400
+ if choice == "w":
401
+ return self.wait_for_reset(wait_seconds, wait_minutes)
402
+
403
+ elif choice == "s":
404
+ if self.try_switch_profile():
405
+ return True
406
+ else:
407
+ print("โš ๏ธ Profile switching failed. Choose another option.")
408
+ continue
409
+
410
+ elif choice == "t":
411
+ print("\n๐Ÿ’ก Opening GitHub token setup...")
412
+ print(" Run this command in another terminal:")
413
+ print(" $ skill-seekers config --github\n")
414
+ print(" Then restart your scraping job.\n")
415
+ return False
416
+
417
+ elif choice == "c":
418
+ print("\nโธ๏ธ Operation cancelled by user\n")
419
+ return False
420
+
421
+ else:
422
+ print("โŒ Invalid choice. Please enter w, s, t, or c.")
423
+
424
+
425
+ def create_github_headers(token: str | None = None) -> dict[str, str]:
426
+ """
427
+ Create GitHub API headers with optional token.
428
+
429
+ Args:
430
+ token: GitHub token (or None)
431
+
432
+ Returns:
433
+ Dict of headers
434
+ """
435
+ headers = {}
436
+ if token:
437
+ headers["Authorization"] = f"token {token}"
438
+ return headers
@@ -0,0 +1,160 @@
1
+ """
2
+ Resume Command for Skill Seekers
3
+
4
+ Allows users to resume interrupted scraping jobs from saved progress.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+
10
+ from .config_manager import get_config_manager
11
+
12
+
13
+ def list_resumable_jobs():
14
+ """List all resumable jobs with details."""
15
+ config = get_config_manager()
16
+ jobs = config.list_resumable_jobs()
17
+
18
+ if not jobs:
19
+ print("\n๐Ÿ“ฆ No resumable jobs found.\n")
20
+ print("Jobs are automatically saved when:")
21
+ print(" โ€ข You interrupt a scraping operation (Ctrl+C)")
22
+ print(" โ€ข A rate limit is reached")
23
+ print(" โ€ข An error occurs during scraping\n")
24
+ return
25
+
26
+ print(f"\n๐Ÿ“ฆ Resumable Jobs ({len(jobs)} available):\n")
27
+
28
+ for idx, job in enumerate(jobs, 1):
29
+ job_id = job["job_id"]
30
+ started = job.get("started_at", "Unknown")
31
+ command = job.get("command", "Unknown")
32
+ progress = job.get("progress", {})
33
+ last_updated = job.get("last_updated", "Unknown")
34
+
35
+ print(f"{idx}. Job ID: {job_id}")
36
+ print(f" Started: {started}")
37
+ print(f" Command: {command}")
38
+
39
+ if progress:
40
+ phase = progress.get("phase", "Unknown")
41
+ files_processed = progress.get("files_processed", 0)
42
+ files_total = progress.get("files_total", 0)
43
+
44
+ print(f" Progress: {phase}")
45
+ if files_total > 0:
46
+ percentage = (files_processed / files_total) * 100
47
+ print(f" Files: {files_processed}/{files_total} ({percentage:.1f}%)")
48
+
49
+ print(f" Last updated: {last_updated}")
50
+ print()
51
+
52
+ print("To resume a job:")
53
+ print(" $ skill-seekers resume <job_id>\n")
54
+
55
+
56
+ def resume_job(job_id: str):
57
+ """Resume a specific job."""
58
+ config = get_config_manager()
59
+
60
+ print(f"\n๐Ÿ”„ Resuming job: {job_id}\n")
61
+
62
+ # Load progress
63
+ progress = config.load_progress(job_id)
64
+
65
+ if not progress:
66
+ print(f"โŒ Job '{job_id}' not found or cannot be resumed.\n")
67
+ print("Use 'skill-seekers resume --list' to see available jobs.\n")
68
+ return 1
69
+
70
+ if not progress.get("can_resume", False):
71
+ print(f"โŒ Job '{job_id}' is not marked as resumable.\n")
72
+ return 1
73
+
74
+ # Extract job details
75
+ command = progress.get("command", "")
76
+ _job_config = progress.get("config", {})
77
+ checkpoint = progress.get("progress", {}).get("last_checkpoint")
78
+
79
+ print(f"Original command: {command}")
80
+ print(f"Last checkpoint: {checkpoint or 'Unknown'}")
81
+ print()
82
+
83
+ # Reconstruct command
84
+ if "github" in command:
85
+ print("๐Ÿ“Œ Resuming GitHub scraping...")
86
+ print("โš ๏ธ Note: GitHub resume feature not yet implemented")
87
+ print(" You can re-run the original command - it will use cached data where available.\n")
88
+ print(f" Command: {command}\n")
89
+ return 1
90
+
91
+ elif "scrape" in command:
92
+ print("๐Ÿ“Œ Resuming documentation scraping...")
93
+ print("โš ๏ธ Note: Documentation scraping resume feature not yet implemented")
94
+ print(" You can re-run the original command - it will use cached data where available.\n")
95
+ print(f" Command: {command}\n")
96
+ return 1
97
+
98
+ elif "unified" in command:
99
+ print("๐Ÿ“Œ Resuming unified scraping...")
100
+ print("โš ๏ธ Note: Unified scraping resume feature not yet implemented")
101
+ print(" You can re-run the original command - it will use cached data where available.\n")
102
+ print(f" Command: {command}\n")
103
+ return 1
104
+
105
+ else:
106
+ print("โŒ Unknown job type. Cannot resume.\n")
107
+ return 1
108
+
109
+
110
+ def clean_old_jobs():
111
+ """Clean up old progress files."""
112
+ config = get_config_manager()
113
+
114
+ print("\n๐Ÿงน Cleaning up old progress files...\n")
115
+
116
+ jobs_before = len(config.list_resumable_jobs())
117
+ config.cleanup_old_progress()
118
+ jobs_after = len(config.list_resumable_jobs())
119
+
120
+ deleted = jobs_before - jobs_after
121
+
122
+ if deleted > 0:
123
+ print(f"โœ… Deleted {deleted} old job(s)")
124
+ else:
125
+ print("โœ… No old jobs to clean up")
126
+
127
+ if jobs_after > 0:
128
+ print(f"๐Ÿ“ฆ {jobs_after} job(s) remaining\n")
129
+ else:
130
+ print()
131
+
132
+
133
+ def main():
134
+ """Main entry point for resume command."""
135
+ parser = argparse.ArgumentParser(description="Resume interrupted Skill Seekers jobs")
136
+ parser.add_argument("job_id", nargs="?", help="Job ID to resume")
137
+ parser.add_argument("--list", action="store_true", help="List all resumable jobs")
138
+ parser.add_argument("--clean", action="store_true", help="Clean up old progress files")
139
+
140
+ args = parser.parse_args()
141
+
142
+ # Handle options
143
+ if args.list:
144
+ list_resumable_jobs()
145
+ return 0
146
+
147
+ if args.clean:
148
+ clean_old_jobs()
149
+ return 0
150
+
151
+ if not args.job_id:
152
+ print("\nโŒ Error: Job ID required or use --list to see available jobs\n")
153
+ parser.print_help()
154
+ return 1
155
+
156
+ return resume_job(args.job_id)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ sys.exit(main())