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
gobby/servers/http.py CHANGED
@@ -16,7 +16,7 @@ from fastapi import FastAPI, HTTPException, Request
16
16
  from fastapi.middleware.cors import CORSMiddleware
17
17
  from fastapi.responses import JSONResponse
18
18
 
19
- from gobby.adapters.codex import CodexAdapter
19
+ from gobby.adapters.codex_impl.adapter import CodexAdapter
20
20
  from gobby.hooks.broadcaster import HookEventBroadcaster
21
21
  from gobby.hooks.hook_manager import HookManager
22
22
  from gobby.llm import LLMService, create_llm_service
@@ -25,9 +25,6 @@ from gobby.mcp_proxy.semantic_search import SemanticToolSearch
25
25
  from gobby.mcp_proxy.server import GobbyDaemonTools, create_mcp_server
26
26
  from gobby.mcp_proxy.services.tool_filter import ToolFilterService
27
27
  from gobby.memory.manager import MemoryManager
28
-
29
- # Re-export for backward compatibility
30
- from gobby.servers.models import SessionRegisterRequest # noqa: F401
31
28
  from gobby.storage.sessions import LocalSessionManager
32
29
  from gobby.storage.tasks import LocalTaskManager
33
30
  from gobby.sync.tasks import TaskSyncManager
@@ -62,11 +59,13 @@ class HTTPServer:
62
59
  memory_manager: "MemoryManager | None" = None,
63
60
  llm_service: "LLMService | None" = None,
64
61
  memory_sync_manager: Any | None = None,
65
- task_expander: Any | None = None,
66
62
  task_validator: Any | None = None,
67
63
  metrics_manager: Any | None = None,
68
64
  agent_runner: Any | None = None,
69
65
  worktree_storage: Any | None = None,
66
+ clone_storage: Any | None = None,
67
+ git_manager: Any | None = None,
68
+ project_id: str | None = None,
70
69
  ) -> None:
71
70
  """
72
71
  Initialize HTTP server.
@@ -103,11 +102,13 @@ class HTTPServer:
103
102
  self.websocket_server = websocket_server
104
103
  self.llm_service = llm_service
105
104
  self.memory_sync_manager = memory_sync_manager
106
- self.task_expander = task_expander
107
105
  self.task_validator = task_validator
108
106
  self.metrics_manager = metrics_manager
109
107
  self.agent_runner = agent_runner
110
108
  self.worktree_storage = worktree_storage
109
+ self.clone_storage = clone_storage
110
+ self.git_manager = git_manager
111
+ self.project_id = project_id
111
112
 
112
113
  # Initialize WebSocket broadcaster
113
114
  # Note: websocket_server might be None if disabled
@@ -132,7 +133,7 @@ class HTTPServer:
132
133
  self._mcp_db_manager = mcp_db_manager
133
134
  if mcp_manager:
134
135
  # Determine WebSocket port
135
- ws_port = 8766
136
+ ws_port = 60888
136
137
  if config and hasattr(config, "websocket") and config.websocket:
137
138
  ws_port = config.websocket.port
138
139
 
@@ -147,14 +148,17 @@ class HTTPServer:
147
148
  # Create merge managers if db available
148
149
  merge_storage = None
149
150
  merge_resolver = None
151
+ inter_session_message_manager = None
150
152
  if mcp_db_manager:
153
+ from gobby.storage.inter_session_messages import InterSessionMessageManager
151
154
  from gobby.storage.merge_resolutions import MergeResolutionManager
152
155
  from gobby.worktrees.merge.resolver import MergeResolver
153
156
 
154
157
  merge_storage = MergeResolutionManager(mcp_db_manager.db)
155
158
  merge_resolver = MergeResolver()
156
159
  merge_resolver._llm_service = self.llm_service
157
- logger.debug("Merge resolution subsystems initialized")
160
+ inter_session_message_manager = InterSessionMessageManager(mcp_db_manager.db)
161
+ logger.debug("Merge resolution and inter-session messaging subsystems initialized")
158
162
 
159
163
  # Setup internal registries (gobby-tasks, gobby-memory, etc.)
160
164
  self._internal_manager = setup_internal_registries(
@@ -163,7 +167,6 @@ class HTTPServer:
163
167
  memory_manager=memory_manager,
164
168
  task_manager=task_manager,
165
169
  sync_manager=task_sync_manager,
166
- task_expander=self.task_expander,
167
170
  task_validator=self.task_validator,
168
171
  message_manager=message_manager,
169
172
  local_session_manager=session_manager,
@@ -171,11 +174,13 @@ class HTTPServer:
171
174
  llm_service=self.llm_service,
172
175
  agent_runner=self.agent_runner,
173
176
  worktree_storage=self.worktree_storage,
174
- git_manager=None, # Created per-project, not at daemon startup
177
+ clone_storage=self.clone_storage,
178
+ git_manager=self.git_manager,
175
179
  merge_storage=merge_storage,
176
180
  merge_resolver=merge_resolver,
177
- project_id=None, # Project-specific, not global
181
+ project_id=self.project_id,
178
182
  tool_proxy_getter=tool_proxy_getter,
183
+ inter_session_message_manager=inter_session_message_manager,
179
184
  )
180
185
  registry_count = len(self._internal_manager)
181
186
  logger.debug(f"Internal registries initialized: {registry_count} registries")
@@ -235,6 +240,13 @@ class HTTPServer:
235
240
  self._metrics = get_metrics_collector()
236
241
  self._daemon: Any = None # Set externally by daemon
237
242
 
243
+ @property
244
+ def tool_proxy(self) -> Any:
245
+ """Get the ToolProxyService instance for routing tool calls with error enrichment."""
246
+ if self._tools_handler is not None:
247
+ return self._tools_handler.tool_proxy
248
+ return None
249
+
238
250
  def _resolve_project_id(self, project_id: str | None, cwd: str | None) -> str:
239
251
  """
