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,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()
|