agent-skill-manager 0.1.0__py3-none-any.whl → 0.1.1__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.
- {agent_skill_manager-0.1.0.dist-info → agent_skill_manager-0.1.1.dist-info}/METADATA +55 -19
- agent_skill_manager-0.1.1.dist-info/RECORD +12 -0
- skill_manager/__init__.py +79 -0
- skill_manager/agents.py +137 -0
- skill_manager/cli.py +1178 -0
- skill_manager/deployment.py +260 -0
- skill_manager/github.py +177 -0
- skill_manager/metadata.py +141 -0
- skill_manager/removal.py +278 -0
- skill_manager/validation.py +83 -0
- agent_skill_manager-0.1.0.dist-info/RECORD +0 -4
- {agent_skill_manager-0.1.0.dist-info → agent_skill_manager-0.1.1.dist-info}/WHEEL +0 -0
- {agent_skill_manager-0.1.0.dist-info → agent_skill_manager-0.1.1.dist-info}/entry_points.txt +0 -0
skill_manager/removal.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Skill removal and recovery functionality.
|
|
4
|
+
Handles safe deletion (move to trash), hard deletion, and restoration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .agents import get_agent_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_trash_dir(agent_id: str, deployment_type: str = "global", project_root: Path | None = None) -> Path:
|
|
15
|
+
"""
|
|
16
|
+
Get the trash directory for an agent.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
agent_id: Target agent identifier
|
|
20
|
+
deployment_type: Either "global" or "project"
|
|
21
|
+
project_root: Project root directory (required for project deployment)
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Path to the trash directory
|
|
25
|
+
"""
|
|
26
|
+
agent_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
27
|
+
return agent_path.parent / ".trash"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def soft_delete_skill(
|
|
31
|
+
skill_name: str,
|
|
32
|
+
agent_id: str,
|
|
33
|
+
deployment_type: str = "global",
|
|
34
|
+
project_root: Path | None = None,
|
|
35
|
+
) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Safely delete a skill by moving it to trash.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
skill_name: Name of the skill to delete
|
|
41
|
+
agent_id: Target agent identifier
|
|
42
|
+
deployment_type: Either "global" or "project"
|
|
43
|
+
project_root: Project root directory (required for project deployment)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if deletion succeeded, False otherwise
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
agent_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
50
|
+
skill_dir = agent_path / skill_name
|
|
51
|
+
|
|
52
|
+
if not skill_dir.exists():
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
# Create trash directory with timestamp subdirectory
|
|
56
|
+
trash_dir = get_trash_dir(agent_id, deployment_type, project_root)
|
|
57
|
+
timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
|
|
58
|
+
trash_subdir = trash_dir / timestamp
|
|
59
|
+
trash_subdir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# Move skill to trash
|
|
62
|
+
trash_dest = trash_subdir / skill_name
|
|
63
|
+
shutil.move(str(skill_dir), str(trash_dest))
|
|
64
|
+
|
|
65
|
+
# Create metadata file
|
|
66
|
+
metadata_file = trash_dest / ".trash_metadata"
|
|
67
|
+
metadata_file.write_text(
|
|
68
|
+
f"deleted_at: {timestamp}\n"
|
|
69
|
+
f"original_path: {skill_dir}\n"
|
|
70
|
+
f"agent_id: {agent_id}\n"
|
|
71
|
+
f"deployment_type: {deployment_type}\n"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def hard_delete_skill(
|
|
80
|
+
skill_name: str,
|
|
81
|
+
agent_id: str,
|
|
82
|
+
deployment_type: str = "global",
|
|
83
|
+
project_root: Path | None = None,
|
|
84
|
+
) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Permanently delete a skill.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
skill_name: Name of the skill to delete
|
|
90
|
+
agent_id: Target agent identifier
|
|
91
|
+
deployment_type: Either "global" or "project"
|
|
92
|
+
project_root: Project root directory (required for project deployment)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if deletion succeeded, False otherwise
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
agent_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
99
|
+
skill_dir = agent_path / skill_name
|
|
100
|
+
|
|
101
|
+
if not skill_dir.exists():
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
shutil.rmtree(skill_dir)
|
|
105
|
+
return True
|
|
106
|
+
except Exception:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def list_trashed_skills(
|
|
111
|
+
agent_id: str,
|
|
112
|
+
deployment_type: str = "global",
|
|
113
|
+
project_root: Path | None = None,
|
|
114
|
+
) -> list[dict]:
|
|
115
|
+
"""
|
|
116
|
+
List all skills in trash for an agent.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
agent_id: Target agent identifier
|
|
120
|
+
deployment_type: Either "global" or "project"
|
|
121
|
+
project_root: Project root directory (required for project deployment)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of dictionaries containing skill information
|
|
125
|
+
"""
|
|
126
|
+
trash_dir = get_trash_dir(agent_id, deployment_type, project_root)
|
|
127
|
+
|
|
128
|
+
if not trash_dir.exists():
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
trashed_skills = []
|
|
132
|
+
|
|
133
|
+
# Iterate through timestamp directories
|
|
134
|
+
for timestamp_dir in sorted(trash_dir.iterdir(), reverse=True):
|
|
135
|
+
if not timestamp_dir.is_dir():
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Each timestamp directory can contain multiple skills
|
|
139
|
+
for skill_dir in timestamp_dir.iterdir():
|
|
140
|
+
if not skill_dir.is_dir():
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Read metadata if available
|
|
144
|
+
metadata_file = skill_dir / ".trash_metadata"
|
|
145
|
+
deleted_at = timestamp_dir.name
|
|
146
|
+
if metadata_file.exists():
|
|
147
|
+
metadata = metadata_file.read_text()
|
|
148
|
+
for line in metadata.splitlines():
|
|
149
|
+
if line.startswith("deleted_at:"):
|
|
150
|
+
deleted_at = line.split(":", 1)[1].strip()
|
|
151
|
+
|
|
152
|
+
trashed_skills.append({
|
|
153
|
+
"skill_name": skill_dir.name,
|
|
154
|
+
"deleted_at": deleted_at,
|
|
155
|
+
"trash_path": skill_dir,
|
|
156
|
+
"timestamp_dir": timestamp_dir.name,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return trashed_skills
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def restore_skill(
|
|
163
|
+
skill_name: str,
|
|
164
|
+
timestamp: str,
|
|
165
|
+
agent_id: str,
|
|
166
|
+
deployment_type: str = "global",
|
|
167
|
+
project_root: Path | None = None,
|
|
168
|
+
) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Restore a skill from trash.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
skill_name: Name of the skill to restore
|
|
174
|
+
timestamp: Timestamp directory name
|
|
175
|
+
agent_id: Target agent identifier
|
|
176
|
+
deployment_type: Either "global" or "project"
|
|
177
|
+
project_root: Project root directory (required for project deployment)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if restoration succeeded, False otherwise
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
trash_dir = get_trash_dir(agent_id, deployment_type, project_root)
|
|
184
|
+
trashed_skill = trash_dir / timestamp / skill_name
|
|
185
|
+
|
|
186
|
+
if not trashed_skill.exists():
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
agent_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
190
|
+
restore_dest = agent_path / skill_name
|
|
191
|
+
|
|
192
|
+
# Check if destination already exists
|
|
193
|
+
if restore_dest.exists():
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
# Move back from trash
|
|
197
|
+
shutil.move(str(trashed_skill), str(restore_dest))
|
|
198
|
+
|
|
199
|
+
# Remove metadata file if it exists
|
|
200
|
+
metadata_file = restore_dest / ".trash_metadata"
|
|
201
|
+
if metadata_file.exists():
|
|
202
|
+
metadata_file.unlink()
|
|
203
|
+
|
|
204
|
+
# Clean up empty timestamp directory
|
|
205
|
+
timestamp_dir = trash_dir / timestamp
|
|
206
|
+
if timestamp_dir.exists() and not any(timestamp_dir.iterdir()):
|
|
207
|
+
timestamp_dir.rmdir()
|
|
208
|
+
|
|
209
|
+
return True
|
|
210
|
+
except Exception:
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def clean_trash(
|
|
215
|
+
agent_id: str,
|
|
216
|
+
deployment_type: str = "global",
|
|
217
|
+
project_root: Path | None = None,
|
|
218
|
+
) -> int:
|
|
219
|
+
"""
|
|
220
|
+
Permanently delete all skills in trash for an agent.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
agent_id: Target agent identifier
|
|
224
|
+
deployment_type: Either "global" or "project"
|
|
225
|
+
project_root: Project root directory (required for project deployment)
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Number of skills deleted
|
|
229
|
+
"""
|
|
230
|
+
trash_dir = get_trash_dir(agent_id, deployment_type, project_root)
|
|
231
|
+
|
|
232
|
+
if not trash_dir.exists():
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
count = 0
|
|
236
|
+
for timestamp_dir in trash_dir.iterdir():
|
|
237
|
+
if timestamp_dir.is_dir():
|
|
238
|
+
for skill_dir in timestamp_dir.iterdir():
|
|
239
|
+
if skill_dir.is_dir():
|
|
240
|
+
count += 1
|
|
241
|
+
shutil.rmtree(timestamp_dir)
|
|
242
|
+
|
|
243
|
+
return count
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def list_installed_skills(
|
|
247
|
+
agent_id: str,
|
|
248
|
+
deployment_type: str = "global",
|
|
249
|
+
project_root: Path | None = None,
|
|
250
|
+
) -> list[str]:
|
|
251
|
+
"""
|
|
252
|
+
List all installed skills for an agent.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
agent_id: Target agent identifier
|
|
256
|
+
deployment_type: Either "global" or "project"
|
|
257
|
+
project_root: Project root directory (required for project deployment)
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of skill names
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
agent_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
264
|
+
|
|
265
|
+
if not agent_path.exists():
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
skills = []
|
|
269
|
+
for item in agent_path.iterdir():
|
|
270
|
+
# Skip trash directory and non-directories
|
|
271
|
+
if item.is_dir() and item.name != ".trash" and not item.name.startswith("."):
|
|
272
|
+
# Check if it contains SKILL.md
|
|
273
|
+
if (item / "SKILL.md").exists():
|
|
274
|
+
skills.append(item.name)
|
|
275
|
+
|
|
276
|
+
return sorted(skills)
|
|
277
|
+
except Exception:
|
|
278
|
+
return []
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Skill validation utilities.
|
|
4
|
+
Validates skill directories and metadata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_skill(skill_dir: Path) -> bool:
|
|
11
|
+
"""
|
|
12
|
+
Check if a directory contains a valid skill.
|
|
13
|
+
|
|
14
|
+
A valid skill must contain a SKILL.md file.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
skill_dir: Path to the skill directory
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if the directory contains a valid skill, False otherwise
|
|
21
|
+
"""
|
|
22
|
+
skill_md = skill_dir / "SKILL.md"
|
|
23
|
+
return skill_md.exists()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_skill_name(skill_dir: Path) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Extract the skill name from a directory path.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
skill_dir: Path to the skill directory
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The skill name (directory name)
|
|
35
|
+
"""
|
|
36
|
+
return skill_dir.name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_project_root() -> Path:
|
|
40
|
+
"""
|
|
41
|
+
Find the project root directory.
|
|
42
|
+
|
|
43
|
+
Searches for a parent directory containing a 'skills' subdirectory.
|
|
44
|
+
Falls back to the current working directory if not found.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Path to the project root directory
|
|
48
|
+
"""
|
|
49
|
+
current = Path.cwd()
|
|
50
|
+
|
|
51
|
+
# Look for a parent directory containing 'skills'
|
|
52
|
+
while current != current.parent:
|
|
53
|
+
if (current / "skills").is_dir():
|
|
54
|
+
return current
|
|
55
|
+
current = current.parent
|
|
56
|
+
|
|
57
|
+
# If not found, return current directory
|
|
58
|
+
return Path.cwd()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def scan_available_skills(skills_dir: Path) -> list[Path]:
|
|
62
|
+
"""
|
|
63
|
+
Scan a directory for available skills.
|
|
64
|
+
|
|
65
|
+
A skill is identified by the presence of a SKILL.md file.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
skills_dir: Path to the skills directory
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of relative paths to skill directories (relative to skills_dir)
|
|
72
|
+
"""
|
|
73
|
+
if not skills_dir.exists():
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
skills = []
|
|
77
|
+
for skill_path in skills_dir.rglob("SKILL.md"):
|
|
78
|
+
skill_dir = skill_path.parent
|
|
79
|
+
# Calculate path relative to skills directory
|
|
80
|
+
rel_path = skill_dir.relative_to(skills_dir)
|
|
81
|
+
skills.append(rel_path)
|
|
82
|
+
|
|
83
|
+
return sorted(skills)
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
agent_skill_manager-0.1.0.dist-info/METADATA,sha256=MFWGu8q0dBnVmNQLMSZ2m28g8qVxZTVkDUOyvtLNrzA,5457
|
|
2
|
-
agent_skill_manager-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
3
|
-
agent_skill_manager-0.1.0.dist-info/entry_points.txt,sha256=ac3L07OC98p1llk259d9PUhDp-cl3ifV2nmYHGb3WO8,85
|
|
4
|
-
agent_skill_manager-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
{agent_skill_manager-0.1.0.dist-info → agent_skill_manager-0.1.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|