gobby 0.2.5__py3-none-any.whl → 0.2.6__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.
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex.py +43 -3
- gobby/agents/runner.py +8 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +9 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +2 -8
- gobby/cli/installers/shared.py +71 -8
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +3 -3
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/app.py +63 -1
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +6 -14
- gobby/hooks/event_handlers.py +145 -2
- gobby/hooks/hook_manager.py +48 -2
- gobby/hooks/skill_manager.py +130 -0
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +95 -33
- gobby/mcp_proxy/instructions.py +54 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -5
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/clones.py +903 -0
- gobby/mcp_proxy/tools/memory.py +1 -24
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/session_messages.py +1 -2
- gobby/mcp_proxy/tools/skills/__init__.py +631 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +5 -0
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/manager.py +11 -2
- gobby/prompts/defaults/handoff/compact.md +63 -0
- gobby/prompts/defaults/handoff/session_end.md +57 -0
- gobby/prompts/defaults/memory/extract.md +61 -0
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +23 -8
- gobby/servers/routes/admin.py +280 -0
- gobby/servers/routes/mcp/tools.py +241 -52
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +64 -5
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +258 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +180 -6
- gobby/storage/sessions.py +73 -0
- gobby/storage/skills.py +749 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +41 -6
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +39 -4
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/validation.py +24 -15
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/workflows/actions.py +84 -2
- gobby/workflows/context_actions.py +43 -0
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/engine.py +13 -2
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/loader.py +19 -6
- gobby/workflows/memory_actions.py +74 -0
- gobby/workflows/summary_actions.py +17 -0
- gobby/workflows/task_enforcement_actions.py +448 -6
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/top_level.txt +0 -0
gobby/skills/updater.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""SkillUpdater - Refresh skills from their source.
|
|
2
|
+
|
|
3
|
+
This module provides the SkillUpdater class for updating skills from:
|
|
4
|
+
- Local filesystem paths
|
|
5
|
+
- GitHub repositories
|
|
6
|
+
- ZIP archives (future)
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Backup before update with automatic rollback on failure
|
|
10
|
+
- Support for bulk update_all() operations
|
|
11
|
+
- Graceful handling of missing or invalid sources
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from gobby.skills.loader import (
|
|
22
|
+
SkillLoader,
|
|
23
|
+
SkillLoadError,
|
|
24
|
+
clone_skill_repo,
|
|
25
|
+
parse_github_url,
|
|
26
|
+
)
|
|
27
|
+
from gobby.skills.parser import ParsedSkill, SkillParseError, parse_skill_file
|
|
28
|
+
from gobby.skills.validator import SkillValidator
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from gobby.storage.skills import LocalSkillManager, Skill
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SkillUpdateError(Exception):
|
|
37
|
+
"""Error updating a skill from source."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, skill_id: str | None = None):
|
|
40
|
+
self.skill_id = skill_id
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SkillUpdateResult:
|
|
46
|
+
"""Result of a skill update operation.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
skill_id: ID of the skill that was updated
|
|
50
|
+
skill_name: Name of the skill
|
|
51
|
+
success: Whether the update succeeded
|
|
52
|
+
updated: Whether the skill content changed
|
|
53
|
+
error: Error message if update failed
|
|
54
|
+
backup_created: Whether a backup was created
|
|
55
|
+
rolled_back: Whether changes were rolled back
|
|
56
|
+
skipped: Whether update was skipped (no source)
|
|
57
|
+
skip_reason: Reason for skipping
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
skill_id: str
|
|
61
|
+
skill_name: str
|
|
62
|
+
success: bool = True
|
|
63
|
+
updated: bool = False
|
|
64
|
+
error: str | None = None
|
|
65
|
+
backup_created: bool = False
|
|
66
|
+
rolled_back: bool = False
|
|
67
|
+
skipped: bool = False
|
|
68
|
+
skip_reason: str | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class _SkillBackup:
|
|
73
|
+
"""Internal backup of skill data for rollback."""
|
|
74
|
+
|
|
75
|
+
skill_id: str
|
|
76
|
+
name: str
|
|
77
|
+
description: str
|
|
78
|
+
content: str
|
|
79
|
+
version: str | None
|
|
80
|
+
license: str | None
|
|
81
|
+
compatibility: str | None
|
|
82
|
+
allowed_tools: list[str] | None
|
|
83
|
+
metadata: dict[str, Any] | None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SkillUpdater:
|
|
87
|
+
"""Update skills from their source locations.
|
|
88
|
+
|
|
89
|
+
This class handles:
|
|
90
|
+
- Fetching latest version from source (local, GitHub)
|
|
91
|
+
- Backing up current version before update
|
|
92
|
+
- Rolling back on validation/parse failures
|
|
93
|
+
- Bulk update of all sourceable skills
|
|
94
|
+
|
|
95
|
+
Example usage:
|
|
96
|
+
```python
|
|
97
|
+
from gobby.skills.updater import SkillUpdater
|
|
98
|
+
from gobby.storage.skills import LocalSkillManager
|
|
99
|
+
|
|
100
|
+
storage = LocalSkillManager(db)
|
|
101
|
+
updater = SkillUpdater(storage)
|
|
102
|
+
|
|
103
|
+
# Update a single skill
|
|
104
|
+
result = updater.update_skill(skill_id)
|
|
105
|
+
if result.success:
|
|
106
|
+
print(f"Updated {result.skill_name}")
|
|
107
|
+
|
|
108
|
+
# Update all skills with sources
|
|
109
|
+
results = updater.update_all()
|
|
110
|
+
for r in results:
|
|
111
|
+
print(f"{r.skill_name}: {'updated' if r.updated else 'unchanged'}")
|
|
112
|
+
```
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
storage: LocalSkillManager,
|
|
118
|
+
):
|
|
119
|
+
"""Initialize the updater.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
storage: Storage manager for skill CRUD operations
|
|
123
|
+
"""
|
|
124
|
+
self._storage = storage
|
|
125
|
+
self._loader = SkillLoader()
|
|
126
|
+
self._validator = SkillValidator()
|
|
127
|
+
|
|
128
|
+
def update_skill(
|
|
129
|
+
self,
|
|
130
|
+
skill_id: str,
|
|
131
|
+
cache_dir: Path | None = None,
|
|
132
|
+
) -> SkillUpdateResult:
|
|
133
|
+
"""Update a skill from its source.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
skill_id: ID of the skill to update
|
|
137
|
+
cache_dir: Optional cache directory for GitHub repos
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
SkillUpdateResult with update status
|
|
141
|
+
"""
|
|
142
|
+
# Get current skill
|
|
143
|
+
try:
|
|
144
|
+
skill = self._storage.get_skill(skill_id)
|
|
145
|
+
except ValueError as e:
|
|
146
|
+
return SkillUpdateResult(
|
|
147
|
+
skill_id=skill_id,
|
|
148
|
+
skill_name="unknown",
|
|
149
|
+
success=False,
|
|
150
|
+
error=f"Skill not found: {e}",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Check if skill has a source
|
|
154
|
+
if not skill.source_path or not skill.source_type:
|
|
155
|
+
return SkillUpdateResult(
|
|
156
|
+
skill_id=skill_id,
|
|
157
|
+
skill_name=skill.name,
|
|
158
|
+
success=True,
|
|
159
|
+
updated=False,
|
|
160
|
+
skipped=True,
|
|
161
|
+
skip_reason="No source path or source type",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Create backup
|
|
165
|
+
backup = self._create_backup(skill)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
# Fetch updated content based on source type
|
|
169
|
+
if skill.source_type == "github":
|
|
170
|
+
parsed = self._fetch_from_github(skill, cache_dir)
|
|
171
|
+
elif skill.source_type in ("local", "filesystem"):
|
|
172
|
+
parsed = self._fetch_from_local(skill)
|
|
173
|
+
else:
|
|
174
|
+
return SkillUpdateResult(
|
|
175
|
+
skill_id=skill_id,
|
|
176
|
+
skill_name=skill.name,
|
|
177
|
+
success=True,
|
|
178
|
+
updated=False,
|
|
179
|
+
skipped=True,
|
|
180
|
+
skip_reason=f"Unknown source type: {skill.source_type}",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Check if content changed
|
|
184
|
+
if not self._has_changes(skill, parsed):
|
|
185
|
+
return SkillUpdateResult(
|
|
186
|
+
skill_id=skill_id,
|
|
187
|
+
skill_name=skill.name,
|
|
188
|
+
success=True,
|
|
189
|
+
updated=False,
|
|
190
|
+
backup_created=True,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Validate the updated skill
|
|
194
|
+
validation = self._validator.validate(parsed)
|
|
195
|
+
if not validation.valid:
|
|
196
|
+
raise SkillUpdateError(
|
|
197
|
+
f"Validation failed: {'; '.join(validation.errors)}",
|
|
198
|
+
skill_id=skill_id,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Apply update
|
|
202
|
+
self._apply_update(skill, parsed)
|
|
203
|
+
|
|
204
|
+
return SkillUpdateResult(
|
|
205
|
+
skill_id=skill_id,
|
|
206
|
+
skill_name=skill.name,
|
|
207
|
+
success=True,
|
|
208
|
+
updated=True,
|
|
209
|
+
backup_created=True,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
except (SkillLoadError, SkillParseError, SkillUpdateError) as e:
|
|
213
|
+
# Rollback on failure
|
|
214
|
+
rollback_succeeded = self._restore_backup(backup)
|
|
215
|
+
return SkillUpdateResult(
|
|
216
|
+
skill_id=skill_id,
|
|
217
|
+
skill_name=skill.name,
|
|
218
|
+
success=False,
|
|
219
|
+
error=str(e),
|
|
220
|
+
backup_created=True,
|
|
221
|
+
rolled_back=rollback_succeeded,
|
|
222
|
+
)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
# Rollback on unexpected errors too
|
|
225
|
+
rollback_succeeded = self._restore_backup(backup)
|
|
226
|
+
logger.exception(f"Unexpected error updating skill {skill_id}")
|
|
227
|
+
return SkillUpdateResult(
|
|
228
|
+
skill_id=skill_id,
|
|
229
|
+
skill_name=skill.name,
|
|
230
|
+
success=False,
|
|
231
|
+
error=f"Unexpected error: {e}",
|
|
232
|
+
backup_created=True,
|
|
233
|
+
rolled_back=rollback_succeeded,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def update_all(
|
|
237
|
+
self,
|
|
238
|
+
cache_dir: Path | None = None,
|
|
239
|
+
) -> list[SkillUpdateResult]:
|
|
240
|
+
"""Update all skills that have sources.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
cache_dir: Optional cache directory for GitHub repos
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of SkillUpdateResult for each updated skill
|
|
247
|
+
"""
|
|
248
|
+
results: list[SkillUpdateResult] = []
|
|
249
|
+
|
|
250
|
+
# Get all skills
|
|
251
|
+
skills = self._storage.list_skills(limit=10000)
|
|
252
|
+
|
|
253
|
+
for skill in skills:
|
|
254
|
+
# Skip skills without sources
|
|
255
|
+
if not skill.source_path or not skill.source_type:
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
result = self.update_skill(skill.id, cache_dir=cache_dir)
|
|
259
|
+
results.append(result)
|
|
260
|
+
|
|
261
|
+
return results
|
|
262
|
+
|
|
263
|
+
def _create_backup(self, skill: Skill) -> _SkillBackup:
|
|
264
|
+
"""Create a backup of skill data for rollback."""
|
|
265
|
+
return _SkillBackup(
|
|
266
|
+
skill_id=skill.id,
|
|
267
|
+
name=skill.name,
|
|
268
|
+
description=skill.description,
|
|
269
|
+
content=skill.content,
|
|
270
|
+
version=skill.version,
|
|
271
|
+
license=skill.license,
|
|
272
|
+
compatibility=skill.compatibility,
|
|
273
|
+
allowed_tools=skill.allowed_tools,
|
|
274
|
+
metadata=skill.metadata,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _restore_backup(self, backup: _SkillBackup) -> bool:
|
|
278
|
+
"""Restore skill from backup.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if restore succeeded, False if it failed
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
self._storage.update_skill(
|
|
285
|
+
skill_id=backup.skill_id,
|
|
286
|
+
name=backup.name,
|
|
287
|
+
description=backup.description,
|
|
288
|
+
content=backup.content,
|
|
289
|
+
version=backup.version,
|
|
290
|
+
license=backup.license,
|
|
291
|
+
compatibility=backup.compatibility,
|
|
292
|
+
allowed_tools=backup.allowed_tools,
|
|
293
|
+
metadata=backup.metadata,
|
|
294
|
+
)
|
|
295
|
+
return True
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Failed to restore backup for skill {backup.skill_id}: {e}")
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
def _fetch_from_local(self, skill: Skill) -> ParsedSkill:
|
|
301
|
+
"""Fetch updated skill from local filesystem."""
|
|
302
|
+
if skill.source_path is None:
|
|
303
|
+
raise SkillLoadError("Source path is not set")
|
|
304
|
+
source_path = Path(skill.source_path)
|
|
305
|
+
|
|
306
|
+
# Check if path exists
|
|
307
|
+
if not source_path.exists():
|
|
308
|
+
raise SkillLoadError("Source path not found", source_path)
|
|
309
|
+
|
|
310
|
+
# Load from directory or file
|
|
311
|
+
if source_path.is_dir():
|
|
312
|
+
skill_file = source_path / "SKILL.md"
|
|
313
|
+
if not skill_file.exists():
|
|
314
|
+
raise SkillLoadError("SKILL.md not found in source directory", source_path)
|
|
315
|
+
return parse_skill_file(skill_file)
|
|
316
|
+
else:
|
|
317
|
+
return parse_skill_file(source_path)
|
|
318
|
+
|
|
319
|
+
def _fetch_from_github(
|
|
320
|
+
self,
|
|
321
|
+
skill: Skill,
|
|
322
|
+
cache_dir: Path | None = None,
|
|
323
|
+
) -> ParsedSkill:
|
|
324
|
+
"""Fetch updated skill from GitHub."""
|
|
325
|
+
# Parse the source path to get GitHub ref
|
|
326
|
+
if skill.source_path is None:
|
|
327
|
+
raise SkillLoadError("Source path is not set")
|
|
328
|
+
source: str = skill.source_path
|
|
329
|
+
if source.startswith("github:"):
|
|
330
|
+
source = source[7:] # Remove github: prefix
|
|
331
|
+
|
|
332
|
+
# Add branch if stored
|
|
333
|
+
if skill.source_ref:
|
|
334
|
+
if "#" not in source:
|
|
335
|
+
source = f"{source}#{skill.source_ref}"
|
|
336
|
+
|
|
337
|
+
ref = parse_github_url(source)
|
|
338
|
+
repo_path = clone_skill_repo(ref, cache_dir=cache_dir)
|
|
339
|
+
|
|
340
|
+
# Determine skill path in repo
|
|
341
|
+
if ref.path:
|
|
342
|
+
skill_path = repo_path / ref.path
|
|
343
|
+
else:
|
|
344
|
+
skill_path = repo_path
|
|
345
|
+
|
|
346
|
+
# Load the skill
|
|
347
|
+
skill_file = skill_path / "SKILL.md"
|
|
348
|
+
if not skill_file.exists():
|
|
349
|
+
# Maybe it's a single-skill repo
|
|
350
|
+
skill_file = skill_path
|
|
351
|
+
if not skill_file.exists() or skill_file.is_dir():
|
|
352
|
+
skill_file = skill_path / "SKILL.md"
|
|
353
|
+
|
|
354
|
+
if skill_file.is_dir():
|
|
355
|
+
skill_file = skill_file / "SKILL.md"
|
|
356
|
+
|
|
357
|
+
if not skill_file.exists():
|
|
358
|
+
raise SkillLoadError("SKILL.md not found in repository", repo_path)
|
|
359
|
+
|
|
360
|
+
return parse_skill_file(skill_file)
|
|
361
|
+
|
|
362
|
+
def _has_changes(self, skill: Skill, parsed: ParsedSkill) -> bool:
|
|
363
|
+
"""Check if the parsed skill differs from current."""
|
|
364
|
+
return (
|
|
365
|
+
skill.description != parsed.description
|
|
366
|
+
or skill.content != parsed.content
|
|
367
|
+
or skill.version != parsed.version
|
|
368
|
+
or skill.license != parsed.license
|
|
369
|
+
or skill.compatibility != parsed.compatibility
|
|
370
|
+
or skill.allowed_tools != parsed.allowed_tools
|
|
371
|
+
or skill.metadata != parsed.metadata
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def _apply_update(self, skill: Skill, parsed: ParsedSkill) -> None:
|
|
375
|
+
"""Apply parsed skill data to storage."""
|
|
376
|
+
self._storage.update_skill(
|
|
377
|
+
skill_id=skill.id,
|
|
378
|
+
description=parsed.description,
|
|
379
|
+
content=parsed.content,
|
|
380
|
+
version=parsed.version,
|
|
381
|
+
license=parsed.license,
|
|
382
|
+
compatibility=parsed.compatibility,
|
|
383
|
+
allowed_tools=parsed.allowed_tools,
|
|
384
|
+
metadata=parsed.metadata,
|
|
385
|
+
)
|