gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,416 @@
1
+ """
2
+ Admin routes for Gobby HTTP server.
3
+
4
+ Provides status, metrics, config, and shutdown endpoints.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import os
10
+ import time
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ import psutil
14
+ from fastapi import APIRouter
15
+ from fastapi.responses import PlainTextResponse
16
+
17
+ from gobby.utils.metrics import Counter, get_metrics_collector
18
+ from gobby.utils.version import get_version
19
+
20
+ if TYPE_CHECKING:
21
+ from gobby.servers.http import HTTPServer
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def create_admin_router(server: "HTTPServer") -> APIRouter:
27
+ """
28
+ Create admin router with endpoints bound to server instance.
29
+
30
+ Args:
31
+ server: HTTPServer instance for accessing state and dependencies
32
+
33
+ Returns:
34
+ Configured APIRouter with admin endpoints
35
+ """
36
+ router = APIRouter(prefix="/admin", tags=["admin"])
37
+
38
+ @router.get("/status")
39
+ async def status_check() -> dict[str, Any]:
40
+ """
41
+ Comprehensive status check endpoint.
42
+
43
+ Returns detailed health status including daemon state, uptime,
44
+ memory usage, background tasks, and connection statistics.
45
+ """
46
+ start_time = time.perf_counter()
47
+
48
+ # Get server uptime
49
+ uptime_seconds = None
50
+ if server._start_time is not None:
51
+ uptime_seconds = time.time() - server._start_time
52
+
53
+ # Get daemon status if available
54
+ daemon_status = None
55
+ if server._daemon is not None:
56
+ try:
57
+ daemon_status = server._daemon.status()
58
+ except Exception as e:
59
+ logger.warning(f"Failed to get daemon status: {e}")
60
+
61
+ # Get process metrics
62
+ try:
63
+ process = psutil.Process(os.getpid())
64
+ memory_info = process.memory_info()
65
+ # Run cpu_percent in a thread executor to avoid blocking the event loop
66
+ # (interval=0.1 would block for 100ms otherwise)
67
+ cpu_percent = await asyncio.to_thread(process.cpu_percent, 0.1)
68
+
69
+ process_metrics = {
70
+ "memory_rss_mb": round(memory_info.rss / (1024 * 1024), 2),
71
+ "memory_vms_mb": round(memory_info.vms / (1024 * 1024), 2),
72
+ "cpu_percent": cpu_percent,
73
+ "num_threads": process.num_threads(),
74
+ }
75
+ except Exception as e:
76
+ logger.warning(f"Failed to get process metrics: {e}")
77
+ process_metrics = None
78
+
79
+ # Get background task status
80
+ metrics = get_metrics_collector()
81
+ background_tasks = {
82
+ "active": len(server._background_tasks),
83
+ "total": metrics._counters.get("background_tasks_total", Counter("", "")).value,
84
+ "completed": metrics._counters.get(
85
+ "background_tasks_completed_total", Counter("", "")
86
+ ).value,
87
+ "failed": metrics._counters.get("background_tasks_failed_total", Counter("", "")).value,
88
+ }
89
+
90
+ # Get MCP server status - include ALL configured servers
91
+ mcp_health = {}
92
+ if server.mcp_manager is not None:
93
+ try:
94
+ # Iterate over all configured servers, not just connected ones
95
+ for config in server.mcp_manager.server_configs:
96
+ health = server.mcp_manager.health.get(config.name)
97
+ is_connected = config.name in server.mcp_manager.connections
98
+ mcp_health[config.name] = {
99
+ "connected": is_connected,
100
+ "status": (
101
+ health.state.value
102
+ if health
103
+ else ("connected" if is_connected else "not_started")
104
+ ),
105
+ "enabled": config.enabled,
106
+ "transport": config.transport,
107
+ "health": health.health.value if health else None,
108
+ "consecutive_failures": health.consecutive_failures if health else 0,
109
+ "last_health_check": (
110
+ health.last_health_check.isoformat()
111
+ if health and health.last_health_check
112
+ else None
113
+ ),
114
+ "response_time_ms": health.response_time_ms if health else None,
115
+ }
116
+ except Exception as e:
117
+ logger.warning(f"Failed to get MCP health: {e}")
118
+
119
+ # Count internal tools from gobby-* registries and add them to mcp_health
120
+ internal_tools_count = 0
121
+ if server._internal_manager:
122
+ for registry in server._internal_manager.get_all_registries():
123
+ tools = registry.list_tools()
124
+ internal_tools_count += len(tools)
125
+ # Include internal servers in mcp_health for unified server count
126
+ mcp_health[registry.name] = {
127
+ "connected": True, # Internal servers are always available
128
+ "status": "connected",
129
+ "enabled": True,
130
+ "transport": "internal",
131
+ "health": "healthy",
132
+ "consecutive_failures": 0,
133
+ "last_health_check": None,
134
+ "response_time_ms": None,
135
+ "internal": True, # Flag to distinguish from downstream servers
136
+ "tool_count": len(tools),
137
+ }
138
+
139
+ # Get session statistics using efficient count queries
140
+ session_stats = {"active": 0, "paused": 0, "handoff_ready": 0, "total": 0}
141
+ if server.session_manager is not None:
142
+ try:
143
+ # Use count_by_status for efficient grouped counts
144
+ status_counts = server.session_manager.count_by_status()
145
+ session_stats["total"] = sum(status_counts.values())
146
+ session_stats["active"] = status_counts.get("active", 0)
147
+ session_stats["paused"] = status_counts.get("paused", 0)
148
+ session_stats["handoff_ready"] = status_counts.get("handoff_ready", 0)
149
+ except Exception as e:
150
+ logger.warning(f"Failed to get session stats: {e}")
151
+
152
+ # Get task statistics using efficient count queries
153
+ task_stats = {"open": 0, "in_progress": 0, "closed": 0, "ready": 0, "blocked": 0}
154
+ if server.task_manager is not None:
155
+ try:
156
+ # Use count_by_status for efficient grouped counts
157
+ status_counts = server.task_manager.count_by_status()
158
+ task_stats["open"] = status_counts.get("open", 0)
159
+ task_stats["in_progress"] = status_counts.get("in_progress", 0)
160
+ task_stats["closed"] = status_counts.get("closed", 0)
161
+ # Get ready and blocked counts using dedicated count methods
162
+ task_stats["ready"] = server.task_manager.count_ready_tasks()
163
+ task_stats["blocked"] = server.task_manager.count_blocked_tasks()
164
+ except Exception as e:
165
+ logger.warning(f"Failed to get task stats: {e}")
166
+
167
+ # Get memory statistics
168
+ memory_stats = {"count": 0, "avg_importance": 0.0}
169
+ if server.memory_manager is not None:
170
+ try:
171
+ stats = server.memory_manager.get_stats()
172
+ memory_stats["count"] = stats.get("total_count", 0)
173
+ memory_stats["avg_importance"] = stats.get("avg_importance", 0.0)
174
+ except Exception as e:
175
+ logger.warning(f"Failed to get memory stats: {e}")
176
+
177
+ # Get plugin status
178
+ plugin_stats: dict[str, Any] = {"enabled": False, "loaded": 0, "handlers": 0}
179
+ if hasattr(server, "_hook_manager") and server._hook_manager is not None:
180
+ try:
181
+ hook_manager = server._hook_manager
182
+ if hasattr(hook_manager, "plugin_loader") and hook_manager.plugin_loader:
183
+ plugin_loader = hook_manager.plugin_loader
184
+ plugin_stats["enabled"] = plugin_loader.config.enabled
185
+ plugins = plugin_loader.registry.list_plugins()
186
+ plugin_stats["loaded"] = len(plugins)
187
+ plugin_stats["handlers"] = sum(len(p.get("handlers", [])) for p in plugins)
188
+ plugin_stats["plugins"] = [
189
+ {
190
+ "name": p["name"],
191
+ "version": p["version"],
192
+ "handlers": len(p.get("handlers", [])),
193
+ "actions": len(p.get("actions", [])),
194
+ }
195
+ for p in plugins
196
+ ]
197
+ except Exception as e:
198
+ logger.warning(f"Failed to get plugin stats: {e}")
199
+
200
+ # Calculate response time
201
+ response_time_ms = (time.perf_counter() - start_time) * 1000
202
+
203
+ return {
204
+ "status": "healthy" if server._running else "degraded",
205
+ "server": {
206
+ "port": server.port,
207
+ "test_mode": server.test_mode,
208
+ "running": server._running,
209
+ "uptime_seconds": uptime_seconds,
210
+ },
211
+ "daemon": daemon_status,
212
+ "process": process_metrics,
213
+ "background_tasks": background_tasks,
214
+ "mcp_servers": mcp_health,
215
+ # Count of tools from internal gobby-* registries (tasks, memory)
216
+ "internal_tools_count": internal_tools_count,
217
+ "sessions": session_stats,
218
+ "tasks": task_stats,
219
+ "memory": memory_stats,
220
+ "plugins": plugin_stats,
221
+ "response_time_ms": response_time_ms,
222
+ }
223
+
224
+ @router.get("/metrics")
225
+ async def get_metrics() -> PlainTextResponse:
226
+ """
227
+ Prometheus-compatible metrics endpoint.
228
+
229
+ Returns metrics in Prometheus text exposition format including:
230
+ - HTTP request counts and durations
231
+ - Background task metrics
232
+ - Daemon health metrics
233
+ """
234
+ metrics = get_metrics_collector()
235
+ try:
236
+ # Update daemon health metrics if available
237
+ if server._daemon is not None:
238
+ try:
239
+ uptime = server._daemon.uptime
240
+ if uptime is not None:
241
+ metrics.set_gauge("daemon_uptime_seconds", uptime)
242
+
243
+ # Get process info for daemon
244
+ process = psutil.Process(os.getpid())
245
+ memory_info = process.memory_info()
246
+ metrics.set_gauge("daemon_memory_usage_bytes", float(memory_info.rss))
247
+
248
+ cpu_percent = process.cpu_percent(interval=0)
249
+ metrics.set_gauge("daemon_cpu_percent", cpu_percent)
250
+ except Exception as e:
251
+ logger.warning(f"Failed to update daemon metrics: {e}")
252
+
253
+ # Update background task gauge
254
+ metrics.set_gauge("background_tasks_active", float(len(server._background_tasks)))
255
+
256
+ # Export in Prometheus format
257
+ prometheus_output = metrics.export_prometheus()
258
+ return PlainTextResponse(
259
+ content=prometheus_output, media_type="text/plain; version=0.0.4"
260
+ )
261
+
262
+ except Exception as e:
263
+ logger.error(f"Failed to export metrics: {e}", exc_info=True)
264
+ raise
265
+
266
+ @router.get("/config")
267
+ async def get_config() -> dict[str, Any]:
268
+ """
269
+ Get daemon configuration and version information.
270
+
271
+ Returns:
272
+ Configuration data including ports, features, and versions
273
+ """
274
+ start_time = time.perf_counter()
275
+ metrics = get_metrics_collector()
276
+ metrics.inc_counter("http_requests_total")
277
+
278
+ try:
279
+ config_data = {
280
+ "server": {
281
+ "port": server.port,
282
+ "test_mode": server.test_mode,
283
+ "running": server._running,
284
+ "version": get_version(),
285
+ },
286
+ "features": {
287
+ "session_manager": server.session_manager is not None,
288
+ "mcp_manager": server.mcp_manager is not None,
289
+ },
290
+ "endpoints": {
291
+ "mcp": [
292
+ "/mcp/{server_name}/tools/{tool_name}",
293
+ ],
294
+ "sessions": [
295
+ "/sessions/register",
296
+ "/sessions/{id}",
297
+ ],
298
+ "admin": [
299
+ "/admin/status",
300
+ "/admin/metrics",
301
+ "/admin/config",
302
+ "/admin/shutdown",
303
+ ],
304
+ },
305
+ }
306
+
307
+ response_time_ms = (time.perf_counter() - start_time) * 1000
308
+
309
+ return {
310
+ "status": "success",
311
+ "config": config_data,
312
+ "response_time_ms": response_time_ms,
313
+ }
314
+
315
+ except Exception as e:
316
+ logger.error(f"Config retrieval error: {e}", exc_info=True)
317
+ from fastapi import HTTPException
318
+
319
+ raise HTTPException(status_code=500, detail=str(e)) from e
320
+
321
+ @router.post("/shutdown")
322
+ async def shutdown() -> dict[str, Any]:
323
+ """
324
+ Graceful daemon shutdown endpoint.
325
+
326
+ Returns:
327
+ Shutdown confirmation
328
+ """
329
+ start_time = time.perf_counter()
330
+ metrics = get_metrics_collector()
331
+
332
+ metrics.inc_counter("http_requests_total")
333
+ metrics.inc_counter("shutdown_requests_total")
334
+
335
+ try:
336
+ logger.debug("Shutdown requested via HTTP endpoint")
337
+
338
+ # Create background task for shutdown
339
+ task = asyncio.create_task(server._process_shutdown())
340
+
341
+ server._background_tasks.add(task)
342
+ task.add_done_callback(server._background_tasks.discard)
343
+
344
+ response_time_ms = (time.perf_counter() - start_time) * 1000
345
+
346
+ return {
347
+ "status": "shutting_down",
348
+ "message": "Graceful shutdown initiated",
349
+ "response_time_ms": response_time_ms,
350
+ }
351
+
352
+ except Exception as e:
353
+ metrics.inc_counter("http_requests_errors_total")
354
+ logger.error("Error initiating shutdown: %s", e, exc_info=True)
355
+ return {
356
+ "message": "Shutdown failed to initiate",
357
+ }
358
+
359
+ @router.post("/workflows/reload")
360
+ async def reload_workflows() -> dict[str, Any]:
361
+ """
362
+ Reload workflow definitions from disk.
363
+
364
+ Triggers the gobby-workflows.reload_cache MCP tool internally.
365
+ """
366
+ start_time = time.perf_counter()
367
+ metrics = get_metrics_collector()
368
+ metrics.inc_counter("http_requests_total")
369
+
370
+ try:
371
+ # Find the gobby-workflows registry
372
+ workflows_registry = None
373
+ if server._internal_manager:
374
+ for registry in server._internal_manager.get_all_registries():
375
+ if registry.name == "gobby-workflows":
376
+ workflows_registry = registry
377
+ break
378
+
379
+ if not workflows_registry:
380
+ return {
381
+ "status": "error",
382
+ "message": "Workflow registry not available",
383
+ }
384
+
385
+ # Call reload_cache tool directly via registry.call which handles async/sync
386
+ try:
387
+ result = await workflows_registry.call("reload_cache", {})
388
+ except ValueError:
389
+ return {
390
+ "status": "error",
391
+ "message": "reload_cache tool not found",
392
+ }
393
+ except Exception as e:
394
+ logger.error(f"Failed to execute reload_cache: {e}")
395
+ return {
396
+ "status": "error",
397
+ "message": f"Failed to reload cache: {e}",
398
+ }
399
+
400
+ response_time_ms = (time.perf_counter() - start_time) * 1000
401
+
402
+ return {
403
+ "status": "success",
404
+ "message": "Workflow cache reloaded",
405
+ "details": result,
406
+ "response_time_ms": response_time_ms,
407
+ }
408
+
409
+ except Exception as e:
410
+ metrics.inc_counter("http_requests_errors_total")
411
+ logger.error(f"Error reloading workflows: {e}", exc_info=True)
412
+ from fastapi import HTTPException
413
+
414
+ raise HTTPException(status_code=500, detail=str(e)) from e
415
+
416
+ return router
@@ -0,0 +1,118 @@
1
+ """FastAPI dependency injection functions for MCP routes.
2
+
3
+ These dependencies extract server components from app.state, enabling:
4
+ - Proper testability (dependencies can be mocked/overridden)
5
+ - Clear dependency graph
6
+ - Natural code splitting
7
+ - Standard FastAPI conventions
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, cast
13
+
14
+ from fastapi import HTTPException, Request
15
+
16
+ if TYPE_CHECKING:
17
+ from gobby.config.app import DaemonConfig
18
+ from gobby.llm import LLMService
19
+ from gobby.mcp_proxy.manager import MCPClientManager
20
+ from gobby.mcp_proxy.metrics import ToolMetricsManager
21
+ from gobby.mcp_proxy.registry_manager import InternalToolRegistryManager
22
+ from gobby.servers.http import HTTPServer
23
+ from gobby.storage.mcp_db import MCPDatabaseManager
24
+
25
+ __all__ = [
26
+ "get_server",
27
+ "get_mcp_manager",
28
+ "get_mcp_manager_required",
29
+ "get_internal_manager",
30
+ "get_tools_handler",
31
+ "get_config",
32
+ "get_mcp_db_manager",
33
+ "get_llm_service",
34
+ "get_metrics_manager",
35
+ "resolve_project_id",
36
+ ]
37
+
38
+
39
+ async def get_server(request: Request) -> HTTPServer:
40
+ """Get the HTTPServer instance from app state."""
41
+ server = getattr(request.app.state, "server", None)
42
+ if server is None:
43
+ raise HTTPException(status_code=503, detail="Server not initialized")
44
+ # Import here to avoid circular import, cast to help mypy
45
+ from gobby.servers.http import HTTPServer
46
+
47
+ return cast(HTTPServer, server)
48
+
49
+
50
+ async def get_mcp_manager(request: Request) -> MCPClientManager | None:
51
+ """Get the MCP client manager for external MCP servers."""
52
+ server = await get_server(request)
53
+ return server.mcp_manager
54
+
55
+
56
+ async def get_mcp_manager_required(request: Request) -> MCPClientManager:
57
+ """Get the MCP client manager, raising if unavailable."""
58
+ manager = await get_mcp_manager(request)
59
+ if manager is None:
60
+ raise HTTPException(status_code=503, detail="MCP manager not available")
61
+ return manager
62
+
63
+
64
+ async def get_internal_manager(request: Request) -> InternalToolRegistryManager | None:
65
+ """Get the internal tool registry manager (gobby-tasks, gobby-memory, etc.)."""
66
+ server = await get_server(request)
67
+ return server._internal_manager
68
+
69
+
70
+ async def get_tools_handler(request: Request) -> Any:
71
+ """Get the tools handler for Gobby daemon tools."""
72
+ server = await get_server(request)
73
+ return server._tools_handler
74
+
75
+
76
+ async def get_config(request: Request) -> DaemonConfig | None:
77
+ """Get the application configuration."""
78
+ server = await get_server(request)
79
+ return server.config
80
+
81
+
82
+ async def get_mcp_db_manager(request: Request) -> MCPDatabaseManager | None:
83
+ """Get the MCP database manager."""
84
+ server = await get_server(request)
85
+ return server._mcp_db_manager
86
+
87
+
88
+ async def get_llm_service(request: Request) -> LLMService | None:
89
+ """Get the LLM service for AI-powered operations."""
90
+ server = await get_server(request)
91
+ return server.llm_service
92
+
93
+
94
+ async def resolve_project_id(request: Request, project_id: str | None = None) -> str:
95
+ """
96
+ Resolve a project ID, defaulting to the current project if not specified.
97
+
98
+ Args:
99
+ request: FastAPI request object
100
+ project_id: Optional explicit project ID
101
+
102
+ Returns:
103
+ Resolved project ID
104
+
105
+ Raises:
106
+ HTTPException: If no project ID can be resolved
107
+ """
108
+ server = await get_server(request)
109
+ resolved = server._resolve_project_id(project_id, cwd=None)
110
+ if resolved is None:
111
+ raise HTTPException(status_code=400, detail="No project ID provided or detected")
112
+ return resolved
113
+
114
+
115
+ async def get_metrics_manager(request: Request) -> ToolMetricsManager | None:
116
+ """Get the tool metrics manager for tracking tool call statistics."""
117
+ server = await get_server(request)
118
+ return server.metrics_manager
@@ -0,0 +1,24 @@
1
+ """
2
+ MCP routes package.
3
+
4
+ Decomposed from monolithic mcp.py using Strangler Fig pattern.
5
+ Each router is now in its own focused module:
6
+ - tools.py: create_mcp_router (tool discovery, execution, search)
7
+ - hooks.py: create_hooks_router (CLI hook adapter)
8
+ - plugins.py: create_plugins_router (plugin management)
9
+ - webhooks.py: create_webhooks_router (webhook management)
10
+
11
+ This __init__.py re-exports all routers for backward compatibility.
12
+ """
13
+
14
+ from gobby.servers.routes.mcp.hooks import create_hooks_router
15
+ from gobby.servers.routes.mcp.plugins import create_plugins_router
16
+ from gobby.servers.routes.mcp.tools import create_mcp_router
17
+ from gobby.servers.routes.mcp.webhooks import create_webhooks_router
18
+
19
+ __all__ = [
20
+ "create_hooks_router",
21
+ "create_mcp_router",
22
+ "create_plugins_router",
23
+ "create_webhooks_router",
24
+ ]
@@ -0,0 +1,135 @@
1
+ """
2
+ Hooks management routes for Gobby HTTP server.
3
+
4
+ Provides hook execution endpoint for CLI adapters.
5
+ Extracted from base.py as part of Strangler Fig decomposition.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import time
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from fastapi import APIRouter, HTTPException, Request
14
+
15
+ from gobby.utils.metrics import get_metrics_collector
16
+
17
+ if TYPE_CHECKING:
18
+ from gobby.servers.http import HTTPServer
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def create_hooks_router(server: "HTTPServer") -> APIRouter:
24
+ """
25
+ Create hooks router with endpoints bound to server instance.
26
+
27
+ Args:
28
+ server: HTTPServer instance for accessing state and dependencies
29
+
30
+ Returns:
31
+ Configured APIRouter with hooks endpoints
32
+ """
33
+ router = APIRouter(prefix="/hooks", tags=["hooks"])
34
+ metrics = get_metrics_collector()
35
+
36
+ @router.post("/execute")
37
+ async def execute_hook(request: Request) -> dict[str, Any]:
38
+ """
39
+ Execute CLI hook via adapter pattern.
40
+
41
+ Request body:
42
+ {
43
+ "hook_type": "session-start",
44
+ "input_data": {...},
45
+ "source": "claude"
46
+ }
47
+
48
+ Returns:
49
+ Hook execution result with status
50
+ """
51
+ start_time = time.perf_counter()
52
+ metrics.inc_counter("http_requests_total")
53
+ metrics.inc_counter("hooks_total")
54
+
55
+ try:
56
+ # Parse request
57
+ payload = await request.json()
58
+ hook_type = payload.get("hook_type")
59
+ source = payload.get("source")
60
+
61
+ if not hook_type:
62
+ raise HTTPException(status_code=400, detail="hook_type required")
63
+
64
+ if not source:
65
+ raise HTTPException(status_code=400, detail="source required")
66
+
67
+ # Get HookManager from app.state
68
+ if not hasattr(request.app.state, "hook_manager"):
69
+ raise HTTPException(status_code=503, detail="HookManager not initialized")
70
+
71
+ hook_manager = request.app.state.hook_manager
72
+
73
+ # Select adapter based on source
74
+ from gobby.adapters.base import BaseAdapter
75
+ from gobby.adapters.claude_code import ClaudeCodeAdapter
76
+ from gobby.adapters.codex import CodexNotifyAdapter
77
+ from gobby.adapters.gemini import GeminiAdapter
78
+
79
+ if source == "claude":
80
+ adapter: BaseAdapter = ClaudeCodeAdapter(hook_manager=hook_manager)
81
+ elif source == "antigravity":
82
+ adapter = ClaudeCodeAdapter(hook_manager=hook_manager) # Same format as Claude
83
+ elif source == "gemini":
84
+ adapter = GeminiAdapter(hook_manager=hook_manager)
85
+ elif source == "codex":
86
+ adapter = CodexNotifyAdapter(hook_manager=hook_manager)
87
+ else:
88
+ raise HTTPException(
89
+ status_code=400,
90
+ detail=f"Unsupported source: {source}. Supported: claude, antigravity, gemini, codex",
91
+ )
92
+
93
+ # Execute hook via adapter
94
+ try:
95
+ result = await asyncio.to_thread(adapter.handle_native, payload, hook_manager)
96
+
97
+ response_time_ms = (time.perf_counter() - start_time) * 1000
98
+ metrics.inc_counter("hooks_succeeded_total")
99
+
100
+ logger.debug(
101
+ f"Hook executed: {hook_type}",
102
+ extra={
103
+ "hook_type": hook_type,
104
+ "continue": result.get("continue"),
105
+ "response_time_ms": response_time_ms,
106
+ },
107
+ )
108
+
109
+ return result
110
+
111
+ except ValueError as e:
112
+ metrics.inc_counter("hooks_failed_total")
113
+ logger.warning(
114
+ f"Invalid hook request: {hook_type}",
115
+ extra={"hook_type": hook_type, "error": str(e)},
116
+ )
117
+ raise HTTPException(status_code=400, detail=str(e)) from e
118
+
119
+ except Exception as e:
120
+ metrics.inc_counter("hooks_failed_total")
121
+ logger.error(
122
+ f"Hook execution failed: {hook_type}",
123
+ exc_info=True,
124
+ extra={"hook_type": hook_type},
125
+ )
126
+ raise HTTPException(status_code=500, detail=str(e)) from e
127
+
128
+ except HTTPException:
129
+ raise
130
+ except Exception as e:
131
+ metrics.inc_counter("hooks_failed_total")
132
+ logger.error("Hook endpoint error", exc_info=True)
133
+ raise HTTPException(status_code=500, detail=str(e)) from e
134
+
135
+ return router