240
252
  Resolve project_id from cwd if not provided.
@@ -548,7 +560,7 @@ class HTTPServer:
548
560
 
549
561
 
550
562
  async def create_server(
551
- port: int = 8765,
563
+ port: int = 60887,
552
564
  test_mode: bool = False,
553
565
  mcp_manager: Any | None = None,
554
566
  config: Any | None = None,
@@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Any
13
13
  import psutil
14
14
  from fastapi import APIRouter
15
15
  from fastapi.responses import PlainTextResponse
16
+ from pydantic import BaseModel
16
17
 
17
18
  from gobby.utils.metrics import Counter, get_metrics_collector
18
19
  from gobby.utils.version import get_version
@@ -174,6 +175,19 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
174
175
  except Exception as e:
175
176
  logger.warning(f"Failed to get memory stats: {e}")
176
177
 
178
+ # Get skills statistics
179
+ skills_stats: dict[str, Any] = {"total": 0}
180
+ if server._internal_manager:
181
+ try:
182
+ for registry in server._internal_manager.get_all_registries():
183
+ if registry.name == "gobby-skills":
184
+ result = await registry.call("list_skills", {"limit": 10000})
185
+ if result.get("success"):
186
+ skills_stats["total"] = result.get("count", 0)
187
+ break
188
+ except Exception as e:
189
+ logger.warning(f"Failed to get skills stats: {e}")
190
+
177
191
  # Get plugin status
178
192
  plugin_stats: dict[str, Any] = {"enabled": False, "loaded": 0, "handlers": 0}
179
193
  if hasattr(server, "_hook_manager") and server._hook_manager is not None:
@@ -217,6 +231,7 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
217
231
  "sessions": session_stats,
218
232
  "tasks": task_stats,
219
233
  "memory": memory_stats,
234
+ "skills": skills_stats,
220
235
  "plugins": plugin_stats,
221
236
  "response_time_ms": response_time_ms,
222
237
  }
@@ -413,4 +428,283 @@ def create_admin_router(server: "HTTPServer") -> APIRouter:
413
428
 
414
429
  raise HTTPException(status_code=500, detail=str(e)) from e
415
430
 
