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,289 @@
1
+ """ClawdHub provider implementation.
2
+
3
+ This module provides the ClawdHubProvider class which wraps the official
4
+ `clawdhub` CLI tool to provide skill search, listing, and download functionality.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ from typing import Any
13
+
14
+ from gobby.skills.hubs.base import DownloadResult, HubProvider, HubSkillDetails, HubSkillInfo
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ClawdHubProvider(HubProvider):
20
+ """Provider for ClawdHub skill registry using the CLI tool.
21
+
22
+ This provider wraps the official `clawdhub` CLI tool (installed via
23
+ `npm i -g clawdhub`) to provide access to the ClawdHub skill registry.
24
+
25
+ The CLI provides commands for:
26
+ - search: Search for skills by query
27
+ - list: List available skills
28
+ - install: Download/install a skill
29
+ - info: Get detailed skill information
30
+
31
+ Example usage:
32
+ ```python
33
+ provider = ClawdHubProvider(
34
+ hub_name="clawdhub",
35
+ base_url="https://clawdhub.com",
36
+ )
37
+
38
+ # Check if CLI is available
39
+ info = await provider.discover()
40
+ if info["cli_available"]:
41
+ results = await provider.search("commit message")
42
+ for skill in results:
43
+ print(f"{skill.slug}: {skill.description}")
44
+ ```
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ hub_name: str,
50
+ base_url: str,
51
+ auth_token: str | None = None,
52
+ ) -> None:
53
+ """Initialize the ClawdHub provider.
54
+
55
+ Args:
56
+ hub_name: The configured name for this hub instance
57
+ base_url: Base URL for ClawdHub (used for reference)
58
+ auth_token: Optional authentication token for private skills
59
+ """
60
+ super().__init__(hub_name=hub_name, base_url=base_url, auth_token=auth_token)
61
+ self._cli_available: bool | None = None
62
+
63
+ @property
64
+ def provider_type(self) -> str:
65
+ """Return the provider type identifier."""
66
+ return "clawdhub"
67
+
68
+ async def _check_cli_available(self) -> bool:
69
+ """Check if the clawdhub CLI is available.
70
+
71
+ Returns:
72
+ True if CLI is installed and accessible, False otherwise
73
+ """
74
+ try:
75
+ process = await asyncio.create_subprocess_exec(
76
+ "clawdhub",
77
+ "--version",
78
+ stdout=asyncio.subprocess.PIPE,
79
+ stderr=asyncio.subprocess.PIPE,
80
+ )
81
+ stdout, _ = await process.communicate()
82
+
83
+ if process.returncode == 0:
84
+ version = stdout.decode().strip()
85
+ logger.debug(f"ClawdHub CLI version: {version}")
86
+ return True
87
+ return False
88
+ except FileNotFoundError:
89
+ logger.warning("ClawdHub CLI not found. Install with: npm i -g clawdhub")
90
+ return False
91
+ except Exception as e:
92
+ logger.error(f"Error checking ClawdHub CLI: {e}")
93
+ return False
94
+
95
+ async def _run_cli_command(
96
+ self,
97
+ command: str,
98
+ args: list[str] | None = None,
99
+ ) -> dict[str, Any]:
100
+ """Run a clawdhub CLI command and return parsed JSON output.
101
+
102
+ Args:
103
+ command: The CLI command (search, list, install, info, etc.)
104
+ args: Additional arguments for the command
105
+
106
+ Returns:
107
+ Parsed JSON output from the CLI
108
+
109
+ Raises:
110
+ RuntimeError: If CLI is not available or command fails
111
+ """
112
+ cmd_args = ["clawdhub", command, "--json"]
113
+ if args:
114
+ cmd_args.extend(args)
115
+
116
+ # Add auth token if available
117
+ if self.auth_token:
118
+ cmd_args.extend(["--token", self.auth_token])
119
+
120
+ try:
121
+ process = await asyncio.create_subprocess_exec(
122
+ *cmd_args,
123
+ stdout=asyncio.subprocess.PIPE,
124
+ stderr=asyncio.subprocess.PIPE,
125
+ )
126
+ stdout, stderr = await process.communicate()
127
+
128
+ if process.returncode != 0:
129
+ error_msg = stderr.decode().strip() if stderr else "Unknown error"
130
+ logger.error(f"ClawdHub CLI error: {error_msg}")
131
+ raise RuntimeError(f"ClawdHub CLI command failed: {error_msg}")
132
+
133
+ output = stdout.decode().strip()
134
+ if not output:
135
+ return {}
136
+
137
+ parsed: dict[str, Any] = json.loads(output)
138
+ return parsed
139
+ except FileNotFoundError as e:
140
+ raise RuntimeError("ClawdHub CLI not found. Install with: npm i -g clawdhub") from e
141
+ except json.JSONDecodeError as e:
142
+ logger.error(f"Failed to parse CLI output: {e}")
143
+ raise RuntimeError(f"Invalid JSON from ClawdHub CLI: {e}") from e
144
+
145
+ async def discover(self) -> dict[str, Any]:
146
+ """Discover hub capabilities and check CLI availability.
147
+
148
+ Returns:
149
+ Dictionary with hub info and CLI availability status
150
+ """
151
+ cli_available = await self._check_cli_available()
152
+ self._cli_available = cli_available
153
+
154
+ return {
155
+ "hub_name": self.hub_name,
156
+ "provider_type": self.provider_type,
157
+ "cli_available": cli_available,
158
+ "base_url": self.base_url,
159
+ }
160
+
161
+ async def search(
162
+ self,
163
+ query: str,
164
+ limit: int = 20,
165
+ ) -> list[HubSkillInfo]:
166
+ """Search for skills matching a query.
167
+
168
+ Args:
169
+ query: Search query string
170
+ limit: Maximum number of results
171
+
172
+ Returns:
173
+ List of matching skills with basic info
174
+ """
175
+ result = await self._run_cli_command("search", [query, "--limit", str(limit)])
176
+
177
+ skills = result.get("skills", [])
178
+ return [
179
+ HubSkillInfo(
180
+ slug=skill.get("slug", skill.get("name", "")),
181
+ display_name=skill.get("name", skill.get("slug", "")),
182
+ description=skill.get("description", ""),
183
+ hub_name=self.hub_name,
184
+ version=skill.get("version"),
185
+ score=skill.get("score"),
186
+ )
187
+ for skill in skills
188
+ ]
189
+
190
+ async def list_skills(
191
+ self,
192
+ limit: int = 50,
193
+ offset: int = 0,
194
+ ) -> list[HubSkillInfo]:
195
+ """List available skills from the hub.
196
+
197
+ Args:
198
+ limit: Maximum number of results
199
+ offset: Number of results to skip
200
+
201
+ Returns:
202
+ List of skills with basic info
203
+ """
204
+ args = ["--limit", str(limit)]
205
+ if offset > 0:
206
+ args.extend(["--offset", str(offset)])
207
+
208
+ result = await self._run_cli_command("list", args)
209
+
210
+ skills = result.get("skills", [])
211
+ return [
212
+ HubSkillInfo(
213
+ slug=skill.get("slug", skill.get("name", "")),
214
+ display_name=skill.get("name", skill.get("slug", "")),
215
+ description=skill.get("description", ""),
216
+ hub_name=self.hub_name,
217
+ version=skill.get("version"),
218
+ )
219
+ for skill in skills
220
+ ]
221
+
222
+ async def get_skill_details(
223
+ self,
224
+ slug: str,
225
+ ) -> HubSkillDetails | None:
226
+ """Get detailed information about a specific skill.
227
+
228
+ Args:
229
+ slug: The skill's unique identifier
230
+
231
+ Returns:
232
+ Detailed skill info, or None if not found
233
+ """
234
+ try:
235
+ result = await self._run_cli_command("info", [slug])
236
+
237
+ if not result:
238
+ return None
239
+
240
+ return HubSkillDetails(
241
+ slug=result.get("slug", slug),
242
+ display_name=result.get("name", slug),
243
+ description=result.get("description", ""),
244
+ hub_name=self.hub_name,
245
+ version=result.get("version"),
246
+ latest_version=result.get("latest_version", result.get("version")),
247
+ versions=result.get("versions", []),
248
+ )
249
+ except RuntimeError:
250
+ return None
251
+
252
+ async def download_skill(
253
+ self,
254
+ slug: str,
255
+ version: str | None = None,
256
+ target_dir: str | None = None,
257
+ ) -> DownloadResult:
258
+ """Download and extract a skill from the hub.
259
+
260
+ Args:
261
+ slug: The skill's unique identifier
262
+ version: Specific version to download (None for latest)
263
+ target_dir: Directory to extract to
264
+
265
+ Returns:
266
+ DownloadResult with success status, path, version, or error
267
+ """
268
+ args = [slug]
269
+
270
+ if version:
271
+ args.extend(["--version", version])
272
+
273
+ if target_dir:
274
+ args.extend(["--output", target_dir])
275
+
276
+ try:
277
+ result = await self._run_cli_command("install", args)
278
+ return DownloadResult(
279
+ success=result.get("success", True),
280
+ slug=slug,
281
+ path=result.get("path", target_dir),
282
+ version=result.get("version", version),
283
+ )
284
+ except RuntimeError as e:
285
+ return DownloadResult(
286
+ success=False,
287
+ slug=slug,
288
+ error=str(e),
289
+ )