gobby 0.2.5__py3-none-any.whl → 0.2.7__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.
Files changed (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -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
+ )