431
+ # --- Test endpoints (for E2E testing) ---
432
+
433
+ class TestProjectRegisterRequest(BaseModel):
434
+ """Request model for registering a test project."""
435
+
436
+ project_id: str
437
+ name: str
438
+ repo_path: str | None = None
439
+
440
+ @router.post("/test/register-project")
441
+ async def register_test_project(request: TestProjectRegisterRequest) -> dict[str, Any]:
442
+ """
443
+ Register a test project in the database.
444
+
445
+ This endpoint is for E2E testing. It ensures the project exists
446
+ in the projects table so sessions can be created with valid project_ids.
447
+
448
+ Args:
449
+ request: Project registration details
450
+
451
+ Returns:
452
+ Registration confirmation
453
+ """
454
+ from fastapi import HTTPException
455
+
456
+ from gobby.storage.projects import LocalProjectManager
457
+
458
+ # Guard: Only available in test mode
459
+ if not server.test_mode:
460
+ raise HTTPException(
461
+ status_code=403, detail="Test endpoints only available in test mode"
462
+ )
463
+
464
+ start_time = time.perf_counter()
465
+ metrics = get_metrics_collector()
466
+ metrics.inc_counter("http_requests_total")
467
+
468
+ try:
469
+ # Use server's session manager database to avoid creating separate connections
470
+ if server.session_manager is None:
471
+ raise HTTPException(status_code=503, detail="Session manager not available")
472
+
473
+ db = server.session_manager.db
474
+
475
+ project_manager = LocalProjectManager(db)
476
+
477
+ # Check if project exists
478
+ existing = project_manager.get(request.project_id)
479
+ if existing:
480
+ return {
481
+ "status": "already_exists",
482
+ "project_id": existing.id,
483
+ "name": existing.name,
484
+ }
485
+
486
+ # Create the project with the specific ID
487
+ from datetime import UTC, datetime
488
+
489
+ now = datetime.now(UTC).isoformat()
490
+ db.execute(
491
+ """
492
+ INSERT INTO projects (id, name, repo_path, created_at, updated_at)
493
+ VALUES (?, ?, ?, ?, ?)
494
+ """,
495
+ (request.project_id, request.name, request.repo_path, now, now),
496
+ )
497
+
498
+ response_time_ms = (time.perf_counter() - start_time) * 1000
499
+
500
+ return {
501
+ "status": "success",
502
+ "message": f"Registered test project {request.project_id}",
503
+ "project_id": request.project_id,
504
+ "name": request.name,
505
+ "response_time_ms": response_time_ms,
506
+ }
507
+
508
+ except Exception as e:
509
+ metrics.inc_counter("http_requests_errors_total")
510
+ logger.error(f"Error registering test project: {e}", exc_info=True)
511
+ from fastapi import HTTPException
512
+
513
+ raise HTTPException(status_code=500, detail=str(e)) from e
514
+
515
+ class TestAgentRegisterRequest(BaseModel):
516
+ """Request model for registering a test agent."""
517
+
518
+ run_id: str
519
+ session_id: str
520
+ parent_session_id: str
521
+ mode: str = "terminal"
522
+
523
+ @router.post("/test/register-agent")
524
+ async def register_test_agent(request: TestAgentRegisterRequest) -> dict[str, Any]:
525
+ """
526
+ Register a test agent in the running agent registry.
527
+
528
+ This endpoint is for E2E testing of inter-agent messaging.
529
+ It allows tests to set up parent-child agent relationships
530
+ without actually spawning agent processes.
531
+
532
+ Args:
533
+ request: Agent registration details
534
+
535
+ Returns:
536
+ Registration confirmation
537
+ """
538
+ from fastapi import HTTPException
539
+
540
+ from gobby.agents.registry import RunningAgent, get_running_agent_registry
541
+
542
+ # Guard: Only available in test mode
543
+ if not server.test_mode:
544
+ raise HTTPException(
545
+ status_code=403, detail="Test endpoints only available in test mode"
546
+ )
547
+
548
+ start_time = time.perf_counter()
549
+ metrics = get_metrics_collector()
550
+ metrics.inc_counter("http_requests_total")
551
+
552
+ try:
553
+ registry = get_running_agent_registry()
554
+
555
+ # Create and register the agent
556
+ running_agent = RunningAgent(
557
+ run_id=request.run_id,
558
+ session_id=request.session_id,
559
+ parent_session_id=request.parent_session_id,
560
+ mode=request.mode,
561
+ )
562
+ registry.add(running_agent)
563
+
564
+ response_time_ms = (time.perf_counter() - start_time) * 1000
565
+
566
+ return {
567
+ "status": "success",
568
+ "message": f"Registered test agent {request.run_id}",
569
+ "agent": running_agent.to_dict(),
570
+ "response_time_ms": response_time_ms,
571
+ }
572
+
573
+ except Exception as e:
574
+ metrics.inc_counter("http_requests_errors_total")
575
+ logger.error(f"Error registering test agent: {e}", exc_info=True)
576
+ from fastapi import HTTPException
577
+
578
+ raise HTTPException(status_code=500, detail=str(e)) from e
579
+
580
+ @router.delete("/test/unregister-agent/{run_id}")
581
+ async def unregister_test_agent(run_id: str) -> dict[str, Any]:
582
+ """
583
+ Unregister a test agent from the running agent registry.
584
+
585
+ Args:
586
+ run_id: The agent run ID to remove
587
+
588
+ Returns:
589
+ Unregistration confirmation
590
+ """
591
+ from fastapi import HTTPException
592
+
593
+ from gobby.agents.registry import get_running_agent_registry
594
+
595
+ # Guard: Only available in test mode
596
+ if not server.test_mode:
597
+ raise HTTPException(
598
+ status_code=403, detail="Test endpoints only available in test mode"
599
+ )
600
+
601
+ start_time = time.perf_counter()
602
+ metrics = get_metrics_collector()
603
+ metrics.inc_counter("http_requests_total")
604
+
605
+ try:
606
+ registry = get_running_agent_registry()
607
+ agent = registry.remove(run_id)
608
+
609
+ response_time_ms = (time.perf_counter() - start_time) * 1000
610
+
611
+ if agent:
612
+ return {
613
+ "status": "success",
614
+ "message": f"Unregistered test agent {run_id}",
615
+ "response_time_ms": response_time_ms,
616
+ }
617
+ else:
618
+ return {
619
+ "status": "not_found",
620
+ "message": f"Agent {run_id} not found in registry",
621
+ "response_time_ms": response_time_ms,
622
+ }
623
+
624
+ except Exception as e:
625
+ metrics.inc_counter("http_requests_errors_total")
626
+ logger.error(f"Error unregistering test agent: {e}", exc_info=True)
627
+ from fastapi import HTTPException
628
+
629
+ raise HTTPException(status_code=500, detail=str(e)) from e
630
+
631
+ class TestSessionUsageRequest(BaseModel):
632
+ """Request body for setting test session usage."""
633
+
634
+ session_id: str
635
+ input_tokens: int = 0
636
+ output_tokens: int = 0
637
+ cache_creation_tokens: int = 0
638
+ cache_read_tokens: int = 0
639
+ total_cost_usd: float = 0.0
640
+
641
+ @router.post("/test/set-session-usage")
642
+ async def set_test_session_usage(request: TestSessionUsageRequest) -> dict[str, Any]:
643
+ """
644
+ Set usage statistics for a test session.
645
+
646
+ This endpoint is for E2E testing of token budget throttling.
647
+ It allows tests to set session usage values to simulate
648
+ budget consumption.
649
+
650
+ Args:
651
+ request: Session usage details
652
+
653
+ Returns:
654
+ Update confirmation
655
+ """
656
+ from fastapi import HTTPException
657
+
658
+ # Guard: Only available in test mode
659
+ if not server.test_mode:
660
+ raise HTTPException(
661
+ status_code=403, detail="Test endpoints only available in test mode"
662
+ )
663
+
664
+ start_time = time.perf_counter()
665
+ metrics = get_metrics_collector()
666
+ metrics.inc_counter("http_requests_total")
667
+
668
+ try:
669
+ if server.session_manager is None:
670
+ raise HTTPException(status_code=503, detail="Session manager not available")
671
+
672
+ success = server.session_manager.update_usage(
673
+ session_id=request.session_id,
674
+ input_tokens=request.input_tokens,
675
+ output_tokens=request.output_tokens,
676
+ cache_creation_tokens=request.cache_creation_tokens,
677
+ cache_read_tokens=request.cache_read_tokens,
678
+ total_cost_usd=request.total_cost_usd,
679
+ )
680
+
681
+ response_time_ms = (time.perf_counter() - start_time) * 1000
682
+
683
+ if success:
684
+ return {
685
+ "status": "success",
686
+ "session_id": request.session_id,
687
+ "usage_set": {
688
+ "input_tokens": request.input_tokens,
689
+ "output_tokens": request.output_tokens,
690
+ "cache_creation_tokens": request.cache_creation_tokens,
691
+ "cache_read_tokens": request.cache_read_tokens,
692
+ "total_cost_usd": request.total_cost_usd,
693
+ },
694
+ "response_time_ms": response_time_ms,
695
+ }
696
+ else:
697
+ return {
698
+ "status": "not_found",
699
+ "message": f"Session {request.session_id} not found",
700
+ "response_time_ms": response_time_ms,
701
+ }
702
+
703
+ except Exception as e:
704
+ metrics.inc_counter("http_requests_errors_total")
705
+ logger.error(f"Error setting test session usage: {e}", exc_info=True)
706
+ from fastapi import HTTPException
707
+
708
+ raise HTTPException(status_code=500, detail=str(e)) from e
709
+
416
710
  return router
