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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- 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
|