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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
+
)
|