@@ -0,0 +1,61 @@
1
+ """
2
+ MCP endpoint modules for the Gobby HTTP server.
3
+
4
+ This package contains decomposed endpoint handlers extracted from tools.py
5
+ using the Strangler Fig pattern. Each module handles a specific domain:
6
+
7
+ - discovery: Tool and server listing endpoints
8
+ - execution: Tool invocation endpoints
9
+ - server: Server management (add/remove/import)
10
+ - registry: Tool embedding, status, and refresh
11
+
12
+ External modules should import `create_mcp_router` from the parent package:
13
+ from gobby.servers.routes.mcp import create_mcp_router
14
+
15
+ For direct endpoint access (e.g., testing), import from submodules:
16
+ from gobby.servers.routes.mcp.endpoints.discovery import list_all_mcp_tools
17
+ """
18
+
19
+ from gobby.servers.routes.mcp.endpoints.discovery import (
20
+ list_all_mcp_tools,
21
+ recommend_mcp_tools,
22
+ search_mcp_tools,
23
+ )
24
+ from gobby.servers.routes.mcp.endpoints.execution import (
25
+ call_mcp_tool,
26
+ get_tool_schema,
27
+ list_mcp_tools,
28
+ mcp_proxy,
29
+ )
30
+ from gobby.servers.routes.mcp.endpoints.registry import (
31
+ embed_mcp_tools,
32
+ get_mcp_status,
33
+ refresh_mcp_tools,
34
+ )
35
+ from gobby.servers.routes.mcp.endpoints.server import (
36
+ add_mcp_server,
37
+ import_mcp_server,
38
+ list_mcp_servers,
39
+ remove_mcp_server,
40
+ )
41
+
42
+ __all__ = [
43
+ # Discovery
44
+ "list_all_mcp_tools",
45
+ "recommend_mcp_tools",
46
+ "search_mcp_tools",
47
+ # Execution
48
+ "call_mcp_tool",
49
+ "get_tool_schema",
50
+ "list_mcp_tools",
51
+ "mcp_proxy",
52
+ # Registry
53
+ "embed_mcp_tools",
54
+ "get_mcp_status",
55
+ "refresh_mcp_tools",
56
+ # Server
57
+ "add_mcp_server",
58
+ "import_mcp_server",
59
+ "list_mcp_servers",
60
+ "remove_mcp_server",
61
+ ]