gobby 0.2.8__py3-none-any.whl → 0.2.11__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 (168) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +5 -28
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +64 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,234 @@
1
+ """Base classes for skill hub providers.
2
+
3
+ This module defines the abstract base class for hub providers and the
4
+ data classes used to represent skill information from hubs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ @dataclass
15
+ class DownloadResult:
16
+ """Result of downloading a skill from a hub.
17
+
18
+ Provides a typed structure for download results, ensuring consistent
19
+ return values across all hub providers.
20
+
21
+ Attributes:
22
+ success: Whether the download succeeded
23
+ slug: The skill's unique identifier
24
+ path: Path to the extracted skill (if successful)
25
+ version: The version that was downloaded
26
+ error: Error message (if failed)
27
+ """
28
+
29
+ success: bool
30
+ slug: str
31
+ path: str | None = None
32
+ version: str | None = None
33
+ error: str | None = None
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ """Convert to dictionary representation."""
37
+ return {
38
+ "success": self.success,
39
+ "path": self.path,
40
+ "slug": self.slug,
41
+ "version": self.version,
42
+ "error": self.error,
43
+ }
44
+
45
+
46
+ @dataclass
47
+ class HubSkillInfo:
48
+ """Basic information about a skill from a hub.
49
+
50
+ This is the lightweight representation returned by search and list operations.
51
+
52
+ Attributes:
53
+ slug: Unique identifier for the skill within the hub
54
+ display_name: Human-readable name for display
55
+ description: Brief description of what the skill does
56
+ hub_name: Name of the hub this skill comes from
57
+ version: Current version (if available)
58
+ score: Search relevance score (0-1, if from search)
59
+ """
60
+
61
+ slug: str
62
+ display_name: str
63
+ description: str
64
+ hub_name: str
65
+ version: str | None = None
66
+ score: float | None = None
67
+
68
+ def to_dict(self) -> dict[str, Any]:
69
+ """Convert to dictionary representation."""
70
+ return {
71
+ "slug": self.slug,
72
+ "display_name": self.display_name,
73
+ "description": self.description,
74
+ "hub_name": self.hub_name,
75
+ "version": self.version,
76
+ "score": self.score,
77
+ }
78
+
79
+
80
+ @dataclass
81
+ class HubSkillDetails(HubSkillInfo):
82
+ """Detailed information about a skill from a hub.
83
+
84
+ Extends HubSkillInfo with additional metadata available when
85
+ fetching full skill details.
86
+
87
+ Attributes:
88
+ latest_version: The most recent version available
89
+ versions: List of all available versions
90
+ """
91
+
92
+ latest_version: str | None = None
93
+ versions: list[str] = field(default_factory=list)
94
+
95
+ def to_dict(self) -> dict[str, Any]:
96
+ """Convert to dictionary representation."""
97
+ d = super().to_dict()
98
+ d["latest_version"] = self.latest_version
99
+ d["versions"] = self.versions
100
+ return d
101
+
102
+
103
+ class HubProvider(ABC):
104
+ """Abstract base class for skill hub providers.
105
+
106
+ Hub providers implement the interface for searching, listing, and
107
+ downloading skills from external registries like ClawdHub, SkillHub,
108
+ or GitHub Collections.
109
+
110
+ Subclasses must implement all abstract methods and the provider_type property.
111
+
112
+ Attributes:
113
+ hub_name: The configured name for this hub instance
114
+ base_url: Base URL for the hub's API
115
+ auth_token: Optional authentication token
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ hub_name: str,
121
+ base_url: str,
122
+ auth_token: str | None = None,
123
+ ) -> None:
124
+ """Initialize the hub provider.
125
+
126
+ Args:
127
+ hub_name: The configured name for this hub instance
128
+ base_url: Base URL for the hub's API
129
+ auth_token: Optional authentication token
130
+ """
131
+ self._hub_name = hub_name
132
+ self._base_url = base_url
133
+ self._auth_token = auth_token
134
+
135
+ @property
136
+ def hub_name(self) -> str:
137
+ """The configured name for this hub instance."""
138
+ return self._hub_name
139
+
140
+ @property
141
+ def base_url(self) -> str:
142
+ """Base URL for the hub's API."""
143
+ return self._base_url
144
+
145
+ @property
146
+ def auth_token(self) -> str | None:
147
+ """Optional authentication token."""
148
+ return self._auth_token
149
+
150
+ @property
151
+ @abstractmethod
152
+ def provider_type(self) -> str:
153
+ """The type identifier for this provider (e.g., 'clawdhub', 'skillhub')."""
154
+ ...
155
+
156
+ @abstractmethod
157
+ async def discover(self) -> dict[str, Any]:
158
+ """Discover hub capabilities and configuration.
159
+
160
+ For hubs that support discovery endpoints (e.g., .well-known/),
161
+ this method fetches and returns the hub's configuration.
162
+
163
+ Returns:
164
+ Dictionary with hub configuration including API endpoints
165
+ """
166
+ ...
167
+
168
+ @abstractmethod
169
+ async def search(
170
+ self,
171
+ query: str,
172
+ limit: int = 20,
173
+ ) -> list[HubSkillInfo]:
174
+ """Search for skills matching a query.
175
+
176
+ Args:
177
+ query: Search query string
178
+ limit: Maximum number of results to return
179
+
180
+ Returns:
181
+ List of matching skills with basic info
182
+ """
183
+ ...
184
+
185
+ @abstractmethod
186
+ async def list_skills(
187
+ self,
188
+ limit: int = 50,
189
+ offset: int = 0,
190
+ ) -> list[HubSkillInfo]:
191
+ """List available skills from the hub.
192
+
193
+ Args:
194
+ limit: Maximum number of results to return
195
+ offset: Number of results to skip (for pagination)
196
+
197
+ Returns:
198
+ List of skills with basic info
199
+ """
200
+ ...
201
+
202
+ @abstractmethod
203
+ async def get_skill_details(
204
+ self,
205
+ slug: str,
206
+ ) -> HubSkillDetails | None:
207
+ """Get detailed information about a specific skill.
208
+
209
+ Args:
210
+ slug: The skill's unique identifier
211
+
212
+ Returns:
213
+ Detailed skill info, or None if not found
214
+ """
215
+ ...
216
+
217
+ @abstractmethod
218
+ async def download_skill(
219
+ self,
220
+ slug: str,
221
+ version: str | None = None,
222
+ target_dir: str | None = None,
223
+ ) -> DownloadResult:
224
+ """Download and extract a skill from the hub.
225
+
226
+ Args:
227
+ slug: The skill's unique identifier
228
+ version: Specific version to download (None for latest)
229
+ target_dir: Directory to extract to (None for temp dir)
230
+
231
+ Returns:
232
+ DownloadResult with success status, path, version, or error
233
+ """
234
+ ...
@@ -0,0 +1,328 @@
1
+ """Claude Plugins provider implementation.
2
+
3
+ This module provides the ClaudePluginsProvider class which connects to the
4
+ claude-plugins.dev REST API for skill search, listing, and download functionality.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import tempfile
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import httpx
15
+
16
+ from gobby.skills.hubs.base import DownloadResult, HubProvider, HubSkillDetails, HubSkillInfo
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ClaudePluginsProvider(HubProvider):
22
+ """Provider for claude-plugins.dev skill registry using REST API.
23
+
24
+ This provider connects to the claude-plugins.dev API to provide access to
25
+ skills indexed from various GitHub repositories.
26
+
27
+ The API returns skills with metadata including:
28
+ - sourceUrl: GitHub URL to the skill directory
29
+ - metadata.rawFileUrl: Direct URL to the SKILL.md file
30
+
31
+ Example usage:
32
+ ```python
33
+ provider = ClaudePluginsProvider(
34
+ hub_name="claude-plugins",
35
+ base_url="https://claude-plugins.dev",
36
+ )
37
+
38
+ results = await provider.search("frontend")
39
+ for skill in results:
40
+ print(f"{skill.slug}: {skill.description}")
41
+ ```
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ hub_name: str,
47
+ base_url: str,
48
+ auth_token: str | None = None,
49
+ ) -> None:
50
+ """Initialize the Claude Plugins provider.
51
+
52
+ Args:
53
+ hub_name: The configured name for this hub instance
54
+ base_url: Base URL for the claude-plugins.dev API
55
+ auth_token: Optional API key for authentication (not currently required)
56
+ """
57
+ super().__init__(hub_name=hub_name, base_url=base_url, auth_token=auth_token)
58
+
59
+ @property
60
+ def provider_type(self) -> str:
61
+ """Return the provider type identifier."""
62
+ return "claude-plugins"
63
+
64
+ def _get_headers(self) -> dict[str, str]:
65
+ """Get HTTP headers for API requests.
66
+
67
+ Returns:
68
+ Dictionary of headers including Authorization if auth_token is set
69
+ """
70
+ headers: dict[str, str] = {
71
+ "Accept": "application/json",
72
+ }
73
+
74
+ if self.auth_token:
75
+ headers["Authorization"] = f"Bearer {self.auth_token}"
76
+
77
+ return headers
78
+
79
+ async def _make_request(
80
+ self,
81
+ endpoint: str,
82
+ params: dict[str, Any] | None = None,
83
+ ) -> dict[str, Any]:
84
+ """Make an HTTP GET request to the claude-plugins.dev API.
85
+
86
+ Args:
87
+ endpoint: API endpoint path
88
+ params: Query parameters
89
+
90
+ Returns:
91
+ Parsed JSON response
92
+
93
+ Raises:
94
+ RuntimeError: If the request fails
95
+ """
96
+ url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
97
+
98
+ async with httpx.AsyncClient() as client:
99
+ try:
100
+ response = await client.get(
101
+ url,
102
+ headers=self._get_headers(),
103
+ params=params,
104
+ timeout=30.0,
105
+ )
106
+ response.raise_for_status()
107
+ result: dict[str, Any] = response.json()
108
+ return result
109
+ except httpx.HTTPStatusError as e:
110
+ logger.error(f"claude-plugins.dev API error: {e.response.status_code}")
111
+ raise RuntimeError(f"claude-plugins.dev API error: {e.response.status_code}") from e
112
+ except httpx.RequestError as e:
113
+ logger.error(f"claude-plugins.dev request failed: {e}")
114
+ raise RuntimeError(f"claude-plugins.dev request failed: {e}") from e
115
+
116
+ def _parse_skill_info(self, skill: dict[str, Any]) -> HubSkillInfo:
117
+ """Parse a skill dict from the API into HubSkillInfo.
118
+
119
+ Args:
120
+ skill: Raw skill data from the API
121
+
122
+ Returns:
123
+ HubSkillInfo instance
124
+ """
125
+ return HubSkillInfo(
126
+ slug=skill.get("name", skill.get("id", "")),
127
+ display_name=skill.get("name", ""),
128
+ description=skill.get("description", ""),
129
+ hub_name=self.hub_name,
130
+ version=None, # API doesn't provide version info
131
+ score=skill.get("stars"), # Use stars as a relevance indicator
132
+ )
133
+
134
+ async def discover(self) -> dict[str, Any]:
135
+ """Discover hub capabilities.
136
+
137
+ Returns:
138
+ Dictionary with hub info
139
+ """
140
+ return {
141
+ "hub_name": self.hub_name,
142
+ "provider_type": self.provider_type,
143
+ "base_url": self.base_url,
144
+ "authenticated": self.auth_token is not None,
145
+ }
146
+
147
+ async def search(
148
+ self,
149
+ query: str,
150
+ limit: int = 20,
151
+ ) -> list[HubSkillInfo]:
152
+ """Search for skills matching a query.
153
+
154
+ Args:
155
+ query: Search query string
156
+ limit: Maximum number of results
157
+
158
+ Returns:
159
+ List of matching skills with basic info
160
+ """
161
+ try:
162
+ result = await self._make_request(
163
+ endpoint="/api/skills",
164
+ params={"q": query, "limit": limit},
165
+ )
166
+
167
+ skills = result.get("skills", [])
168
+ return [self._parse_skill_info(skill) for skill in skills]
169
+ except RuntimeError:
170
+ logger.warning(f"Search failed for query: {query}")
171
+ return []
172
+
173
+ async def list_skills(
174
+ self,
175
+ limit: int = 50,
176
+ offset: int = 0,
177
+ ) -> list[HubSkillInfo]:
178
+ """List available skills from the hub.
179
+
180
+ Args:
181
+ limit: Maximum number of results
182
+ offset: Number of results to skip
183
+
184
+ Returns:
185
+ List of skills with basic info
186
+ """
187
+ try:
188
+ result = await self._make_request(
189
+ endpoint="/api/skills",
190
+ params={"limit": limit, "offset": offset},
191
+ )
192
+
193
+ skills = result.get("skills", [])
194
+ return [self._parse_skill_info(skill) for skill in skills]
195
+ except RuntimeError:
196
+ logger.warning("Failed to list skills from claude-plugins.dev")
197
+ return []
198
+
199
+ async def get_skill_details(
200
+ self,
201
+ slug: str,
202
+ ) -> HubSkillDetails | None:
203
+ """Get detailed information about a specific skill.
204
+
205
+ Args:
206
+ slug: The skill's unique identifier (name)
207
+
208
+ Returns:
209
+ Detailed skill info, or None if not found
210
+ """
211
+ # Search for the specific skill by name
212
+ try:
213
+ result = await self._make_request(
214
+ endpoint="/api/skills",
215
+ params={"q": slug, "limit": 10},
216
+ )
217
+
218
+ skills = result.get("skills", [])
219
+ for skill in skills:
220
+ if skill.get("name") == slug:
221
+ return HubSkillDetails(
222
+ slug=skill.get("name", slug),
223
+ display_name=skill.get("name", slug),
224
+ description=skill.get("description", ""),
225
+ hub_name=self.hub_name,
226
+ version=None,
227
+ latest_version=None,
228
+ versions=[],
229
+ )
230
+ return None
231
+ except RuntimeError:
232
+ return None
233
+
234
+ async def download_skill(
235
+ self,
236
+ slug: str,
237
+ version: str | None = None,
238
+ target_dir: str | None = None,
239
+ ) -> DownloadResult:
240
+ """Download a skill from claude-plugins.dev.
241
+
242
+ Downloads the SKILL.md file directly using the rawFileUrl from metadata.
243
+
244
+ Args:
245
+ slug: The skill's unique identifier (name)
246
+ version: Not used (no versioning in this registry)
247
+ target_dir: Directory to save to
248
+
249
+ Returns:
250
+ DownloadResult with success status, path, or error
251
+ """
252
+ # First, find the skill to get its metadata
253
+ try:
254
+ result = await self._make_request(
255
+ endpoint="/api/skills",
256
+ params={"q": slug, "limit": 10},
257
+ )
258
+
259
+ skills = result.get("skills", [])
260
+ skill_data = None
261
+ for skill in skills:
262
+ if skill.get("name") == slug:
263
+ skill_data = skill
264
+ break
265
+
266
+ if not skill_data:
267
+ return DownloadResult(
268
+ success=False,
269
+ slug=slug,
270
+ error=f"Skill '{slug}' not found",
271
+ )
272
+
273
+ # Get the raw file URL from metadata
274
+ metadata = skill_data.get("metadata", {})
275
+ raw_url = metadata.get("rawFileUrl")
276
+
277
+ if not raw_url:
278
+ return DownloadResult(
279
+ success=False,
280
+ slug=slug,
281
+ error="No download URL available for this skill",
282
+ )
283
+
284
+ # Download the SKILL.md content
285
+ async with httpx.AsyncClient() as client:
286
+ response = await client.get(raw_url, timeout=30.0, follow_redirects=True)
287
+ response.raise_for_status()
288
+ content = response.text
289
+
290
+ # Determine target directory
291
+ if target_dir:
292
+ extract_path = Path(target_dir)
293
+ else:
294
+ extract_path = Path(tempfile.mkdtemp(prefix="claude_plugins_"))
295
+
296
+ extract_path.mkdir(parents=True, exist_ok=True)
297
+
298
+ # Write the SKILL.md file
299
+ skill_file = extract_path / "SKILL.md"
300
+ skill_file.write_text(content)
301
+
302
+ return DownloadResult(
303
+ success=True,
304
+ slug=slug,
305
+ path=str(extract_path),
306
+ version=None,
307
+ )
308
+
309
+ except httpx.HTTPStatusError as e:
310
+ logger.error(f"Failed to download skill {slug}: {e.response.status_code}")
311
+ return DownloadResult(
312
+ success=False,
313
+ slug=slug,
314
+ error=f"Download failed: HTTP {e.response.status_code}",
315
+ )
316
+ except httpx.RequestError as e:
317
+ logger.error(f"Download request failed for skill {slug}: {e}")
318
+ return DownloadResult(
319
+ success=False,
320
+ slug=slug,
321
+ error=f"Download failed: {e}",
322
+ )
323
+ except RuntimeError as e:
324
+ return DownloadResult(
325
+ success=False,
326
+ slug=slug,
327
+ error=str(e),
328
+ )