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,1096 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SKILL.md Enhancement Script (Local - Using Claude Code)
4
+ Opens a new terminal with Claude Code to enhance SKILL.md, then reports back.
5
+ No API key needed - uses your existing Claude Code Max plan!
6
+
7
+ Usage:
8
+ # Headless mode (default - runs in foreground, waits for completion)
9
+ skill-seekers enhance output/react/
10
+
11
+ # Background mode (runs in background, returns immediately)
12
+ skill-seekers enhance output/react/ --background
13
+
14
+ # Force mode (no confirmations, auto-yes to everything)
15
+ skill-seekers enhance output/react/ --force
16
+
17
+ # Daemon mode (persistent background process)
18
+ skill-seekers enhance output/react/ --daemon
19
+
20
+ # Interactive terminal mode
21
+ skill-seekers enhance output/react/ --interactive-enhancement
22
+
23
+ Modes:
24
+ - headless: Runs claude CLI directly, BLOCKS until done (default)
25
+ - background: Runs claude CLI in background, returns immediately
26
+ - daemon: Runs as persistent background process with monitoring
27
+ - terminal: Opens new terminal window (interactive)
28
+
29
+ Terminal Selection:
30
+ The script automatically detects which terminal app to use:
31
+ 1. SKILL_SEEKER_TERMINAL env var (highest priority)
32
+ Example: export SKILL_SEEKER_TERMINAL="Ghostty"
33
+ 2. TERM_PROGRAM env var (current terminal)
34
+ 3. Terminal.app (fallback)
35
+
36
+ Supported terminals: Ghostty, iTerm, Terminal, WezTerm
37
+ """
38
+
39
+ import json
40
+ import os
41
+ import subprocess
42
+ import sys
43
+ import tempfile
44
+ import threading
45
+ import time
46
+ from datetime import datetime
47
+ from pathlib import Path
48
+
49
+ # Add parent directory to path for imports when run as script
50
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
51
+
52
+ import contextlib
53
+
54
+ from skill_seekers.cli.constants import LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT
55
+ from skill_seekers.cli.utils import read_reference_files
56
+
57
+
58
+ def detect_terminal_app():
59
+ """Detect which terminal app to use with cascading priority.
60
+
61
+ Priority order:
62
+ 1. SKILL_SEEKER_TERMINAL environment variable (explicit user preference)
63
+ 2. TERM_PROGRAM environment variable (inherit current terminal)
64
+ 3. Terminal.app (fallback default)
65
+
66
+ Returns:
67
+ tuple: (terminal_app_name, detection_method)
68
+ - terminal_app_name (str): Name of terminal app to launch (e.g., "Ghostty", "Terminal")
69
+ - detection_method (str): How the terminal was detected (for logging)
70
+
71
+ Examples:
72
+ >>> os.environ['SKILL_SEEKER_TERMINAL'] = 'Ghostty'
73
+ >>> detect_terminal_app()
74
+ ('Ghostty', 'SKILL_SEEKER_TERMINAL')
75
+
76
+ >>> os.environ['TERM_PROGRAM'] = 'iTerm.app'
77
+ >>> detect_terminal_app()
78
+ ('iTerm', 'TERM_PROGRAM')
79
+ """
80
+ # Map TERM_PROGRAM values to macOS app names
81
+ TERMINAL_MAP = {
82
+ "Apple_Terminal": "Terminal",
83
+ "iTerm.app": "iTerm",
84
+ "ghostty": "Ghostty",
85
+ "WezTerm": "WezTerm",
86
+ }
87
+
88
+ # Priority 1: Check SKILL_SEEKER_TERMINAL env var (explicit preference)
89
+ preferred_terminal = os.environ.get("SKILL_SEEKER_TERMINAL", "").strip()
90
+ if preferred_terminal:
91
+ return preferred_terminal, "SKILL_SEEKER_TERMINAL"
92
+
93
+ # Priority 2: Check TERM_PROGRAM (inherit current terminal)
94
+ term_program = os.environ.get("TERM_PROGRAM", "").strip()
95
+ if term_program and term_program in TERMINAL_MAP:
96
+ return TERMINAL_MAP[term_program], "TERM_PROGRAM"
97
+
98
+ # Priority 3: Fallback to Terminal.app
99
+ if term_program:
100
+ # TERM_PROGRAM is set but unknown
101
+ return "Terminal", f"unknown TERM_PROGRAM ({term_program})"
102
+ else:
103
+ # No TERM_PROGRAM set
104
+ return "Terminal", "default"
105
+
106
+
107
+ class LocalSkillEnhancer:
108
+ def __init__(self, skill_dir, force=True):
109
+ """Initialize enhancer.
110
+
111
+ Args:
112
+ skill_dir: Path to skill directory
113
+ force: If True, skip all confirmations (default: True, use --no-force to disable)
114
+ """
115
+ self.skill_dir = Path(skill_dir)
116
+ self.references_dir = self.skill_dir / "references"
117
+ self.skill_md_path = self.skill_dir / "SKILL.md"
118
+ self.force = force
119
+ self.status_file = self.skill_dir / ".enhancement_status.json"
120
+
121
+ def summarize_reference(self, content: str, target_ratio: float = 0.3) -> str:
122
+ """Intelligently summarize reference content to reduce size.
123
+
124
+ Strategy:
125
+ 1. Keep first 20% (introduction/overview)
126
+ 2. Extract code blocks (prioritize examples)
127
+ 3. Keep headings and their first paragraph
128
+ 4. Skip repetitive content
129
+
130
+ Args:
131
+ content: Full reference content
132
+ target_ratio: Target size as ratio of original (0.3 = 30%)
133
+
134
+ Returns:
135
+ Summarized content
136
+ """
137
+ lines = content.split("\n")
138
+ _target_lines = int(len(lines) * target_ratio)
139
+
140
+ # Priority 1: Keep introduction (first 20%)
141
+ intro_lines = int(len(lines) * 0.2)
142
+ result_lines = lines[:intro_lines]
143
+
144
+ # Priority 2: Extract code blocks
145
+ in_code_block = False
146
+ code_blocks = []
147
+ current_block = []
148
+ block_start_idx = 0
149
+
150
+ for i, line in enumerate(lines[intro_lines:], start=intro_lines):
151
+ if line.strip().startswith("```"):
152
+ if in_code_block:
153
+ # End of code block - add closing ``` and save
154
+ current_block.append(line)
155
+ code_blocks.append((block_start_idx, current_block))
156
+ current_block = []
157
+ in_code_block = False
158
+ else:
159
+ # Start of code block
160
+ in_code_block = True
161
+ block_start_idx = i
162
+ current_block = [line]
163
+ elif in_code_block:
164
+ current_block.append(line)
165
+
166
+ # Combine: intro + code blocks + headings
167
+ result = result_lines.copy()
168
+
169
+ # Add code blocks first (prioritize code examples)
170
+ for _idx, block in code_blocks[:5]: # Max 5 code blocks
171
+ result.append("") # Add blank line before code block
172
+ result.extend(block)
173
+
174
+ # Priority 3: Keep headings with first paragraph
175
+ i = intro_lines
176
+ headings_added = 0
177
+ while i < len(lines) and headings_added < 10:
178
+ line = lines[i]
179
+ if line.startswith("#"):
180
+ # Found heading - keep it and next 3 lines
181
+ chunk = lines[i : min(i + 4, len(lines))]
182
+ result.extend(chunk)
183
+ headings_added += 1
184
+ i += 4
185
+ else:
186
+ i += 1
187
+
188
+ result.append("\n\n[Content intelligently summarized - full details in reference files]")
189
+
190
+ return "\n".join(result)
191
+
192
+ def create_enhancement_prompt(self, use_summarization=False, summarization_ratio=0.3):
193
+ """Create the prompt file for Claude Code
194
+
195
+ Args:
196
+ use_summarization: If True, apply smart summarization to reduce size
197
+ summarization_ratio: Target size ratio when summarizing (0.3 = 30%)
198
+ """
199
+
200
+ # Read reference files (with enriched metadata)
201
+ references = read_reference_files(
202
+ self.skill_dir, max_chars=LOCAL_CONTENT_LIMIT, preview_limit=LOCAL_PREVIEW_LIMIT
203
+ )
204
+
205
+ if not references:
206
+ print("❌ No reference files found")
207
+ return None
208
+
209
+ # Analyze sources
210
+ sources_found = set()
211
+ for metadata in references.values():
212
+ sources_found.add(metadata["source"])
213
+
214
+ # Calculate total size
215
+ total_ref_size = sum(meta["size"] for meta in references.values())
216
+
217
+ # Apply summarization if requested or if content is too large
218
+ if use_summarization or total_ref_size > 30000:
219
+ if not use_summarization:
220
+ print(f" ⚠️ Large skill detected ({total_ref_size:,} chars)")
221
+ print(
222
+ f" 📊 Applying smart summarization (target: {int(summarization_ratio * 100)}% of original)"
223
+ )
224
+ print()
225
+
226
+ # Summarize each reference
227
+ for _filename, metadata in references.items():
228
+ summarized = self.summarize_reference(metadata["content"], summarization_ratio)
229
+ metadata["content"] = summarized
230
+ metadata["size"] = len(summarized)
231
+
232
+ new_size = sum(meta["size"] for meta in references.values())
233
+ print(
234
+ f" ✓ Reduced from {total_ref_size:,} to {new_size:,} chars ({int(new_size / total_ref_size * 100)}%)"
235
+ )
236
+ print()
237
+
238
+ # Read current SKILL.md
239
+ current_skill_md = ""
240
+ if self.skill_md_path.exists():
241
+ current_skill_md = self.skill_md_path.read_text(encoding="utf-8")
242
+
243
+ # Analyze conflicts if present
244
+ has_conflicts = any("conflicts" in meta["path"] for meta in references.values())
245
+
246
+ # Build prompt with multi-source awareness
247
+ prompt = f"""I need you to enhance the SKILL.md file for the {self.skill_dir.name} skill.
248
+
249
+ SKILL OVERVIEW:
250
+ - Name: {self.skill_dir.name}
251
+ - Source Types: {", ".join(sorted(sources_found))}
252
+ - Multi-Source: {"Yes" if len(sources_found) > 1 else "No"}
253
+ - Conflicts Detected: {"Yes - see conflicts.md in references" if has_conflicts else "No"}
254
+
255
+ CURRENT SKILL.MD:
256
+ {"-" * 60}
257
+ {current_skill_md if current_skill_md else "(No existing SKILL.md - create from scratch)"}
258
+ {"-" * 60}
259
+
260
+ SOURCE ANALYSIS:
261
+ {"-" * 60}
262
+ This skill combines knowledge from {len(sources_found)} source type(s):
263
+
264
+ """
265
+
266
+ # Group references by (source_type, repo_id) for multi-source support
267
+ by_source = {}
268
+ for filename, metadata in references.items():
269
+ source = metadata["source"]
270
+ repo_id = metadata.get("repo_id") # None for single-source
271
+ key = (source, repo_id) if repo_id else (source, None)
272
+
273
+ if key not in by_source:
274
+ by_source[key] = []
275
+ by_source[key].append((filename, metadata))
276
+
277
+ # Add source breakdown with repo identity
278
+ for source, repo_id in sorted(by_source.keys()):
279
+ files = by_source[(source, repo_id)]
280
+ if repo_id:
281
+ prompt += f"\n**{source.upper()} - {repo_id} ({len(files)} file(s))**\n"
282
+ else:
283
+ prompt += f"\n**{source.upper()} ({len(files)} file(s))**\n"
284
+ for filename, metadata in files[:5]: # Top 5 per source
285
+ prompt += f"- {filename} (confidence: {metadata['confidence']}, {metadata['size']:,} chars)\n"
286
+ if len(files) > 5:
287
+ prompt += f"- ... and {len(files) - 5} more\n"
288
+
289
+ prompt += f"""
290
+ {"-" * 60}
291
+
292
+ REFERENCE DOCUMENTATION:
293
+ {"-" * 60}
294
+ """
295
+
296
+ # Add references grouped by (source, repo_id) with metadata
297
+ for source, repo_id in sorted(by_source.keys()):
298
+ if repo_id:
299
+ prompt += f"\n### {source.upper()} SOURCES - {repo_id}\n\n"
300
+ else:
301
+ prompt += f"\n### {source.upper()} SOURCES\n\n"
302
+
303
+ for filename, metadata in by_source[(source, repo_id)]:
304
+ # Further limit per-file to 12K to be safe
305
+ content = metadata["content"]
306
+ max_per_file = 12000
307
+ if len(content) > max_per_file:
308
+ content = content[:max_per_file] + "\n\n[Content truncated for size...]"
309
+
310
+ prompt += f"\n#### {filename}\n"
311
+ if repo_id:
312
+ prompt += f"*Source: {metadata['source']} ({repo_id}), Confidence: {metadata['confidence']}*\n\n"
313
+ else:
314
+ prompt += (
315
+ f"*Source: {metadata['source']}, Confidence: {metadata['confidence']}*\n\n"
316
+ )
317
+ prompt += f"{content}\n"
318
+
319
+ prompt += f"""
320
+ {"-" * 60}
321
+
322
+ REFERENCE PRIORITY (when sources differ):
323
+ 1. **Code patterns (codebase_analysis)**: Ground truth - what the code actually does
324
+ 2. **Official documentation**: Intended API and usage patterns
325
+ 3. **GitHub issues**: Real-world usage and known problems
326
+ 4. **PDF documentation**: Additional context and tutorials
327
+
328
+ MULTI-REPOSITORY HANDLING:
329
+ """
330
+
331
+ # Detect multiple repos from same source type
332
+ repo_ids = set()
333
+ for metadata in references.values():
334
+ if metadata.get("repo_id"):
335
+ repo_ids.add(metadata["repo_id"])
336
+
337
+ if len(repo_ids) > 1:
338
+ prompt += f"""
339
+ ⚠️ MULTIPLE REPOSITORIES DETECTED: {", ".join(sorted(repo_ids))}
340
+
341
+ This skill combines codebase analysis from {len(repo_ids)} different repositories.
342
+ Each repo has its own ARCHITECTURE.md, patterns, examples, and configuration.
343
+
344
+ When synthesizing:
345
+ - Clearly identify which content comes from which repo
346
+ - Compare and contrast patterns across repos (e.g., "httpx uses Strategy pattern 50 times, httpcore uses it 32 times")
347
+ - Highlight relationships (e.g., "httpx is a client library built on top of httpcore")
348
+ - Present examples from BOTH repos to show different use cases
349
+ - If repos serve different purposes, explain when to use each
350
+ """
351
+ else:
352
+ prompt += "\nSingle repository - standard synthesis applies.\n"
353
+
354
+ prompt += """
355
+
356
+ YOUR TASK:
357
+ Create an EXCELLENT SKILL.md file that synthesizes knowledge from multiple sources.
358
+
359
+ Requirements:
360
+ 1. **Multi-Source Synthesis**
361
+ - Acknowledge that this skill combines multiple sources
362
+ - Highlight agreements between sources (builds confidence)
363
+ - Note discrepancies transparently (if present)
364
+ - Use source priority when synthesizing conflicting information
365
+
366
+ 2. **Clear "When to Use This Skill" section**
367
+ - Be SPECIFIC about trigger conditions
368
+ - List concrete use cases
369
+ - Include perspective from both docs AND real-world usage (if GitHub/codebase data available)
370
+
371
+ 3. **Excellent Quick Reference section**
372
+ - Extract 5-10 of the BEST, most practical code examples
373
+ - Prefer examples from HIGH CONFIDENCE sources first
374
+ - If code examples exist from codebase analysis, prioritize those (real usage)
375
+ - If docs examples exist, include those too (official patterns)
376
+ - Choose SHORT, clear examples (5-20 lines max)
377
+ - Use proper language tags (cpp, python, javascript, json, etc.)
378
+ - Add clear descriptions noting the source (e.g., "From official docs" or "From codebase")
379
+
380
+ 4. **Detailed Reference Files description**
381
+ - Explain what's in each reference file
382
+ - Note the source type and confidence level
383
+ - Help users navigate multi-source documentation
384
+
385
+ 5. **Practical "Working with This Skill" section**
386
+ - Clear guidance for beginners, intermediate, and advanced users
387
+ - Navigation tips for multi-source references
388
+ - How to resolve conflicts if present
389
+
390
+ 6. **Key Concepts section** (if applicable)
391
+ - Explain core concepts
392
+ - Define important terminology
393
+ - Reconcile differences between sources if needed
394
+
395
+ 7. **Conflict Handling** (if conflicts detected)
396
+ - Add a "Known Discrepancies" section
397
+ - Explain major conflicts transparently
398
+ - Provide guidance on which source to trust in each case
399
+
400
+ IMPORTANT:
401
+ - Extract REAL examples from the reference docs above
402
+ - Prioritize HIGH CONFIDENCE sources when synthesizing
403
+ - Note source attribution when helpful (e.g., "Official docs say X, but codebase shows Y")
404
+ - Make discrepancies transparent, not hidden
405
+ - Prioritize SHORT, clear examples
406
+ - Make it actionable and practical
407
+ - Keep the frontmatter (---\\nname: ...\\n---) intact
408
+ - Use proper markdown formatting
409
+
410
+ SAVE THE RESULT:
411
+ You MUST save the complete enhanced SKILL.md file.
412
+
413
+ CRITICAL INSTRUCTIONS:
414
+ 1. First, create a backup: Write the current SKILL.md content to SKILL.md.backup
415
+ 2. Then, write the enhanced content to: SKILL.md
416
+
417
+ This is NOT a read-only task - you have permission to modify SKILL.md.
418
+ Even if running from within another Claude Code session, this modification is ALLOWED and EXPECTED.
419
+
420
+ VERIFICATION:
421
+ After writing, the file SKILL.md should:
422
+ - Exist in the current directory
423
+ - Be larger than the original (200-1000+ lines)
424
+ - Contain all the enhancements from the references above
425
+ """
426
+
427
+ return prompt
428
+
429
+ def write_status(self, status, message="", progress=0.0, error=None):
430
+ """Write enhancement status to file for monitoring.
431
+
432
+ Args:
433
+ status: One of: pending, running, completed, failed
434
+ message: Status message
435
+ progress: Progress percentage (0.0-1.0)
436
+ error: Error message if failed
437
+ """
438
+ status_data = {
439
+ "status": status,
440
+ "message": message,
441
+ "progress": progress,
442
+ "timestamp": datetime.now().isoformat(),
443
+ "skill_dir": str(self.skill_dir),
444
+ "error": error,
445
+ }
446
+
447
+ self.status_file.write_text(json.dumps(status_data, indent=2), encoding="utf-8")
448
+
449
+ def read_status(self):
450
+ """Read enhancement status from file.
451
+
452
+ Returns:
453
+ dict: Status data or None if not found
454
+ """
455
+ if not self.status_file.exists():
456
+ return None
457
+
458
+ try:
459
+ return json.loads(self.status_file.read_text(encoding="utf-8"))
460
+ except Exception:
461
+ return None
462
+
463
+ def run(self, headless=True, timeout=600, background=False, daemon=False):
464
+ """Main enhancement workflow with automatic smart summarization for large skills.
465
+
466
+ Automatically detects large skills (>30K chars) and applies smart summarization
467
+ to ensure compatibility with Claude CLI's ~30-40K character limit.
468
+
469
+ Smart summarization strategy:
470
+ - Keeps first 20% (introduction/overview)
471
+ - Extracts up to 5 best code blocks
472
+ - Keeps up to 10 section headings with first paragraph
473
+ - Reduces to ~30% of original size
474
+
475
+ Args:
476
+ headless: If True, run claude directly without opening terminal (default: True)
477
+ timeout: Maximum time to wait for enhancement in seconds (default: 600 = 10 minutes)
478
+ background: If True, run in background and return immediately (default: False)
479
+ daemon: If True, run as persistent daemon with monitoring (default: False)
480
+
481
+ Returns:
482
+ bool: True if enhancement process started successfully, False otherwise
483
+ """
484
+ # Background mode: Run in background thread, return immediately
485
+ if background:
486
+ return self._run_background(headless, timeout)
487
+
488
+ # Daemon mode: Run as persistent process with monitoring
489
+ if daemon:
490
+ return self._run_daemon(timeout)
491
+ print(f"\n{'=' * 60}")
492
+ print(f"LOCAL ENHANCEMENT: {self.skill_dir.name}")
493
+ print(f"{'=' * 60}\n")
494
+
495
+ # Validate
496
+ if not self.skill_dir.exists():
497
+ print(f"❌ Directory not found: {self.skill_dir}")
498
+ return False
499
+
500
+ # Read reference files
501
+ print("📖 Reading reference documentation...")
502
+ references = read_reference_files(
503
+ self.skill_dir, max_chars=LOCAL_CONTENT_LIMIT, preview_limit=LOCAL_PREVIEW_LIMIT
504
+ )
505
+
506
+ if not references:
507
+ print("❌ No reference files found to analyze")
508
+ return False
509
+
510
+ print(f" ✓ Read {len(references)} reference files")
511
+ total_size = sum(ref["size"] for ref in references.values())
512
+ print(f" ✓ Total size: {total_size:,} characters\n")
513
+
514
+ # Check if we need smart summarization
515
+ use_summarization = total_size > 30000
516
+
517
+ if use_summarization:
518
+ print("⚠️ LARGE SKILL DETECTED")
519
+ print(f" 📊 Reference content: {total_size:,} characters")
520
+ print(" 💡 Claude CLI limit: ~30,000-40,000 characters")
521
+ print()
522
+ print(" 🔧 Applying smart summarization to ensure success...")
523
+ print(" • Keeping introductions and overviews")
524
+ print(" • Extracting best code examples")
525
+ print(" • Preserving key concepts and headings")
526
+ print(" • Target: ~30% of original size")
527
+ print()
528
+
529
+ # Create prompt
530
+ print("📝 Creating enhancement prompt...")
531
+ prompt = self.create_enhancement_prompt(use_summarization=use_summarization)
532
+
533
+ if not prompt:
534
+ return False
535
+
536
+ # Save prompt to temp file
537
+ with tempfile.NamedTemporaryFile(
538
+ mode="w", suffix=".txt", delete=False, encoding="utf-8"
539
+ ) as f:
540
+ prompt_file = f.name
541
+ f.write(prompt)
542
+
543
+ if use_summarization:
544
+ print(f" ✓ Prompt created and optimized ({len(prompt):,} characters)")
545
+ print(" ✓ Ready for Claude CLI (within safe limits)")
546
+ print()
547
+ else:
548
+ print(f" ✓ Prompt saved ({len(prompt):,} characters)\n")
549
+
550
+ # Headless mode: Run claude directly without opening terminal
551
+ if headless:
552
+ return self._run_headless(prompt_file, timeout)
553
+
554
+ # Terminal mode: Launch Claude Code in new terminal
555
+ print("🚀 Launching Claude Code in new terminal...")
556
+ print(" This will:")
557
+ print(" 1. Open a new terminal window")
558
+ print(" 2. Run Claude Code with the enhancement task")
559
+ print(" 3. Claude will read the docs and enhance SKILL.md")
560
+ print(" 4. Terminal will auto-close when done")
561
+ print()
562
+
563
+ # Create a shell script to run in the terminal
564
+ shell_script = f"""#!/bin/bash
565
+ claude {prompt_file}
566
+ echo ""
567
+ echo "✅ Enhancement complete!"
568
+ echo "Press any key to close..."
569
+ read -n 1
570
+ rm {prompt_file}
571
+ """
572
+
573
+ # Save shell script
574
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f:
575
+ script_file = f.name
576
+ f.write(shell_script)
577
+
578
+ os.chmod(script_file, 0o755)
579
+
580
+ # Launch in new terminal (macOS specific)
581
+ if sys.platform == "darwin":
582
+ # Detect which terminal app to use
583
+ terminal_app, detection_method = detect_terminal_app()
584
+
585
+ # Show detection info
586
+ if detection_method == "SKILL_SEEKER_TERMINAL":
587
+ print(f" Using terminal: {terminal_app} (from SKILL_SEEKER_TERMINAL)")
588
+ elif detection_method == "TERM_PROGRAM":
589
+ print(f" Using terminal: {terminal_app} (inherited from current terminal)")
590
+ elif detection_method.startswith("unknown TERM_PROGRAM"):
591
+ print(f"⚠️ {detection_method}")
592
+ print(" → Using Terminal.app as fallback")
593
+ else:
594
+ print(f" Using terminal: {terminal_app} (default)")
595
+
596
+ try:
597
+ subprocess.Popen(["open", "-a", terminal_app, script_file])
598
+ except Exception as e:
599
+ print(f"⚠️ Error launching {terminal_app}: {e}")
600
+ print(f"\nManually run: {script_file}")
601
+ return False
602
+ else:
603
+ print("⚠️ Auto-launch only works on macOS")
604
+ print("\nManually run this command in a new terminal:")
605
+ print(f" claude '{prompt_file}'")
606
+ print("\nThen delete the prompt file:")
607
+ print(f" rm '{prompt_file}'")
608
+ return False
609
+
610
+ print("✅ New terminal launched with Claude Code!")
611
+ print()
612
+ print("📊 Status:")
613
+ print(f" - Prompt file: {prompt_file}")
614
+ print(f" - Skill directory: {self.skill_dir.absolute()}")
615
+ print(f" - SKILL.md will be saved to: {self.skill_md_path.absolute()}")
616
+ print(
617
+ f" - Original backed up to: {self.skill_md_path.with_suffix('.md.backup').absolute()}"
618
+ )
619
+ print()
620
+ print("⏳ Wait for Claude Code to finish in the other terminal...")
621
+ print(" (Usually takes 30-60 seconds)")
622
+ print()
623
+ print("💡 When done:")
624
+ print(f" 1. Check the enhanced SKILL.md: {self.skill_md_path}")
625
+ print(
626
+ f" 2. If you don't like it, restore: mv {self.skill_md_path.with_suffix('.md.backup')} {self.skill_md_path}"
627
+ )
628
+ print(f" 3. Package: skill-seekers package {self.skill_dir}/")
629
+
630
+ return True
631
+
632
+ def _run_headless(self, prompt_file, timeout):
633
+ """Run Claude enhancement in headless mode (no terminal window)
634
+
635
+ Args:
636
+ prompt_file: Path to prompt file
637
+ timeout: Maximum seconds to wait
638
+
639
+ Returns:
640
+ bool: True if enhancement succeeded
641
+ """
642
+ import time
643
+
644
+ print("✨ Running Claude Code enhancement (headless mode)...")
645
+ print(f" Timeout: {timeout} seconds ({timeout // 60} minutes)")
646
+ print()
647
+
648
+ # Record initial state
649
+ initial_mtime = self.skill_md_path.stat().st_mtime if self.skill_md_path.exists() else 0
650
+ initial_size = self.skill_md_path.stat().st_size if self.skill_md_path.exists() else 0
651
+
652
+ # Start timer
653
+ start_time = time.time()
654
+
655
+ try:
656
+ # Run claude command directly (this WAITS for completion)
657
+ # Use --dangerously-skip-permissions to bypass ALL permission checks
658
+ print(f" Running: claude --dangerously-skip-permissions {prompt_file}")
659
+ print(" ⏳ Please wait...")
660
+ print(f" Working directory: {self.skill_dir}")
661
+ print()
662
+
663
+ result = subprocess.run(
664
+ ["claude", "--dangerously-skip-permissions", prompt_file],
665
+ capture_output=True,
666
+ text=True,
667
+ timeout=timeout,
668
+ cwd=str(self.skill_dir), # Run from skill directory
669
+ )
670
+
671
+ elapsed = time.time() - start_time
672
+
673
+ # Check if successful
674
+ if result.returncode == 0:
675
+ # Verify SKILL.md was actually updated
676
+ if self.skill_md_path.exists():
677
+ new_mtime = self.skill_md_path.stat().st_mtime
678
+ new_size = self.skill_md_path.stat().st_size
679
+
680
+ if new_mtime > initial_mtime and new_size > initial_size:
681
+ print(f"✅ Enhancement complete! ({elapsed:.1f} seconds)")
682
+ print(f" SKILL.md updated: {new_size:,} bytes")
683
+ print()
684
+
685
+ # Clean up prompt file
686
+ with contextlib.suppress(Exception):
687
+ os.unlink(prompt_file)
688
+
689
+ return True
690
+ else:
691
+ print("⚠️ Claude finished but SKILL.md was not updated")
692
+ print(f" Initial: mtime={initial_mtime}, size={initial_size}")
693
+ print(f" Final: mtime={new_mtime}, size={new_size}")
694
+ print(" This might indicate an error during enhancement")
695
+ print()
696
+ # Show last 20 lines of stdout for debugging
697
+ if result.stdout:
698
+ print(" Last output from Claude:")
699
+ lines = result.stdout.strip().split("\n")[-20:]
700
+ for line in lines:
701
+ print(f" | {line}")
702
+ print()
703
+ return False
704
+ else:
705
+ print("❌ SKILL.md not found after enhancement")
706
+ return False
707
+ else:
708
+ print(f"❌ Claude Code returned error (exit code: {result.returncode})")
709
+ if result.stderr:
710
+ print(f" Error: {result.stderr[:200]}")
711
+ return False
712
+
713
+ except subprocess.TimeoutExpired:
714
+ elapsed = time.time() - start_time
715
+ print(f"\n⚠️ Enhancement timed out after {elapsed:.0f} seconds")
716
+ print(f" Timeout limit: {timeout} seconds")
717
+ print()
718
+ print(" Possible reasons:")
719
+ print(" - Skill is very large (many references)")
720
+ print(" - Claude is taking longer than usual")
721
+ print(" - Network issues")
722
+ print()
723
+ print(" Try:")
724
+ print(" 1. Use terminal mode: --interactive-enhancement")
725
+ print(" 2. Reduce reference content")
726
+ print(" 3. Try again later")
727
+
728
+ # Clean up
729
+ with contextlib.suppress(Exception):
730
+ os.unlink(prompt_file)
731
+
732
+ return False
733
+
734
+ except FileNotFoundError:
735
+ print("❌ 'claude' command not found")
736
+ print()
737
+ print(" Make sure Claude Code CLI is installed:")
738
+ print(" See: https://docs.claude.com/claude-code")
739
+ print()
740
+ print(" Try terminal mode instead: --interactive-enhancement")
741
+
742
+ return False
743
+
744
+ except Exception as e:
745
+ print(f"❌ Unexpected error: {e}")
746
+ return False
747
+
748
+ def _run_background(self, headless, timeout):
749
+ """Run enhancement in background thread, return immediately.
750
+
751
+ Args:
752
+ headless: Run headless mode
753
+ timeout: Timeout in seconds
754
+
755
+ Returns:
756
+ bool: True if background task started successfully
757
+ """
758
+ print(f"\n{'=' * 60}")
759
+ print(f"BACKGROUND ENHANCEMENT: {self.skill_dir.name}")
760
+ print(f"{'=' * 60}\n")
761
+
762
+ # Write initial status
763
+ self.write_status("pending", "Starting background enhancement...")
764
+
765
+ def background_worker():
766
+ """Worker function for background thread"""
767
+ try:
768
+ self.write_status("running", "Enhancement in progress...", progress=0.1)
769
+
770
+ # Read reference files
771
+ references = read_reference_files(
772
+ self.skill_dir, max_chars=LOCAL_CONTENT_LIMIT, preview_limit=LOCAL_PREVIEW_LIMIT
773
+ )
774
+
775
+ if not references:
776
+ self.write_status("failed", error="No reference files found")
777
+ return
778
+
779
+ total_size = sum(len(c) for c in references.values())
780
+ use_summarization = total_size > 30000
781
+
782
+ self.write_status("running", "Creating enhancement prompt...", progress=0.3)
783
+
784
+ # Create prompt
785
+ prompt = self.create_enhancement_prompt(use_summarization=use_summarization)
786
+ if not prompt:
787
+ self.write_status("failed", error="Failed to create prompt")
788
+ return
789
+
790
+ # Save prompt to temp file
791
+ with tempfile.NamedTemporaryFile(
792
+ mode="w", suffix=".txt", delete=False, encoding="utf-8"
793
+ ) as f:
794
+ prompt_file = f.name
795
+ f.write(prompt)
796
+
797
+ self.write_status("running", "Running Claude Code enhancement...", progress=0.5)
798
+
799
+ # Run enhancement
800
+ if headless:
801
+ # Run headless (subprocess.run - blocking in thread)
802
+ result = subprocess.run(
803
+ ["claude", prompt_file], capture_output=True, text=True, timeout=timeout
804
+ )
805
+
806
+ # Clean up
807
+ with contextlib.suppress(Exception):
808
+ os.unlink(prompt_file)
809
+
810
+ if result.returncode == 0:
811
+ self.write_status(
812
+ "completed", "Enhancement completed successfully!", progress=1.0
813
+ )
814
+ else:
815
+ self.write_status(
816
+ "failed", error=f"Claude returned error: {result.returncode}"
817
+ )
818
+ else:
819
+ # Terminal mode in background doesn't make sense
820
+ self.write_status("failed", error="Terminal mode not supported in background")
821
+
822
+ except subprocess.TimeoutExpired:
823
+ self.write_status("failed", error=f"Enhancement timed out after {timeout} seconds")
824
+ except Exception as e:
825
+ self.write_status("failed", error=str(e))
826
+
827
+ # Start background thread
828
+ thread = threading.Thread(target=background_worker, daemon=True)
829
+ thread.start()
830
+
831
+ print("✅ Background enhancement started!")
832
+ print()
833
+ print("📊 Monitoring:")
834
+ print(f" - Status file: {self.status_file}")
835
+ print(f" - Check status: cat {self.status_file}")
836
+ print(f" - Or use: skill-seekers enhance-status {self.skill_dir}")
837
+ print()
838
+ print("💡 The enhancement will continue in the background.")
839
+ print(" You can close this terminal - the process will keep running.")
840
+ print()
841
+
842
+ return True
843
+
844
+ def _run_daemon(self, timeout):
845
+ """Run as persistent daemon process with monitoring.
846
+
847
+ Creates a detached background process that continues running even if parent exits.
848
+
849
+ Args:
850
+ timeout: Timeout in seconds
851
+
852
+ Returns:
853
+ bool: True if daemon started successfully
854
+ """
855
+ print(f"\n{'=' * 60}")
856
+ print(f"DAEMON MODE: {self.skill_dir.name}")
857
+ print(f"{'=' * 60}\n")
858
+
859
+ # Write initial status
860
+ self.write_status("pending", "Starting daemon process...")
861
+
862
+ print("🔧 Creating daemon process...")
863
+
864
+ # Create Python script for daemon
865
+ daemon_script = f'''#!/usr/bin/env python3
866
+ import os
867
+ import sys
868
+ import time
869
+ import subprocess
870
+ import tempfile
871
+ import json
872
+ from pathlib import Path
873
+ from datetime import datetime
874
+
875
+ skill_dir = Path("{self.skill_dir}")
876
+ status_file = skill_dir / ".enhancement_status.json"
877
+ skill_md_path = skill_dir / "SKILL.md"
878
+
879
+ def write_status(status, message="", progress=0.0, error=None):
880
+ status_data = {{
881
+ "status": status,
882
+ "message": message,
883
+ "progress": progress,
884
+ "timestamp": datetime.now().isoformat(),
885
+ "skill_dir": str(skill_dir),
886
+ "error": error,
887
+ "pid": os.getpid()
888
+ }}
889
+ status_file.write_text(json.dumps(status_data, indent=2), encoding='utf-8')
890
+
891
+ try:
892
+ write_status("running", "Daemon started, loading references...", progress=0.1)
893
+
894
+ # Import enhancement logic
895
+ sys.path.insert(0, "{os.path.dirname(os.path.dirname(os.path.abspath(__file__)))}")
896
+ from skill_seekers.cli.enhance_skill_local import LocalSkillEnhancer
897
+
898
+ enhancer = LocalSkillEnhancer("{self.skill_dir}")
899
+
900
+ # Create prompt
901
+ write_status("running", "Creating enhancement prompt...", progress=0.3)
902
+ prompt = enhancer.create_enhancement_prompt(use_summarization=True)
903
+
904
+ if not prompt:
905
+ write_status("failed", error="Failed to create prompt")
906
+ sys.exit(1)
907
+
908
+ # Save prompt
909
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
910
+ prompt_file = f.name
911
+ f.write(prompt)
912
+
913
+ write_status("running", "Running Claude Code...", progress=0.5)
914
+
915
+ # Run Claude
916
+ result = subprocess.run(
917
+ ['claude', prompt_file],
918
+ capture_output=True,
919
+ text=True,
920
+ timeout={timeout}
921
+ )
922
+
923
+ # Clean up
924
+ try:
925
+ os.unlink(prompt_file)
926
+ except Exception:
927
+ pass
928
+
929
+ if result.returncode == 0:
930
+ write_status("completed", "Enhancement completed successfully!", progress=1.0)
931
+ sys.exit(0)
932
+ else:
933
+ write_status("failed", error=f"Claude returned error: {{result.returncode}}")
934
+ sys.exit(1)
935
+
936
+ except subprocess.TimeoutExpired:
937
+ write_status("failed", error=f"Enhancement timed out after {timeout} seconds")
938
+ sys.exit(1)
939
+ except Exception as e:
940
+ write_status("failed", error=str(e))
941
+ sys.exit(1)
942
+ '''
943
+
944
+ # Save daemon script
945
+ daemon_script_path = self.skill_dir / ".enhancement_daemon.py"
946
+ daemon_script_path.write_text(daemon_script, encoding="utf-8")
947
+ daemon_script_path.chmod(0o755)
948
+
949
+ # Start daemon process (fully detached)
950
+ try:
951
+ # Use nohup to detach from terminal
952
+ log_file = self.skill_dir / ".enhancement_daemon.log"
953
+
954
+ if self.force:
955
+ # Force mode: No output, fully silent
956
+ subprocess.Popen(
957
+ ["nohup", "python3", str(daemon_script_path)],
958
+ stdout=subprocess.DEVNULL,
959
+ stderr=subprocess.DEVNULL,
960
+ start_new_session=True,
961
+ )
962
+ else:
963
+ # Normal mode: Log to file
964
+ with open(log_file, "w") as log:
965
+ subprocess.Popen(
966
+ ["nohup", "python3", str(daemon_script_path)],
967
+ stdout=log,
968
+ stderr=log,
969
+ start_new_session=True,
970
+ )
971
+
972
+ # Give daemon time to start
973
+ time.sleep(1)
974
+
975
+ # Read status to verify it started
976
+ status = self.read_status()
977
+
978
+ if status and status.get("status") in ["pending", "running"]:
979
+ print("✅ Daemon process started successfully!")
980
+ print()
981
+ print("📊 Monitoring:")
982
+ print(f" - Status file: {self.status_file}")
983
+ print(f" - Log file: {log_file}")
984
+ print(f" - PID: {status.get('pid', 'unknown')}")
985
+ print()
986
+ print("💡 Commands:")
987
+ print(f" - Check status: cat {self.status_file}")
988
+ print(f" - View logs: tail -f {log_file}")
989
+ print(f" - Or use: skill-seekers enhance-status {self.skill_dir}")
990
+ print()
991
+ print("🔥 The daemon will continue running even if you close this terminal!")
992
+ print()
993
+
994
+ return True
995
+ else:
996
+ print("❌ Daemon failed to start")
997
+ return False
998
+
999
+ except Exception as e:
1000
+ print(f"❌ Failed to start daemon: {e}")
1001
+ return False
1002
+
1003
+
1004
+ def main():
1005
+ import argparse
1006
+
1007
+ parser = argparse.ArgumentParser(
1008
+ description="Enhance a skill with Claude Code (local)",
1009
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1010
+ epilog="""
1011
+ Examples:
1012
+ # Headless mode (default - runs in foreground, waits for completion, auto-force)
1013
+ skill-seekers enhance output/react/
1014
+
1015
+ # Background mode (runs in background, returns immediately)
1016
+ skill-seekers enhance output/react/ --background
1017
+
1018
+ # Daemon mode (persistent background process, fully detached)
1019
+ skill-seekers enhance output/react/ --daemon
1020
+
1021
+ # Disable force mode (ask for confirmations)
1022
+ skill-seekers enhance output/react/ --no-force
1023
+
1024
+ # Interactive mode (opens terminal window)
1025
+ skill-seekers enhance output/react/ --interactive-enhancement
1026
+
1027
+ # Custom timeout
1028
+ skill-seekers enhance output/react/ --timeout 1200
1029
+
1030
+ Mode Comparison:
1031
+ - headless: Runs claude CLI directly, BLOCKS until done (default)
1032
+ - background: Runs in background thread, returns immediately
1033
+ - daemon: Fully detached process, continues after parent exits
1034
+ - terminal: Opens new terminal window (interactive)
1035
+
1036
+ Force Mode (Default ON):
1037
+ By default, all modes skip confirmations (auto-yes).
1038
+ Use --no-force to enable confirmation prompts.
1039
+ """,
1040
+ )
1041
+
1042
+ parser.add_argument("skill_directory", help="Path to skill directory (e.g., output/react/)")
1043
+
1044
+ parser.add_argument(
1045
+ "--interactive-enhancement",
1046
+ action="store_true",
1047
+ help="Open terminal window for enhancement (default: headless mode)",
1048
+ )
1049
+
1050
+ parser.add_argument(
1051
+ "--background",
1052
+ action="store_true",
1053
+ help="Run in background and return immediately (non-blocking)",
1054
+ )
1055
+
1056
+ parser.add_argument(
1057
+ "--daemon", action="store_true", help="Run as persistent daemon process (fully detached)"
1058
+ )
1059
+
1060
+ parser.add_argument(
1061
+ "--no-force",
1062
+ action="store_true",
1063
+ help="Disable force mode: enable confirmation prompts (default: force mode ON)",
1064
+ )
1065
+
1066
+ parser.add_argument(
1067
+ "--timeout",
1068
+ type=int,
1069
+ default=600,
1070
+ help="Timeout in seconds for headless mode (default: 600 = 10 minutes)",
1071
+ )
1072
+
1073
+ args = parser.parse_args()
1074
+
1075
+ # Validate mutually exclusive options
1076
+ mode_count = sum([args.interactive_enhancement, args.background, args.daemon])
1077
+ if mode_count > 1:
1078
+ print(
1079
+ "❌ Error: --interactive-enhancement, --background, and --daemon are mutually exclusive"
1080
+ )
1081
+ print(" Choose only one mode")
1082
+ sys.exit(1)
1083
+
1084
+ # Run enhancement
1085
+ # Force mode is ON by default, use --no-force to disable
1086
+ enhancer = LocalSkillEnhancer(args.skill_directory, force=not args.no_force)
1087
+ headless = not args.interactive_enhancement # Invert: default is headless
1088
+ success = enhancer.run(
1089
+ headless=headless, timeout=args.timeout, background=args.background, daemon=args.daemon
1090
+ )
1091
+
1092
+ sys.exit(0 if success else 1)
1093
+
1094
+
1095
+ if __name__ == "__main__":
1096
+ main()