agent-skill-manager 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -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,,