oasr 0.3.4__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.
- __init__.py +3 -0
- __main__.py +6 -0
- adapter.py +396 -0
- adapters/__init__.py +17 -0
- adapters/base.py +254 -0
- adapters/claude.py +82 -0
- adapters/codex.py +84 -0
- adapters/copilot.py +210 -0
- adapters/cursor.py +78 -0
- adapters/windsurf.py +83 -0
- cli.py +94 -0
- commands/__init__.py +6 -0
- commands/adapter.py +102 -0
- commands/add.py +302 -0
- commands/clean.py +155 -0
- commands/diff.py +180 -0
- commands/find.py +56 -0
- commands/help.py +51 -0
- commands/info.py +152 -0
- commands/list.py +110 -0
- commands/registry.py +303 -0
- commands/rm.py +128 -0
- commands/status.py +119 -0
- commands/sync.py +143 -0
- commands/update.py +417 -0
- commands/use.py +172 -0
- commands/validate.py +74 -0
- config.py +86 -0
- discovery.py +145 -0
- manifest.py +437 -0
- oasr-0.3.4.dist-info/METADATA +358 -0
- oasr-0.3.4.dist-info/RECORD +43 -0
- oasr-0.3.4.dist-info/WHEEL +4 -0
- oasr-0.3.4.dist-info/entry_points.txt +3 -0
- oasr-0.3.4.dist-info/licenses/LICENSE +187 -0
- oasr-0.3.4.dist-info/licenses/NOTICE +8 -0
- registry.py +173 -0
- remote.py +482 -0
- skillcopy/__init__.py +71 -0
- skillcopy/local.py +40 -0
- skillcopy/remote.py +98 -0
- tracking.py +181 -0
- validate.py +362 -0
adapters/claude.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Claude adapter for generating .claude/commands/*.md files.
|
|
2
|
+
|
|
3
|
+
Claude Code uses markdown command files similar to Cursor's format,
|
|
4
|
+
stored in .claude/commands/ directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from adapters.base import BaseAdapter, SkillInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClaudeAdapter(BaseAdapter):
|
|
13
|
+
"""Adapter for generating Claude Code command files."""
|
|
14
|
+
|
|
15
|
+
target_name = "claude"
|
|
16
|
+
target_subdir = ".claude/commands"
|
|
17
|
+
|
|
18
|
+
def generate(
|
|
19
|
+
self, skill: SkillInfo, output_dir: Path, copy: bool = False, base_output_dir: Path | None = None
|
|
20
|
+
) -> Path:
|
|
21
|
+
"""Generate a Claude command file for a skill.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
skill: Skill information.
|
|
25
|
+
output_dir: Resolved output directory (.claude/commands/).
|
|
26
|
+
copy: If True, use relative paths to local skill copies.
|
|
27
|
+
base_output_dir: Base output directory (for computing relative paths).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path to the generated file.
|
|
31
|
+
"""
|
|
32
|
+
output_file = output_dir / f"{skill.name}.md"
|
|
33
|
+
|
|
34
|
+
skill_path = self.get_skill_path(skill, base_output_dir or output_dir.parent.parent, copy)
|
|
35
|
+
|
|
36
|
+
content = f"""# {skill.name}
|
|
37
|
+
|
|
38
|
+
{skill.description}
|
|
39
|
+
|
|
40
|
+
This command delegates to the agent skill at `{skill_path}/`.
|
|
41
|
+
|
|
42
|
+
## Skill Location
|
|
43
|
+
|
|
44
|
+
- **Path:** `{skill_path}/`
|
|
45
|
+
- **Manifest:** `{skill_path}/SKILL.md`
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
output_file.write_text(content, encoding="utf-8")
|
|
49
|
+
return output_file
|
|
50
|
+
|
|
51
|
+
def cleanup_stale(self, output_dir: Path, valid_names: set[str]) -> list[Path]:
|
|
52
|
+
"""Remove stale Claude command files.
|
|
53
|
+
|
|
54
|
+
Only removes files that look like generated skill commands.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
output_dir: Output directory to clean.
|
|
58
|
+
valid_names: Set of valid skill names (files to keep).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of removed file paths.
|
|
62
|
+
"""
|
|
63
|
+
removed = []
|
|
64
|
+
|
|
65
|
+
if not output_dir.is_dir():
|
|
66
|
+
return removed
|
|
67
|
+
|
|
68
|
+
for file in output_dir.glob("*.md"):
|
|
69
|
+
name = file.stem
|
|
70
|
+
|
|
71
|
+
if name in valid_names:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
content = file.read_text(encoding="utf-8")
|
|
76
|
+
if "This command delegates to the agent skill at" in content:
|
|
77
|
+
file.unlink()
|
|
78
|
+
removed.append(file)
|
|
79
|
+
except (OSError, UnicodeDecodeError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
return removed
|
adapters/codex.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Codex adapter for generating codex skill files.
|
|
2
|
+
|
|
3
|
+
Note: Codex format is TBD. Currently uses same format as Cursor.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from adapters.base import BaseAdapter, SkillInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CodexAdapter(BaseAdapter):
|
|
12
|
+
"""Adapter for generating Codex skill files.
|
|
13
|
+
|
|
14
|
+
Placeholder implementation - uses same format as Cursor commands.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
target_name = "codex"
|
|
18
|
+
target_subdir = ".codex/skills"
|
|
19
|
+
|
|
20
|
+
def generate(
|
|
21
|
+
self, skill: SkillInfo, output_dir: Path, copy: bool = False, base_output_dir: Path | None = None
|
|
22
|
+
) -> Path:
|
|
23
|
+
"""Generate a Codex skill file for a skill.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
skill: Skill information.
|
|
27
|
+
output_dir: Resolved output directory (.codex/skills/).
|
|
28
|
+
copy: If True, use relative paths to local skill copies.
|
|
29
|
+
base_output_dir: Base output directory (for computing relative paths).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Path to the generated file.
|
|
33
|
+
"""
|
|
34
|
+
output_file = output_dir / f"{skill.name}.md"
|
|
35
|
+
|
|
36
|
+
skill_path = self.get_skill_path(skill, base_output_dir or output_dir.parent.parent, copy)
|
|
37
|
+
|
|
38
|
+
content = f"""# {skill.name}
|
|
39
|
+
|
|
40
|
+
{skill.description}
|
|
41
|
+
|
|
42
|
+
This skill delegates to the agent skill at `{skill_path}/`.
|
|
43
|
+
|
|
44
|
+
## Skill Location
|
|
45
|
+
|
|
46
|
+
- **Path:** `{skill_path}/`
|
|
47
|
+
- **Manifest:** `{skill_path}/SKILL.md`
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
output_file.write_text(content, encoding="utf-8")
|
|
51
|
+
return output_file
|
|
52
|
+
|
|
53
|
+
def cleanup_stale(self, output_dir: Path, valid_names: set[str]) -> list[Path]:
|
|
54
|
+
"""Remove stale Codex skill files.
|
|
55
|
+
|
|
56
|
+
Only removes files that look like generated skill files.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
output_dir: Output directory to clean.
|
|
60
|
+
valid_names: Set of valid skill names (files to keep).
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of removed file paths.
|
|
64
|
+
"""
|
|
65
|
+
removed = []
|
|
66
|
+
|
|
67
|
+
if not output_dir.is_dir():
|
|
68
|
+
return removed
|
|
69
|
+
|
|
70
|
+
for file in output_dir.glob("*.md"):
|
|
71
|
+
name = file.stem
|
|
72
|
+
|
|
73
|
+
if name in valid_names:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
content = file.read_text(encoding="utf-8")
|
|
78
|
+
if "This skill delegates to the agent skill at" in content:
|
|
79
|
+
file.unlink()
|
|
80
|
+
removed.append(file)
|
|
81
|
+
except (OSError, UnicodeDecodeError):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
return removed
|
adapters/copilot.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Copilot adapter for generating GitHub Copilot integration files.
|
|
2
|
+
|
|
3
|
+
GitHub Copilot supports three types of custom content:
|
|
4
|
+
1. Prompt files (.github/prompts/*.prompt.md) - Invokable via /name in chat
|
|
5
|
+
2. Instructions file (.github/copilot-instructions.md) - Auto-injected repo-wide rules
|
|
6
|
+
3. Scoped instructions (.github/instructions/*.instructions.md) - Path-specific rules
|
|
7
|
+
|
|
8
|
+
This adapter generates:
|
|
9
|
+
- Per-skill prompt files for invokable workflows (/skill-name)
|
|
10
|
+
- A consolidated instructions file listing available skills
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from adapters.base import BaseAdapter, SkillInfo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CopilotAdapter(BaseAdapter):
|
|
19
|
+
"""Adapter for generating GitHub Copilot files.
|
|
20
|
+
|
|
21
|
+
Generates:
|
|
22
|
+
- .github/prompts/{skill}.prompt.md - Invokable via /{skill} in chat
|
|
23
|
+
- .github/copilot-instructions.md - Repository-wide skill index (auto-injected)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
target_name = "copilot"
|
|
27
|
+
target_subdir = ".github/prompts"
|
|
28
|
+
|
|
29
|
+
MARKER = "<!-- ASR-MANAGED-SKILLS -->"
|
|
30
|
+
MARKER_END = "<!-- /ASR-MANAGED-SKILLS -->"
|
|
31
|
+
|
|
32
|
+
def generate(
|
|
33
|
+
self, skill: SkillInfo, output_dir: Path, copy: bool = False, base_output_dir: Path | None = None
|
|
34
|
+
) -> Path:
|
|
35
|
+
"""Generate a prompt file for a single skill.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
skill: Skill information.
|
|
39
|
+
output_dir: Resolved output directory (.github/prompts/).
|
|
40
|
+
copy: If True, use relative paths to local skill copies.
|
|
41
|
+
base_output_dir: Base output directory (for computing relative paths).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Path to the generated prompt file.
|
|
45
|
+
"""
|
|
46
|
+
output_file = output_dir / f"{skill.name}.prompt.md"
|
|
47
|
+
|
|
48
|
+
skill_path = self.get_skill_path(skill, base_output_dir or output_dir.parent.parent, copy)
|
|
49
|
+
|
|
50
|
+
content = f"""# {skill.name}
|
|
51
|
+
|
|
52
|
+
{skill.description}
|
|
53
|
+
|
|
54
|
+
This prompt delegates to the agent skill at `{skill_path}/`.
|
|
55
|
+
|
|
56
|
+
## Skill Location
|
|
57
|
+
|
|
58
|
+
- **Path:** `{skill_path}/`
|
|
59
|
+
- **Manifest:** `{skill_path}/SKILL.md`
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
Invoke this skill by typing `/{skill.name}` in the Copilot chat.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
output_file.write_text(content, encoding="utf-8")
|
|
67
|
+
return output_file
|
|
68
|
+
|
|
69
|
+
def cleanup_stale(self, output_dir: Path, valid_names: set[str]) -> list[Path]:
|
|
70
|
+
"""Remove stale Copilot prompt files.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
output_dir: Output directory to clean (.github/prompts/).
|
|
74
|
+
valid_names: Set of valid skill names (files to keep).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of removed file paths.
|
|
78
|
+
"""
|
|
79
|
+
removed = []
|
|
80
|
+
|
|
81
|
+
if not output_dir.is_dir():
|
|
82
|
+
return removed
|
|
83
|
+
|
|
84
|
+
for file in output_dir.glob("*.prompt.md"):
|
|
85
|
+
name = file.stem.replace(".prompt", "")
|
|
86
|
+
|
|
87
|
+
if name in valid_names:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
content = file.read_text(encoding="utf-8")
|
|
92
|
+
if "This prompt delegates to the agent skill at" in content:
|
|
93
|
+
file.unlink()
|
|
94
|
+
removed.append(file)
|
|
95
|
+
except (OSError, UnicodeDecodeError):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
return removed
|
|
99
|
+
|
|
100
|
+
def generate_all(
|
|
101
|
+
self,
|
|
102
|
+
skills: list[SkillInfo],
|
|
103
|
+
output_dir: Path,
|
|
104
|
+
exclude: set[str] | None = None,
|
|
105
|
+
copy: bool = False,
|
|
106
|
+
) -> tuple[list[Path], list[Path]]:
|
|
107
|
+
"""Generate prompt files and update instructions file.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
skills: List of skills to include.
|
|
111
|
+
output_dir: Base output directory.
|
|
112
|
+
exclude: Set of skill names to exclude.
|
|
113
|
+
copy: If True, copy skills locally and use relative paths.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Tuple of (generated files, removed stale files).
|
|
117
|
+
"""
|
|
118
|
+
exclude = exclude or set()
|
|
119
|
+
|
|
120
|
+
# Generate prompt files in .github/prompts/
|
|
121
|
+
prompts_dir = self.resolve_output_dir(output_dir)
|
|
122
|
+
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
active_skills = [s for s in skills if s.name not in exclude]
|
|
125
|
+
|
|
126
|
+
# Copy skills if requested
|
|
127
|
+
if copy:
|
|
128
|
+
skills_dir = self.get_skills_dir(output_dir)
|
|
129
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
for skill in active_skills:
|
|
131
|
+
self.copy_skill(skill, skills_dir)
|
|
132
|
+
|
|
133
|
+
generated = []
|
|
134
|
+
valid_names = set()
|
|
135
|
+
|
|
136
|
+
for skill in active_skills:
|
|
137
|
+
valid_names.add(skill.name)
|
|
138
|
+
path = self.generate(skill, prompts_dir, copy=copy, base_output_dir=output_dir)
|
|
139
|
+
generated.append(path)
|
|
140
|
+
|
|
141
|
+
removed = self.cleanup_stale(prompts_dir, valid_names)
|
|
142
|
+
|
|
143
|
+
# Update .github/copilot-instructions.md with skill index
|
|
144
|
+
github_dir = prompts_dir.parent # .github/
|
|
145
|
+
instructions_file = github_dir / "copilot-instructions.md"
|
|
146
|
+
|
|
147
|
+
if active_skills:
|
|
148
|
+
self._update_instructions_file(instructions_file, active_skills)
|
|
149
|
+
generated.append(instructions_file)
|
|
150
|
+
elif instructions_file.exists():
|
|
151
|
+
self._remove_managed_section(instructions_file)
|
|
152
|
+
|
|
153
|
+
return generated, removed
|
|
154
|
+
|
|
155
|
+
def _update_instructions_file(self, file_path: Path, skills: list[SkillInfo]) -> None:
|
|
156
|
+
"""Update or create the copilot-instructions.md file."""
|
|
157
|
+
skills_content = self._build_skills_section(skills)
|
|
158
|
+
|
|
159
|
+
if file_path.exists():
|
|
160
|
+
self._update_managed_section(file_path, skills_content)
|
|
161
|
+
else:
|
|
162
|
+
content = f"""# Copilot Instructions
|
|
163
|
+
|
|
164
|
+
{self.MARKER}
|
|
165
|
+
{skills_content}
|
|
166
|
+
{self.MARKER_END}
|
|
167
|
+
"""
|
|
168
|
+
file_path.write_text(content, encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
def _build_skills_section(self, skills: list[SkillInfo]) -> str:
|
|
171
|
+
"""Build the managed skills section content."""
|
|
172
|
+
lines = ["## Available Skills", ""]
|
|
173
|
+
lines.append("The following agent skills are available. Invoke them by typing `/<skill-name>` in chat.")
|
|
174
|
+
lines.append("")
|
|
175
|
+
|
|
176
|
+
for skill in sorted(skills, key=lambda s: s.name):
|
|
177
|
+
desc = skill.description or "(no description)"
|
|
178
|
+
lines.append(f"- **/{skill.name}** — {desc}")
|
|
179
|
+
|
|
180
|
+
lines.append("")
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
def _update_managed_section(self, file_path: Path, new_content: str) -> None:
|
|
184
|
+
"""Update the managed section in an existing file."""
|
|
185
|
+
content = file_path.read_text(encoding="utf-8")
|
|
186
|
+
|
|
187
|
+
start_idx = content.find(self.MARKER)
|
|
188
|
+
end_idx = content.find(self.MARKER_END)
|
|
189
|
+
|
|
190
|
+
if start_idx != -1 and end_idx != -1:
|
|
191
|
+
before = content[:start_idx]
|
|
192
|
+
after = content[end_idx + len(self.MARKER_END) :]
|
|
193
|
+
new_full = f"{before}{self.MARKER}\n{new_content}\n{self.MARKER_END}{after}"
|
|
194
|
+
else:
|
|
195
|
+
new_full = f"{content.rstrip()}\n\n{self.MARKER}\n{new_content}\n{self.MARKER_END}\n"
|
|
196
|
+
|
|
197
|
+
file_path.write_text(new_full, encoding="utf-8")
|
|
198
|
+
|
|
199
|
+
def _remove_managed_section(self, file_path: Path) -> None:
|
|
200
|
+
"""Remove the managed section from an existing file."""
|
|
201
|
+
content = file_path.read_text(encoding="utf-8")
|
|
202
|
+
|
|
203
|
+
start_idx = content.find(self.MARKER)
|
|
204
|
+
end_idx = content.find(self.MARKER_END)
|
|
205
|
+
|
|
206
|
+
if start_idx != -1 and end_idx != -1:
|
|
207
|
+
before = content[:start_idx].rstrip()
|
|
208
|
+
after = content[end_idx + len(self.MARKER_END) :].lstrip()
|
|
209
|
+
new_content = f"{before}\n{after}".strip() + "\n"
|
|
210
|
+
file_path.write_text(new_content, encoding="utf-8")
|
adapters/cursor.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Cursor adapter for generating .cursor/commands/*.md files."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from adapters.base import BaseAdapter, SkillInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CursorAdapter(BaseAdapter):
|
|
9
|
+
"""Adapter for generating Cursor command files."""
|
|
10
|
+
|
|
11
|
+
target_name = "cursor"
|
|
12
|
+
target_subdir = ".cursor/commands"
|
|
13
|
+
|
|
14
|
+
def generate(
|
|
15
|
+
self, skill: SkillInfo, output_dir: Path, copy: bool = False, base_output_dir: Path | None = None
|
|
16
|
+
) -> Path:
|
|
17
|
+
"""Generate a Cursor command file for a skill.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
skill: Skill information.
|
|
21
|
+
output_dir: Resolved output directory (.cursor/commands/).
|
|
22
|
+
copy: If True, use relative paths to local skill copies.
|
|
23
|
+
base_output_dir: Base output directory (for computing relative paths).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Path to the generated file.
|
|
27
|
+
"""
|
|
28
|
+
output_file = output_dir / f"{skill.name}.md"
|
|
29
|
+
|
|
30
|
+
skill_path = self.get_skill_path(skill, base_output_dir or output_dir.parent.parent, copy)
|
|
31
|
+
|
|
32
|
+
content = f"""# {skill.name}
|
|
33
|
+
|
|
34
|
+
{skill.description}
|
|
35
|
+
|
|
36
|
+
This command delegates to the agent skill at `{skill_path}/`.
|
|
37
|
+
|
|
38
|
+
## Skill Location
|
|
39
|
+
|
|
40
|
+
- **Path:** `{skill_path}/`
|
|
41
|
+
- **Manifest:** `{skill_path}/SKILL.md`
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
output_file.write_text(content, encoding="utf-8")
|
|
45
|
+
return output_file
|
|
46
|
+
|
|
47
|
+
def cleanup_stale(self, output_dir: Path, valid_names: set[str]) -> list[Path]:
|
|
48
|
+
"""Remove stale Cursor command files.
|
|
49
|
+
|
|
50
|
+
Only removes files that look like generated skill commands.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
output_dir: Output directory to clean.
|
|
54
|
+
valid_names: Set of valid skill names (files to keep).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of removed file paths.
|
|
58
|
+
"""
|
|
59
|
+
removed = []
|
|
60
|
+
|
|
61
|
+
if not output_dir.is_dir():
|
|
62
|
+
return removed
|
|
63
|
+
|
|
64
|
+
for file in output_dir.glob("*.md"):
|
|
65
|
+
name = file.stem
|
|
66
|
+
|
|
67
|
+
if name in valid_names:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
content = file.read_text(encoding="utf-8")
|
|
72
|
+
if "This command delegates to the agent skill at" in content:
|
|
73
|
+
file.unlink()
|
|
74
|
+
removed.append(file)
|
|
75
|
+
except (OSError, UnicodeDecodeError):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
return removed
|
adapters/windsurf.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Windsurf adapter for generating .windsurf/workflows/*.md files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from adapters.base import BaseAdapter, SkillInfo
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WindsurfAdapter(BaseAdapter):
|
|
10
|
+
"""Adapter for generating Windsurf workflow files."""
|
|
11
|
+
|
|
12
|
+
target_name = "windsurf"
|
|
13
|
+
target_subdir = ".windsurf/workflows"
|
|
14
|
+
|
|
15
|
+
def generate(
|
|
16
|
+
self, skill: SkillInfo, output_dir: Path, copy: bool = False, base_output_dir: Path | None = None
|
|
17
|
+
) -> Path:
|
|
18
|
+
"""Generate a Windsurf workflow file for a skill.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
skill: Skill information.
|
|
22
|
+
output_dir: Resolved output directory (.windsurf/workflows/).
|
|
23
|
+
copy: If True, use relative paths to local skill copies.
|
|
24
|
+
base_output_dir: Base output directory (for computing relative paths).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path to the generated file.
|
|
28
|
+
"""
|
|
29
|
+
output_file = output_dir / f"{skill.name}.md"
|
|
30
|
+
|
|
31
|
+
skill_path = self.get_skill_path(skill, base_output_dir or output_dir.parent.parent, copy)
|
|
32
|
+
desc_yaml = json.dumps(skill.description)
|
|
33
|
+
|
|
34
|
+
content = f"""---
|
|
35
|
+
description: {desc_yaml}
|
|
36
|
+
auto_execution_mode: 1
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# {skill.name}
|
|
40
|
+
|
|
41
|
+
This workflow delegates to the agent skill at `{skill_path}/`.
|
|
42
|
+
|
|
43
|
+
## Skill Location
|
|
44
|
+
|
|
45
|
+
- **Path:** `{skill_path}/`
|
|
46
|
+
- **Manifest:** `{skill_path}/SKILL.md`
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
output_file.write_text(content, encoding="utf-8")
|
|
50
|
+
return output_file
|
|
51
|
+
|
|
52
|
+
def cleanup_stale(self, output_dir: Path, valid_names: set[str]) -> list[Path]:
|
|
53
|
+
"""Remove stale Windsurf workflow files.
|
|
54
|
+
|
|
55
|
+
Only removes files that look like generated skill workflows.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
output_dir: Output directory to clean.
|
|
59
|
+
valid_names: Set of valid skill names (files to keep).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of removed file paths.
|
|
63
|
+
"""
|
|
64
|
+
removed = []
|
|
65
|
+
|
|
66
|
+
if not output_dir.is_dir():
|
|
67
|
+
return removed
|
|
68
|
+
|
|
69
|
+
for file in output_dir.glob("*.md"):
|
|
70
|
+
name = file.stem
|
|
71
|
+
|
|
72
|
+
if name in valid_names:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
content = file.read_text(encoding="utf-8")
|
|
77
|
+
if "This workflow delegates to the agent skill at" in content:
|
|
78
|
+
file.unlink()
|
|
79
|
+
removed.append(file)
|
|
80
|
+
except (OSError, UnicodeDecodeError):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
return removed
|
cli.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""CLI entry point (argparse wiring + dispatch).
|
|
2
|
+
|
|
3
|
+
Command implementations live under `src/commands/`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from commands import adapter, clean, diff, find, registry, sync, update, use, validate
|
|
14
|
+
from commands import help as help_cmd
|
|
15
|
+
|
|
16
|
+
__version__ = "0.3.4"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
"""Main CLI entry point."""
|
|
21
|
+
parser = create_parser()
|
|
22
|
+
args = parser.parse_args(argv)
|
|
23
|
+
|
|
24
|
+
if not hasattr(args, "func"):
|
|
25
|
+
parser.print_help()
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
return args.func(args)
|
|
30
|
+
except KeyboardInterrupt:
|
|
31
|
+
print("\nInterrupted.", file=sys.stderr)
|
|
32
|
+
return 130
|
|
33
|
+
except Exception as e:
|
|
34
|
+
if args.json if hasattr(args, "json") else False:
|
|
35
|
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
|
36
|
+
else:
|
|
37
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
38
|
+
return 3
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
42
|
+
"""Create the argument parser."""
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
prog="oasr",
|
|
45
|
+
description="Open Agent Skills Registry - Manage agent skills across IDE integrations.",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--version",
|
|
49
|
+
action="version",
|
|
50
|
+
version=f"%(prog)s {__version__}",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--config",
|
|
54
|
+
type=Path,
|
|
55
|
+
help="Override config file path",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--json",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Output in JSON format",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--quiet",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Suppress info and warnings",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
69
|
+
|
|
70
|
+
# New taxonomy (v0.3.0)
|
|
71
|
+
registry.register(subparsers) # Registry operations (add, rm, sync, list)
|
|
72
|
+
diff.register(subparsers) # Show tracked skill status
|
|
73
|
+
sync.register(subparsers) # Refresh tracked skills
|
|
74
|
+
|
|
75
|
+
# Unchanged commands
|
|
76
|
+
use.register(subparsers)
|
|
77
|
+
find.register(subparsers)
|
|
78
|
+
validate.register(subparsers)
|
|
79
|
+
clean.register(subparsers)
|
|
80
|
+
adapter.register(subparsers)
|
|
81
|
+
update.register(subparsers)
|
|
82
|
+
|
|
83
|
+
# Import and register info command
|
|
84
|
+
from commands import info as info_cmd
|
|
85
|
+
|
|
86
|
+
info_cmd.register(subparsers)
|
|
87
|
+
|
|
88
|
+
help_cmd.register(subparsers, parser)
|
|
89
|
+
|
|
90
|
+
return parser
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
raise SystemExit(main())
|