python-skills 1.0.0__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.
- python_skills/__init__.py +10 -0
- python_skills/__main__.py +6 -0
- python_skills/adapters/__init__.py +48 -0
- python_skills/adapters/agent_skills.py +415 -0
- python_skills/adapters/aider_adapter.py +226 -0
- python_skills/adapters/base.py +153 -0
- python_skills/adapters/claude.py +474 -0
- python_skills/adapters/cline.py +332 -0
- python_skills/adapters/codex.py +24 -0
- python_skills/adapters/continue_adapter.py +198 -0
- python_skills/adapters/cursor.py +327 -0
- python_skills/adapters/gemini.py +26 -0
- python_skills/adapters/goose.py +26 -0
- python_skills/adapters/junie.py +25 -0
- python_skills/adapters/kiro.py +382 -0
- python_skills/adapters/opencode.py +27 -0
- python_skills/adapters/roo.py +25 -0
- python_skills/adapters/universal.py +203 -0
- python_skills/adapters/vscode.py +27 -0
- python_skills/adapters/windsurf.py +26 -0
- python_skills/adapters/zed.py +27 -0
- python_skills/cli.py +326 -0
- python_skills/config.py +160 -0
- python_skills/detector.py +152 -0
- python_skills/installer.py +163 -0
- python_skills/markers.py +115 -0
- python_skills/skills/__init__.py +14 -0
- python_skills/skills/loader.py +171 -0
- python_skills/skills/metadata.py +152 -0
- python_skills/skills/registry.py +101 -0
- python_skills/state.py +204 -0
- python_skills-1.0.0.dist-info/METADATA +99 -0
- python_skills-1.0.0.dist-info/RECORD +105 -0
- python_skills-1.0.0.dist-info/WHEEL +4 -0
- python_skills-1.0.0.dist-info/entry_points.txt +2 -0
- python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
- skills/advanced_python.md +239 -0
- skills/anti_patterns/index.md +406 -0
- skills/comprehensions.md +167 -0
- skills/control_flow.md +175 -0
- skills/data_structures.md +243 -0
- skills/debugging/common_bugs.md +222 -0
- skills/debugging/inspection_techniques.md +249 -0
- skills/debugging/root_cause.md +203 -0
- skills/engineering/application_logging.md +195 -0
- skills/engineering/cli_apps.md +207 -0
- skills/engineering/configuration.md +218 -0
- skills/engineering/database.md +240 -0
- skills/engineering/dependency_management.md +205 -0
- skills/engineering/http_clients.md +267 -0
- skills/engineering/modules_packages.md +211 -0
- skills/engineering/packaging.md +197 -0
- skills/engineering/project_structure.md +155 -0
- skills/engineering/pyproject_toml.md +302 -0
- skills/engineering/virtual_environments.md +206 -0
- skills/functions.md +244 -0
- skills/generation/async_concurrency.md +291 -0
- skills/generation/error_handling.md +276 -0
- skills/generation/protocols_generics.md +243 -0
- skills/generation/type_hints.md +290 -0
- skills/generation/validation_pipeline.md +274 -0
- skills/generation/workflow.md +190 -0
- skills/oop.md +228 -0
- skills/quality/abstractions.md +154 -0
- skills/quality/comments.md +177 -0
- skills/quality/documentation.md +176 -0
- skills/quality/duplication.md +137 -0
- skills/quality/maintainability.md +142 -0
- skills/quality/naming.md +171 -0
- skills/quality/quality_functions.md +245 -0
- skills/quality/readability.md +239 -0
- skills/quality/type_annotations.md +192 -0
- skills/refactoring/behavior_preservation.md +157 -0
- skills/refactoring/incremental.md +187 -0
- skills/refactoring/interface_stability.md +199 -0
- skills/refactoring/safe_refactoring.md +206 -0
- skills/security/auth_boundaries.md +200 -0
- skills/security/command_injection.md +207 -0
- skills/security/dependency_risks.md +282 -0
- skills/security/file_handling.md +156 -0
- skills/security/input_validation.md +190 -0
- skills/security/path_traversal.md +172 -0
- skills/security/secrets.md +171 -0
- skills/security/sql_injection.md +188 -0
- skills/security/unsafe_deserialization.md +164 -0
- skills/stdlib/argparse.md +178 -0
- skills/stdlib/collections.md +212 -0
- skills/stdlib/datetime.md +187 -0
- skills/stdlib/functools.md +238 -0
- skills/stdlib/itertools.md +183 -0
- skills/stdlib/json.md +162 -0
- skills/stdlib/logging.md +185 -0
- skills/stdlib/os_sys.md +184 -0
- skills/stdlib/pathlib.md +218 -0
- skills/stdlib/re.md +171 -0
- skills/stdlib/statistics.md +112 -0
- skills/stdlib/subprocess.md +211 -0
- skills/testing/async_tests.md +249 -0
- skills/testing/coverage.md +168 -0
- skills/testing/edge_cases.md +197 -0
- skills/testing/fixtures_mocks.md +203 -0
- skills/testing/organization.md +205 -0
- skills/testing/parameterized.md +174 -0
- skills/testing/regression_tests.md +165 -0
- skills/variables_types.md +107 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Cline adapter for python-skills.
|
|
2
|
+
|
|
3
|
+
Cline supports both:
|
|
4
|
+
1. .clinerules/ (rules-based, legacy)
|
|
5
|
+
2. .cline/skills/<name>/SKILL.md (Agent Skills, native)
|
|
6
|
+
3. .agents/skills/<name>/SKILL.md (cross-agent standard)
|
|
7
|
+
|
|
8
|
+
This adapter installs both rules AND skills for maximum compatibility.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ..adapters.base import (
|
|
15
|
+
AgentAdapter,
|
|
16
|
+
DetectionResult,
|
|
17
|
+
InstallResult,
|
|
18
|
+
StatusResult,
|
|
19
|
+
SyncResult,
|
|
20
|
+
UninstallResult,
|
|
21
|
+
)
|
|
22
|
+
from ..config import Target
|
|
23
|
+
from ..skills.registry import SkillRegistry
|
|
24
|
+
from ..state import LockManager
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClineAdapter(AgentAdapter):
|
|
28
|
+
"""Adapter for Cline.
|
|
29
|
+
|
|
30
|
+
Installs:
|
|
31
|
+
- .agents/skills/<name>/SKILL.md (Agent Skills)
|
|
32
|
+
- .cline/skills/<name>/SKILL.md (native)
|
|
33
|
+
- .clinerules/python-skills.md (rules, backward compat)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name = "Cline"
|
|
37
|
+
target = Target.CLINE.value
|
|
38
|
+
|
|
39
|
+
def __init__(self, project_root: Path, skills_registry: SkillRegistry, lock_manager: LockManager, config):
|
|
40
|
+
super().__init__(project_root, skills_registry, lock_manager, config)
|
|
41
|
+
|
|
42
|
+
def _get_global_path(self) -> Path:
|
|
43
|
+
import platform
|
|
44
|
+
system = platform.system()
|
|
45
|
+
if system == "Windows":
|
|
46
|
+
return Path.home() / "Documents" / "Cline" / "Rules"
|
|
47
|
+
else:
|
|
48
|
+
return Path.home() / "Documents" / "Cline" / "Rules"
|
|
49
|
+
|
|
50
|
+
def detect(self) -> DetectionResult:
|
|
51
|
+
app_detected = False
|
|
52
|
+
try:
|
|
53
|
+
import subprocess
|
|
54
|
+
result = subprocess.run(["code", "--list-extensions"], capture_output=True, text=True)
|
|
55
|
+
if "cline.cline" in result.stdout.lower():
|
|
56
|
+
app_detected = True
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
project_config = (
|
|
61
|
+
(self.project_root / ".clinerules").exists() or
|
|
62
|
+
(self.project_root / ".cline").exists() or
|
|
63
|
+
(self.project_root / ".agents" / "skills").exists()
|
|
64
|
+
)
|
|
65
|
+
global_config = self._get_global_path().exists()
|
|
66
|
+
|
|
67
|
+
return DetectionResult(
|
|
68
|
+
application_detected=app_detected,
|
|
69
|
+
project_config_detected=project_config,
|
|
70
|
+
adapter_available=True,
|
|
71
|
+
details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}, Global: {'yes' if global_config else 'no'}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
|
|
75
|
+
result = InstallResult(success=True, target=self.target, scope=scope)
|
|
76
|
+
target_path = self._get_scope_path(scope)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# 1. Install Agent Skills to .agents/skills/
|
|
80
|
+
agents_skills_dir = target_path / ".agents" / "skills"
|
|
81
|
+
agents_created = self._install_skills_to_dir(agents_skills_dir, dry_run)
|
|
82
|
+
result.files_created.extend(agents_created)
|
|
83
|
+
|
|
84
|
+
# 2. Install Agent Skills to .cline/skills/ (native)
|
|
85
|
+
cline_skills_dir = target_path / ".cline" / "skills"
|
|
86
|
+
cline_created = self._install_skills_to_dir(cline_skills_dir, dry_run)
|
|
87
|
+
result.files_created.extend(cline_created)
|
|
88
|
+
|
|
89
|
+
# 3. Generate rules file (backward compat)
|
|
90
|
+
rules_dir = target_path / ".clinerules"
|
|
91
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
rule_path = rules_dir / "python-skills.md"
|
|
94
|
+
content = self._generate_cline_rule()
|
|
95
|
+
|
|
96
|
+
if dry_run:
|
|
97
|
+
result.files_created.append(str(rule_path.relative_to(self.project_root)))
|
|
98
|
+
else:
|
|
99
|
+
self._safe_write_file(rule_path, content, dry_run)
|
|
100
|
+
result.files_created.append(str(rule_path.relative_to(self.project_root)))
|
|
101
|
+
self._record_file(self.target, scope, rule_path)
|
|
102
|
+
|
|
103
|
+
if not dry_run:
|
|
104
|
+
self._update_lock_state(scope)
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
result.success = False
|
|
108
|
+
result.errors.append(str(e))
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def _install_skills_to_dir(self, skills_dir: Path, dry_run: bool) -> list[str]:
|
|
113
|
+
"""Install canonical skills as Agent Skills SKILL.md files."""
|
|
114
|
+
from .agent_skills import _generate_skill_md, _skill_name_to_slug
|
|
115
|
+
created = []
|
|
116
|
+
|
|
117
|
+
if not self.skills_registry:
|
|
118
|
+
return created
|
|
119
|
+
|
|
120
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
for skill in self.skills_registry.get_all_skills():
|
|
123
|
+
slug = _skill_name_to_slug(skill.name)
|
|
124
|
+
target_skill_dir = skills_dir / slug
|
|
125
|
+
target_skill_file = target_skill_dir / "SKILL.md"
|
|
126
|
+
|
|
127
|
+
if dry_run:
|
|
128
|
+
created.append(str(target_skill_file.relative_to(self.project_root)))
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
content = self.skills_registry.get_skill_content(skill.name)
|
|
132
|
+
if not content:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
skill_md = _generate_skill_md(skill, content)
|
|
136
|
+
|
|
137
|
+
target_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
target_skill_file.write_text(skill_md, encoding="utf-8")
|
|
139
|
+
|
|
140
|
+
created.append(str(target_skill_file.relative_to(self.project_root)))
|
|
141
|
+
self._record_file(self.target, "project", target_skill_file)
|
|
142
|
+
|
|
143
|
+
return created
|
|
144
|
+
|
|
145
|
+
def _generate_cline_rule(self) -> str:
|
|
146
|
+
categories = {}
|
|
147
|
+
for skill in self.skills_registry.get_all_skills():
|
|
148
|
+
if skill.category not in categories:
|
|
149
|
+
categories[skill.category] = []
|
|
150
|
+
categories[skill.category].append(skill.name)
|
|
151
|
+
|
|
152
|
+
parts = [
|
|
153
|
+
"---",
|
|
154
|
+
"paths:",
|
|
155
|
+
' - "**/*.py"',
|
|
156
|
+
"---",
|
|
157
|
+
"",
|
|
158
|
+
"<!-- BEGIN PYTHON-SKILLS MANAGED -->",
|
|
159
|
+
"",
|
|
160
|
+
"# Python Skills Integration",
|
|
161
|
+
"",
|
|
162
|
+
"This project uses python-skills for Python engineering standards.",
|
|
163
|
+
"",
|
|
164
|
+
"## Available Skill Categories",
|
|
165
|
+
"",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
for category, skills in sorted(categories.items()):
|
|
169
|
+
parts.append(f"### {category.title()}")
|
|
170
|
+
parts.append("")
|
|
171
|
+
for skill in sorted(skills):
|
|
172
|
+
parts.append(f"- `{skill}`")
|
|
173
|
+
parts.append("")
|
|
174
|
+
|
|
175
|
+
parts.extend([
|
|
176
|
+
"## Usage",
|
|
177
|
+
"",
|
|
178
|
+
"Reference relevant skills when working on Python code.",
|
|
179
|
+
"",
|
|
180
|
+
"<!-- END PYTHON-SKILLS MANAGED -->",
|
|
181
|
+
])
|
|
182
|
+
|
|
183
|
+
return "\n".join(parts)
|
|
184
|
+
|
|
185
|
+
def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
|
|
186
|
+
result = SyncResult(success=True, target=self.target, scope=scope)
|
|
187
|
+
target_path = self._get_scope_path(scope)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
# Sync skills in .agents/skills/
|
|
191
|
+
agents_skills_dir = target_path / ".agents" / "skills"
|
|
192
|
+
if agents_skills_dir.exists():
|
|
193
|
+
for item in agents_skills_dir.iterdir():
|
|
194
|
+
if item.is_dir() and (item / "SKILL.md").exists():
|
|
195
|
+
if not dry_run:
|
|
196
|
+
shutil.rmtree(item)
|
|
197
|
+
result.removed.append(str(item.relative_to(self.project_root)))
|
|
198
|
+
|
|
199
|
+
# Re-install skills
|
|
200
|
+
agents_created = self._install_skills_to_dir(agents_skills_dir, dry_run)
|
|
201
|
+
result.added.extend(agents_created)
|
|
202
|
+
|
|
203
|
+
cline_skills_dir = target_path / ".cline" / "skills"
|
|
204
|
+
if cline_skills_dir.exists():
|
|
205
|
+
for item in cline_skills_dir.iterdir():
|
|
206
|
+
if item.is_dir() and (item / "SKILL.md").exists():
|
|
207
|
+
if not dry_run:
|
|
208
|
+
shutil.rmtree(item)
|
|
209
|
+
result.removed.append(str(item.relative_to(self.project_root)))
|
|
210
|
+
|
|
211
|
+
cline_created = self._install_skills_to_dir(cline_skills_dir, dry_run)
|
|
212
|
+
result.added.extend(cline_created)
|
|
213
|
+
|
|
214
|
+
# Sync rules
|
|
215
|
+
rule_path = target_path / ".clinerules" / "python-skills.md"
|
|
216
|
+
if rule_path.exists():
|
|
217
|
+
current = rule_path.read_text(encoding="utf-8")
|
|
218
|
+
new_content = self._generate_cline_rule()
|
|
219
|
+
if current.strip() != new_content.strip():
|
|
220
|
+
if not dry_run:
|
|
221
|
+
self._safe_write_file(rule_path, new_content, dry_run)
|
|
222
|
+
result.modified.append(str(rule_path.relative_to(self.project_root)))
|
|
223
|
+
|
|
224
|
+
if not dry_run:
|
|
225
|
+
self._update_lock_state(scope)
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
result.success = False
|
|
229
|
+
result.errors.append(str(e))
|
|
230
|
+
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
|
|
234
|
+
result = UninstallResult(success=True, target=self.target, scope=scope)
|
|
235
|
+
target_path = self._get_scope_path(scope)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Remove .agents/skills/ managed content
|
|
239
|
+
agents_skills_dir = target_path / ".agents" / "skills"
|
|
240
|
+
if agents_skills_dir.exists():
|
|
241
|
+
for item in agents_skills_dir.iterdir():
|
|
242
|
+
if item.is_dir():
|
|
243
|
+
skill_file = item / "SKILL.md"
|
|
244
|
+
if skill_file.exists():
|
|
245
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
246
|
+
begin, end = self._get_markers(skill_file)
|
|
247
|
+
if begin in content:
|
|
248
|
+
if not dry_run:
|
|
249
|
+
shutil.rmtree(item)
|
|
250
|
+
result.files_removed.append(str(item.relative_to(self.project_root)))
|
|
251
|
+
|
|
252
|
+
# Remove .cline/skills/ managed content
|
|
253
|
+
cline_skills_dir = target_path / ".cline" / "skills"
|
|
254
|
+
if cline_skills_dir.exists():
|
|
255
|
+
for item in cline_skills_dir.iterdir():
|
|
256
|
+
if item.is_dir():
|
|
257
|
+
skill_file = item / "SKILL.md"
|
|
258
|
+
if skill_file.exists():
|
|
259
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
260
|
+
begin, end = self._get_markers(skill_file)
|
|
261
|
+
if begin in content:
|
|
262
|
+
if not dry_run:
|
|
263
|
+
shutil.rmtree(item)
|
|
264
|
+
result.files_removed.append(str(item.relative_to(self.project_root)))
|
|
265
|
+
|
|
266
|
+
# Remove rules
|
|
267
|
+
rule_path = target_path / ".clinerules" / "python-skills.md"
|
|
268
|
+
if rule_path.exists() and self._has_ownership_marker(rule_path):
|
|
269
|
+
if not dry_run:
|
|
270
|
+
rule_path.unlink()
|
|
271
|
+
result.files_removed.append(str(rule_path.relative_to(self.project_root)))
|
|
272
|
+
|
|
273
|
+
if not dry_run:
|
|
274
|
+
self._update_lock_state(scope)
|
|
275
|
+
|
|
276
|
+
except Exception as e:
|
|
277
|
+
result.success = False
|
|
278
|
+
result.errors.append(str(e))
|
|
279
|
+
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
def _has_ownership_marker(self, path: Path) -> bool:
|
|
283
|
+
if not path.exists():
|
|
284
|
+
return False
|
|
285
|
+
content = path.read_text(encoding="utf-8")
|
|
286
|
+
begin, end = self._get_markers(path)
|
|
287
|
+
return begin in content and end in content
|
|
288
|
+
|
|
289
|
+
def status(self, scope: str = "project") -> StatusResult:
|
|
290
|
+
target_path = self._get_scope_path(scope)
|
|
291
|
+
installed = False
|
|
292
|
+
files = []
|
|
293
|
+
|
|
294
|
+
# Check .agents/skills/
|
|
295
|
+
agents_skills_dir = target_path / ".agents" / "skills"
|
|
296
|
+
if agents_skills_dir.exists():
|
|
297
|
+
for item in agents_skills_dir.iterdir():
|
|
298
|
+
if item.is_dir():
|
|
299
|
+
skill_file = item / "SKILL.md"
|
|
300
|
+
if skill_file.exists():
|
|
301
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
302
|
+
begin, end = self._get_markers(skill_file)
|
|
303
|
+
if begin in content:
|
|
304
|
+
installed = True
|
|
305
|
+
files.append(str(skill_file.relative_to(self.project_root)))
|
|
306
|
+
|
|
307
|
+
# Check .cline/skills/
|
|
308
|
+
cline_skills_dir = target_path / ".cline" / "skills"
|
|
309
|
+
if cline_skills_dir.exists():
|
|
310
|
+
for item in cline_skills_dir.iterdir():
|
|
311
|
+
if item.is_dir():
|
|
312
|
+
skill_file = item / "SKILL.md"
|
|
313
|
+
if skill_file.exists():
|
|
314
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
315
|
+
begin, end = self._get_markers(skill_file)
|
|
316
|
+
if begin in content:
|
|
317
|
+
installed = True
|
|
318
|
+
files.append(str(skill_file.relative_to(self.project_root)))
|
|
319
|
+
|
|
320
|
+
# Check .clinerules/
|
|
321
|
+
rule_path = target_path / ".clinerules" / "python-skills.md"
|
|
322
|
+
if rule_path.exists() and self._has_ownership_marker(rule_path):
|
|
323
|
+
installed = True
|
|
324
|
+
files.append(str(rule_path.relative_to(self.project_root)))
|
|
325
|
+
|
|
326
|
+
return StatusResult(
|
|
327
|
+
target=self.target, scope=scope, installed=installed,
|
|
328
|
+
files=files, version="2.0.0"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _update_lock_state(self, scope: str) -> None:
|
|
332
|
+
self.lock_manager.update_canonical_hash(self.skills_registry.skills_root)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Codex CLI adapter for python-skills."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .agent_skills import AgentSkillsAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CodexAdapter(AgentSkillsAdapter):
|
|
9
|
+
"""Adapter for OpenAI Codex CLI.
|
|
10
|
+
|
|
11
|
+
Codex CLI discovers skills from:
|
|
12
|
+
- .agents/skills/<name>/SKILL.md (cross-agent standard)
|
|
13
|
+
|
|
14
|
+
Codex is the primary architect of AGENTS.md.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
target_name = "codex"
|
|
18
|
+
display_name = "Codex CLI"
|
|
19
|
+
agent_skills_dir = ".agents/skills"
|
|
20
|
+
detection_app_command = "codex"
|
|
21
|
+
detection_project_markers = ["AGENTS.md"]
|
|
22
|
+
|
|
23
|
+
def _get_global_path(self) -> Path:
|
|
24
|
+
return Path.home() / ".agents" / "skills"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Continue adapter for python-skills.
|
|
2
|
+
|
|
3
|
+
Continue uses a native rules system (.continue/rules/) and reads
|
|
4
|
+
AGENTS.md/AGENT.md/CLAUDE.md as fallback instructions.
|
|
5
|
+
It does NOT have a native skills system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..adapters.base import (
|
|
11
|
+
AgentAdapter,
|
|
12
|
+
DetectionResult,
|
|
13
|
+
InstallResult,
|
|
14
|
+
StatusResult,
|
|
15
|
+
SyncResult,
|
|
16
|
+
UninstallResult,
|
|
17
|
+
)
|
|
18
|
+
from ..config import Target
|
|
19
|
+
from ..skills.registry import SkillRegistry
|
|
20
|
+
from ..state import LockManager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ContinueAdapter(AgentAdapter):
|
|
24
|
+
"""Adapter for Continue.
|
|
25
|
+
|
|
26
|
+
Continue uses rules (.continue/rules/*.md) not skills.
|
|
27
|
+
We generate a consolidated Python rules file.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name = "Continue"
|
|
31
|
+
target = Target.CONTINUE.value
|
|
32
|
+
|
|
33
|
+
def __init__(self, project_root: Path, skills_registry: SkillRegistry,
|
|
34
|
+
lock_manager: LockManager, config):
|
|
35
|
+
super().__init__(project_root, skills_registry, lock_manager, config)
|
|
36
|
+
|
|
37
|
+
def _get_global_path(self) -> Path:
|
|
38
|
+
return Path.home() / ".continue" / "rules"
|
|
39
|
+
|
|
40
|
+
def detect(self) -> DetectionResult:
|
|
41
|
+
import shutil
|
|
42
|
+
app_detected = shutil.which("continue") is not None
|
|
43
|
+
project_config = (self.project_root / ".continue").exists()
|
|
44
|
+
global_config = self._get_global_path().exists()
|
|
45
|
+
|
|
46
|
+
return DetectionResult(
|
|
47
|
+
application_detected=app_detected,
|
|
48
|
+
project_config_detected=project_config,
|
|
49
|
+
adapter_available=True,
|
|
50
|
+
details=f"App: {'yes' if app_detected else 'no'}, "
|
|
51
|
+
f"Project: {'yes' if project_config else 'no'}, "
|
|
52
|
+
f"Global: {'yes' if global_config else 'no'}",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
|
|
56
|
+
result = InstallResult(success=True, target=self.target, scope=scope)
|
|
57
|
+
target_path = self._get_scope_path(scope)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
rules_dir = target_path / ".continue" / "rules"
|
|
61
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
rule_path = rules_dir / "python-skills.md"
|
|
64
|
+
content = self._generate_rule()
|
|
65
|
+
|
|
66
|
+
if dry_run:
|
|
67
|
+
result.files_created.append(str(rule_path.relative_to(self.project_root)))
|
|
68
|
+
else:
|
|
69
|
+
self._safe_write_file(rule_path, content, dry_run)
|
|
70
|
+
result.files_created.append(str(rule_path.relative_to(self.project_root)))
|
|
71
|
+
self._record_file(self.target, scope, rule_path)
|
|
72
|
+
|
|
73
|
+
if not dry_run:
|
|
74
|
+
self._update_lock_state(scope)
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
result.success = False
|
|
78
|
+
result.errors.append(str(e))
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def _generate_rule(self) -> str:
|
|
83
|
+
categories = {}
|
|
84
|
+
if self.skills_registry:
|
|
85
|
+
for skill in self.skills_registry.get_all_skills():
|
|
86
|
+
if skill.category not in categories:
|
|
87
|
+
categories[skill.category] = []
|
|
88
|
+
categories[skill.category].append(skill.name)
|
|
89
|
+
|
|
90
|
+
parts = [
|
|
91
|
+
"---",
|
|
92
|
+
"alwaysApply: true",
|
|
93
|
+
"---",
|
|
94
|
+
"",
|
|
95
|
+
"<!-- BEGIN PYTHON-SKILLS MANAGED -->",
|
|
96
|
+
"",
|
|
97
|
+
"# Python Skills Integration",
|
|
98
|
+
"",
|
|
99
|
+
"This project uses python-skills for Python engineering standards.",
|
|
100
|
+
"",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for category, skills in sorted(categories.items()):
|
|
104
|
+
parts.append(f"### {category.title()}")
|
|
105
|
+
parts.append("")
|
|
106
|
+
for skill in sorted(skills):
|
|
107
|
+
parts.append(f"- `{skill}`")
|
|
108
|
+
parts.append("")
|
|
109
|
+
|
|
110
|
+
parts.extend([
|
|
111
|
+
"## Key Patterns",
|
|
112
|
+
"",
|
|
113
|
+
"- **Type hints**: Use built-in generics, `str | None`, `Protocol`",
|
|
114
|
+
"- **Async**: `asyncio.gather`, `Semaphore`, `TaskGroup` (3.11+)",
|
|
115
|
+
"- **HTTP**: `httpx` with retries, timeouts, connection pooling",
|
|
116
|
+
"- **Security**: Parameterized queries, `pathlib` for paths, `yaml.safe_load`",
|
|
117
|
+
"- **Testing**: `pytest` with parametrized tests, edge cases",
|
|
118
|
+
"",
|
|
119
|
+
"<!-- END PYTHON-SKILLS MANAGED -->",
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
return "\n".join(parts)
|
|
123
|
+
|
|
124
|
+
def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
|
|
125
|
+
result = SyncResult(success=True, target=self.target, scope=scope)
|
|
126
|
+
target_path = self._get_scope_path(scope)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
rule_path = target_path / ".continue" / "rules" / "python-skills.md"
|
|
130
|
+
if not rule_path.exists():
|
|
131
|
+
result.success = False
|
|
132
|
+
result.errors.append("python-skills.md not found")
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
current = rule_path.read_text(encoding="utf-8")
|
|
136
|
+
new_content = self._generate_rule()
|
|
137
|
+
|
|
138
|
+
if current.strip() != new_content.strip():
|
|
139
|
+
if not dry_run:
|
|
140
|
+
self._safe_write_file(rule_path, new_content, dry_run)
|
|
141
|
+
result.modified.append(str(rule_path.relative_to(self.project_root)))
|
|
142
|
+
else:
|
|
143
|
+
result.skipped.append(str(rule_path.relative_to(self.project_root)))
|
|
144
|
+
|
|
145
|
+
if not dry_run:
|
|
146
|
+
self._update_lock_state(scope)
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
result.success = False
|
|
150
|
+
result.errors.append(str(e))
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
|
|
155
|
+
result = UninstallResult(success=True, target=self.target, scope=scope)
|
|
156
|
+
target_path = self._get_scope_path(scope)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
rule_path = target_path / ".continue" / "rules" / "python-skills.md"
|
|
160
|
+
if rule_path.exists():
|
|
161
|
+
content = rule_path.read_text(encoding="utf-8")
|
|
162
|
+
begin, end = self._get_markers(rule_path)
|
|
163
|
+
if begin in content:
|
|
164
|
+
if not dry_run:
|
|
165
|
+
rule_path.unlink()
|
|
166
|
+
result.files_removed.append(str(rule_path.relative_to(self.project_root)))
|
|
167
|
+
|
|
168
|
+
if not dry_run:
|
|
169
|
+
self._update_lock_state(scope)
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
result.success = False
|
|
173
|
+
result.errors.append(str(e))
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
def status(self, scope: str = "project") -> StatusResult:
|
|
178
|
+
target_path = self._get_scope_path(scope)
|
|
179
|
+
rule_path = target_path / ".continue" / "rules" / "python-skills.md"
|
|
180
|
+
|
|
181
|
+
installed = False
|
|
182
|
+
files = []
|
|
183
|
+
if rule_path.exists():
|
|
184
|
+
content = rule_path.read_text(encoding="utf-8")
|
|
185
|
+
begin, end = self._get_markers(rule_path)
|
|
186
|
+
if begin in content:
|
|
187
|
+
installed = True
|
|
188
|
+
files.append(str(rule_path.relative_to(self.project_root)))
|
|
189
|
+
|
|
190
|
+
return StatusResult(
|
|
191
|
+
target=self.target, scope=scope, installed=installed,
|
|
192
|
+
files=files, version="2.0.0",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _update_lock_state(self, scope: str) -> None:
|
|
196
|
+
skills_root = self.skills_registry.skills_root
|
|
197
|
+
if skills_root.exists():
|
|
198
|
+
self.lock_manager.update_canonical_hash(skills_root)
|