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
gobby/llm/service.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Service for multi-provider support.
|
|
3
|
+
|
|
4
|
+
Provides a unified interface for accessing multiple LLM providers (Claude, Codex,
|
|
5
|
+
Gemini, LiteLLM) based on the multi-provider config structure with feature-specific
|
|
6
|
+
provider routing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from gobby.config.app import (
|
|
14
|
+
DaemonConfig,
|
|
15
|
+
)
|
|
16
|
+
from gobby.llm.base import LLMProvider
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Type alias for feature configs that have provider/model/prompt fields
|
|
22
|
+
FeatureConfig = "SessionSummaryConfig | TitleSynthesisConfig | RecommendToolsConfig"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LLMService:
|
|
26
|
+
"""
|
|
27
|
+
Service for managing multiple LLM providers.
|
|
28
|
+
|
|
29
|
+
Provides unified access to configured LLM providers and routes requests
|
|
30
|
+
to the appropriate provider based on feature configuration.
|
|
31
|
+
|
|
32
|
+
Example usage:
|
|
33
|
+
# Initialize with config
|
|
34
|
+
service = LLMService(config)
|
|
35
|
+
|
|
36
|
+
# Get provider by name
|
|
37
|
+
claude = service.get_provider("claude")
|
|
38
|
+
|
|
39
|
+
# Get provider for a feature (uses feature's provider/model config)
|
|
40
|
+
provider, model, prompt = service.get_provider_for_feature(config.session_summary)
|
|
41
|
+
|
|
42
|
+
# Use provider
|
|
43
|
+
result = await provider.generate_summary(context, prompt_template=prompt)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: "DaemonConfig"):
|
|
47
|
+
"""
|
|
48
|
+
Initialize LLM service with configuration.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config: Client configuration containing llm_providers settings.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If llm_providers is not configured.
|
|
55
|
+
"""
|
|
56
|
+
self._config = config
|
|
57
|
+
self._providers: dict[str, LLMProvider] = {}
|
|
58
|
+
self._initialized_providers: set[str] = set()
|
|
59
|
+
|
|
60
|
+
if not config.llm_providers:
|
|
61
|
+
raise ValueError("llm_providers config is required for LLMService")
|
|
62
|
+
|
|
63
|
+
# Log enabled providers
|
|
64
|
+
enabled = config.llm_providers.get_enabled_providers()
|
|
65
|
+
logger.debug(f"LLMService initialized with providers: {enabled}")
|
|
66
|
+
|
|
67
|
+
def _get_provider_instance(self, name: str) -> "LLMProvider":
|
|
68
|
+
"""
|
|
69
|
+
Get or create a provider instance by name (lazy initialization).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Provider name (claude, codex, gemini, litellm)
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
LLMProvider instance
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If provider is not configured or not supported
|
|
79
|
+
"""
|
|
80
|
+
if name in self._providers:
|
|
81
|
+
return self._providers[name]
|
|
82
|
+
|
|
83
|
+
# Check if provider is configured
|
|
84
|
+
if not self._config.llm_providers:
|
|
85
|
+
raise ValueError("llm_providers not configured")
|
|
86
|
+
|
|
87
|
+
provider_config = getattr(self._config.llm_providers, name, None)
|
|
88
|
+
if not provider_config:
|
|
89
|
+
enabled = self._config.llm_providers.get_enabled_providers()
|
|
90
|
+
raise ValueError(f"Provider '{name}' is not configured. Available providers: {enabled}")
|
|
91
|
+
|
|
92
|
+
# Create provider instance based on name
|
|
93
|
+
provider: LLMProvider
|
|
94
|
+
|
|
95
|
+
if name == "claude":
|
|
96
|
+
from gobby.llm.claude import ClaudeLLMProvider
|
|
97
|
+
|
|
98
|
+
provider = ClaudeLLMProvider(self._config)
|
|
99
|
+
logger.debug("Initialized Claude provider")
|
|
100
|
+
|
|
101
|
+
elif name == "codex":
|
|
102
|
+
from gobby.llm.codex import CodexProvider
|
|
103
|
+
|
|
104
|
+
provider = CodexProvider(self._config)
|
|
105
|
+
logger.debug(f"Initialized Codex provider (auth_mode: {provider_config.auth_mode})")
|
|
106
|
+
|
|
107
|
+
elif name == "gemini":
|
|
108
|
+
from gobby.llm.gemini import GeminiProvider
|
|
109
|
+
|
|
110
|
+
provider = GeminiProvider(self._config)
|
|
111
|
+
logger.debug(f"Initialized Gemini provider (auth_mode: {provider_config.auth_mode})")
|
|
112
|
+
|
|
113
|
+
elif name == "litellm":
|
|
114
|
+
from gobby.llm.litellm import LiteLLMProvider
|
|
115
|
+
|
|
116
|
+
provider = LiteLLMProvider(self._config)
|
|
117
|
+
logger.debug("Initialized LiteLLM provider")
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Unknown provider '{name}'. Supported providers: claude, codex, gemini, litellm"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self._providers[name] = provider
|
|
125
|
+
self._initialized_providers.add(name)
|
|
126
|
+
return provider
|
|
127
|
+
|
|
128
|
+
def get_provider(self, name: str) -> "LLMProvider":
|
|
129
|
+
"""
|
|
130
|
+
Get a provider by name.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
name: Provider name (claude, codex, gemini, litellm)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
LLMProvider instance
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If provider is not configured or not supported
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
claude = service.get_provider("claude")
|
|
143
|
+
result = await claude.generate_summary(context)
|
|
144
|
+
"""
|
|
145
|
+
return self._get_provider_instance(name)
|
|
146
|
+
|
|
147
|
+
def get_provider_for_feature(
|
|
148
|
+
self, feature_config: Any
|
|
149
|
+
) -> tuple["LLMProvider", str, str | None]:
|
|
150
|
+
"""
|
|
151
|
+
Get provider, model, and prompt for a feature configuration.
|
|
152
|
+
|
|
153
|
+
Feature configs (SessionSummaryConfig, TitleSynthesisConfig, etc.) specify
|
|
154
|
+
which provider and model to use for that feature. This method returns
|
|
155
|
+
the appropriate provider instance along with the configured model and prompt.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
feature_config: Feature configuration object with provider, model, and
|
|
159
|
+
optionally prompt fields.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Tuple of (provider, model, prompt) where:
|
|
163
|
+
- provider: LLMProvider instance
|
|
164
|
+
- model: Model name string
|
|
165
|
+
- prompt: Optional prompt template string (or None if not configured)
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ValueError: If feature config is missing required fields
|
|
169
|
+
ValueError: If specified provider is not configured
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
provider, model, prompt = service.get_provider_for_feature(config.session_summary)
|
|
173
|
+
result = await provider.generate_summary(context, prompt_template=prompt)
|
|
174
|
+
"""
|
|
175
|
+
# Extract provider name from feature config
|
|
176
|
+
provider_name = getattr(feature_config, "provider", None)
|
|
177
|
+
if not provider_name:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Feature config {type(feature_config).__name__} missing 'provider' field"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Extract model
|
|
183
|
+
model = getattr(feature_config, "model", None)
|
|
184
|
+
if not model:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Feature config {type(feature_config).__name__} missing 'model' field"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Extract prompt (optional)
|
|
190
|
+
prompt = getattr(feature_config, "prompt", None)
|
|
191
|
+
|
|
192
|
+
# Get provider instance
|
|
193
|
+
provider = self._get_provider_instance(provider_name)
|
|
194
|
+
|
|
195
|
+
return provider, model, prompt
|
|
196
|
+
|
|
197
|
+
def get_default_provider(self) -> "LLMProvider":
|
|
198
|
+
"""
|
|
199
|
+
Get the default provider (first enabled provider, preferring Claude).
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
LLMProvider instance
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If no providers are configured
|
|
206
|
+
"""
|
|
207
|
+
if not self._config.llm_providers:
|
|
208
|
+
raise ValueError("llm_providers not configured")
|
|
209
|
+
|
|
210
|
+
enabled = self._config.llm_providers.get_enabled_providers()
|
|
211
|
+
if not enabled:
|
|
212
|
+
raise ValueError("No providers configured in llm_providers")
|
|
213
|
+
|
|
214
|
+
# Prefer Claude if available
|
|
215
|
+
if "claude" in enabled:
|
|
216
|
+
return self._get_provider_instance("claude")
|
|
217
|
+
|
|
218
|
+
# Otherwise use first available
|
|
219
|
+
return self._get_provider_instance(enabled[0])
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def enabled_providers(self) -> list[str]:
|
|
223
|
+
"""Get list of enabled provider names."""
|
|
224
|
+
if not self._config.llm_providers:
|
|
225
|
+
return []
|
|
226
|
+
return self._config.llm_providers.get_enabled_providers()
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def initialized_providers(self) -> list[str]:
|
|
230
|
+
"""Get list of providers that have been initialized (lazily loaded)."""
|
|
231
|
+
return list(self._initialized_providers)
|
|
232
|
+
|
|
233
|
+
def __repr__(self) -> str:
|
|
234
|
+
enabled = self.enabled_providers
|
|
235
|
+
initialized = self.initialized_providers
|
|
236
|
+
return f"LLMService(enabled={enabled}, initialized={initialized})"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP (Model Context Protocol) package for gobby daemon.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- MCPClientManager: Multi-server connection management
|
|
6
|
+
- MCPServerConfig: Server configuration dataclass
|
|
7
|
+
- MCP actions: add/remove/list servers
|
|
8
|
+
- create_mcp_server: FastMCP server factory
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .manager import (
|
|
12
|
+
ConnectionState,
|
|
13
|
+
HealthState,
|
|
14
|
+
MCPClientManager,
|
|
15
|
+
MCPConnectionHealth,
|
|
16
|
+
MCPError,
|
|
17
|
+
MCPServerConfig,
|
|
18
|
+
)
|
|
19
|
+
from .server import create_mcp_server
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ConnectionState",
|
|
23
|
+
"HealthState",
|
|
24
|
+
"MCPClientManager",
|
|
25
|
+
"MCPConnectionHealth",
|
|
26
|
+
"MCPError",
|
|
27
|
+
"MCPServerConfig",
|
|
28
|
+
"create_mcp_server",
|
|
29
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP actions for local-first daemon.
|
|
3
|
+
|
|
4
|
+
Provides simplified MCP server management without platform sync.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from gobby.mcp_proxy.manager import MCPClientManager, MCPServerConfig
|
|
11
|
+
from gobby.tools.summarizer import generate_server_description
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def add_mcp_server(
|
|
17
|
+
mcp_manager: MCPClientManager,
|
|
18
|
+
name: str,
|
|
19
|
+
transport: str,
|
|
20
|
+
project_id: str,
|
|
21
|
+
url: str | None = None,
|
|
22
|
+
headers: dict[str, str] | None = None,
|
|
23
|
+
command: str | None = None,
|
|
24
|
+
args: list[str] | None = None,
|
|
25
|
+
env: dict[str, str] | None = None,
|
|
26
|
+
enabled: bool = True,
|
|
27
|
+
description: str | None = None,
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
Dynamically add a new MCP server connection.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
mcp_manager: MCP client manager instance
|
|
34
|
+
name: Unique server name
|
|
35
|
+
transport: Transport type (http, stdio, websocket)
|
|
36
|
+
project_id: Required project ID - all servers must belong to a project
|
|
37
|
+
url: Server URL (for http/websocket)
|
|
38
|
+
headers: Custom HTTP headers
|
|
39
|
+
command: Command to run (for stdio)
|
|
40
|
+
args: Command arguments (for stdio)
|
|
41
|
+
env: Environment variables (for stdio)
|
|
42
|
+
enabled: Whether server is enabled
|
|
43
|
+
description: Optional server description
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Result dict with success status and server info
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
# Normalize server name to lowercase
|
|
50
|
+
name = name.lower()
|
|
51
|
+
|
|
52
|
+
# Create configuration
|
|
53
|
+
config = MCPServerConfig(
|
|
54
|
+
name=name,
|
|
55
|
+
transport=transport,
|
|
56
|
+
url=url,
|
|
57
|
+
headers=headers,
|
|
58
|
+
command=command,
|
|
59
|
+
args=args,
|
|
60
|
+
env=env,
|
|
61
|
+
enabled=enabled,
|
|
62
|
+
description=description,
|
|
63
|
+
project_id=project_id,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Add server via manager (connects and caches tools)
|
|
67
|
+
result = await mcp_manager.add_server(config)
|
|
68
|
+
|
|
69
|
+
if not result.get("success"):
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
# Get full tool schemas from add_server result
|
|
73
|
+
full_tool_schemas = result.get("full_tool_schemas", [])
|
|
74
|
+
|
|
75
|
+
# Generate server description using AI if not provided
|
|
76
|
+
if not description and full_tool_schemas:
|
|
77
|
+
try:
|
|
78
|
+
server_description = await generate_server_description(
|
|
79
|
+
server_name=name, tool_summaries=full_tool_schemas
|
|
80
|
+
)
|
|
81
|
+
config.description = server_description
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.warning(f"Failed to generate server description: {e}")
|
|
84
|
+
|
|
85
|
+
logger.debug(f"Added MCP server: {name} ({transport})")
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Failed to add MCP server '{name}': {e}")
|
|
90
|
+
return {
|
|
91
|
+
"success": False,
|
|
92
|
+
"name": name,
|
|
93
|
+
"error": str(e),
|
|
94
|
+
"message": f"Failed to add server: {e}",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def remove_mcp_server(
|
|
99
|
+
mcp_manager: MCPClientManager,
|
|
100
|
+
name: str,
|
|
101
|
+
project_id: str,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""
|
|
104
|
+
Remove an MCP server.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
mcp_manager: MCP client manager instance
|
|
108
|
+
name: Server name to remove
|
|
109
|
+
project_id: Required project ID
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Result dict with success status
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
result = await mcp_manager.remove_server(name, project_id=project_id)
|
|
116
|
+
if result.get("success"):
|
|
117
|
+
logger.debug(f"Removed MCP server: {name} (project {project_id})")
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Failed to remove MCP server '{name}': {e}")
|
|
122
|
+
return {
|
|
123
|
+
"success": False,
|
|
124
|
+
"name": name,
|
|
125
|
+
"error": str(e),
|
|
126
|
+
"message": f"Failed to remove server: {e}",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def list_mcp_servers(
|
|
131
|
+
mcp_manager: MCPClientManager,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
List all configured MCP servers.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
mcp_manager: MCP client manager instance
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Dict with servers list and status. Each server includes:
|
|
141
|
+
- project_id: None for global servers, UUID string for project-scoped
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
servers = []
|
|
145
|
+
for config in mcp_manager.server_configs:
|
|
146
|
+
health = mcp_manager.health.get(config.name)
|
|
147
|
+
servers.append(
|
|
148
|
+
{
|
|
149
|
+
"name": config.name,
|
|
150
|
+
"project_id": config.project_id,
|
|
151
|
+
"transport": config.transport,
|
|
152
|
+
"enabled": config.enabled,
|
|
153
|
+
"url": config.url,
|
|
154
|
+
"command": config.command,
|
|
155
|
+
"description": config.description,
|
|
156
|
+
"connected": config.name in mcp_manager.connections,
|
|
157
|
+
"state": health.state.value if health else "unknown",
|
|
158
|
+
"tools": config.tools or [],
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"success": True,
|
|
164
|
+
"servers": servers,
|
|
165
|
+
"total_count": len(servers),
|
|
166
|
+
"connected_count": len(mcp_manager.connections),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to list MCP servers: {e}")
|
|
171
|
+
return {
|
|
172
|
+
"success": False,
|
|
173
|
+
"error": str(e),
|
|
174
|
+
"servers": [],
|
|
175
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Daemon process control."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import psutil
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("gobby.daemon.control")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def check_daemon_http_health(port: int, timeout: float = 2.0) -> bool:
|
|
17
|
+
"""Check if daemon is healthy via HTTP."""
|
|
18
|
+
try:
|
|
19
|
+
async with httpx.AsyncClient() as client:
|
|
20
|
+
resp = await client.get(f"http://localhost:{port}/admin/status", timeout=timeout)
|
|
21
|
+
return resp.status_code == 200
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_daemon_pid() -> int | None:
|
|
27
|
+
"""Get PID of running daemon process."""
|
|
28
|
+
current_pid = os.getpid()
|
|
29
|
+
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
|
|
30
|
+
try:
|
|
31
|
+
if proc.info["pid"] == current_pid:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
cmdline = proc.info["cmdline"]
|
|
35
|
+
if not cmdline:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
cmdline_str = " ".join(cmdline)
|
|
39
|
+
# Match either gobby.runner or gobby.cli daemon start
|
|
40
|
+
if "gobby.runner" in cmdline_str or (
|
|
41
|
+
"gobby.cli" in cmdline_str and "daemon" in cmdline_str
|
|
42
|
+
):
|
|
43
|
+
from typing import cast
|
|
44
|
+
|
|
45
|
+
return cast(int, proc.info["pid"])
|
|
46
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
47
|
+
pass
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_daemon_running() -> bool:
|
|
52
|
+
"""Check if daemon is running."""
|
|
53
|
+
return get_daemon_pid() is not None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def start_daemon_process(port: int, websocket_port: int) -> dict[str, Any]:
|
|
57
|
+
"""Start daemon in a new process."""
|
|
58
|
+
if is_daemon_running():
|
|
59
|
+
pid = get_daemon_pid()
|
|
60
|
+
return {
|
|
61
|
+
"success": False,
|
|
62
|
+
"already_running": True,
|
|
63
|
+
"pid": pid,
|
|
64
|
+
"message": f"Daemon is already running with PID {pid}",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
cmd = [
|
|
68
|
+
sys.executable,
|
|
69
|
+
"-m",
|
|
70
|
+
"gobby.cli.app",
|
|
71
|
+
"daemon",
|
|
72
|
+
"start",
|
|
73
|
+
"--port",
|
|
74
|
+
str(port),
|
|
75
|
+
"--websocket-port",
|
|
76
|
+
str(websocket_port),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Use asyncio.create_subprocess_exec to avoid blocking the event loop
|
|
81
|
+
proc = await asyncio.create_subprocess_exec(
|
|
82
|
+
*cmd,
|
|
83
|
+
stdout=asyncio.subprocess.PIPE,
|
|
84
|
+
stderr=asyncio.subprocess.PIPE,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Do NOT await communicate() - this blocks until exit.
|
|
88
|
+
# Instead, wait a brief moment to check for immediate crash.
|
|
89
|
+
await asyncio.sleep(0.5)
|
|
90
|
+
|
|
91
|
+
if proc.returncode is not None:
|
|
92
|
+
# Process exited immediately - capture output
|
|
93
|
+
stdout, stderr = await proc.communicate()
|
|
94
|
+
return {
|
|
95
|
+
"success": False,
|
|
96
|
+
"message": "Start failed - process exited immediately",
|
|
97
|
+
"error": stderr.decode().strip() if stderr else "Unknown error",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Process is running - check health
|
|
101
|
+
if await check_daemon_http_health(port, timeout=5.0):
|
|
102
|
+
return {
|
|
103
|
+
"success": True,
|
|
104
|
+
"pid": proc.pid,
|
|
105
|
+
"output": "Daemon started successfully",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# If health check fails but process is still running, check pid directly
|
|
109
|
+
# This might happen if listening takes longer than health check
|
|
110
|
+
pid = get_daemon_pid()
|
|
111
|
+
if pid:
|
|
112
|
+
return {
|
|
113
|
+
"success": True,
|
|
114
|
+
"pid": pid,
|
|
115
|
+
"output": "Daemon started (health check pending)",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"success": False,
|
|
120
|
+
"message": "Start failed - process running but unhealthy",
|
|
121
|
+
"error": "Health check timed out",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return {"success": False, "error": str(e), "message": f"Failed to start: {e}"}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def stop_daemon_process(pid: int | None = None) -> dict[str, Any]:
|
|
129
|
+
"""Stop running daemon."""
|
|
130
|
+
if pid is None:
|
|
131
|
+
pid = get_daemon_pid()
|
|
132
|
+
|
|
133
|
+
if not pid:
|
|
134
|
+
return {"success": False, "not_running": True, "message": "Daemon not running"}
|
|
135
|
+
|
|
136
|
+
timeout = 5.0
|
|
137
|
+
deadline = asyncio.get_running_loop().time() + timeout
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
os.kill(pid, signal.SIGTERM)
|
|
141
|
+
|
|
142
|
+
# Poll for termination
|
|
143
|
+
while True:
|
|
144
|
+
try:
|
|
145
|
+
os.kill(pid, 0)
|
|
146
|
+
if asyncio.get_running_loop().time() > deadline:
|
|
147
|
+
return {
|
|
148
|
+
"success": False,
|
|
149
|
+
"error": "Process did not exit after SIGTERM",
|
|
150
|
+
"message": "Stop timed out",
|
|
151
|
+
}
|
|
152
|
+
await asyncio.sleep(0.1)
|
|
153
|
+
except ProcessLookupError:
|
|
154
|
+
# Process is gone
|
|
155
|
+
return {"success": True, "output": "Daemon stopped"}
|
|
156
|
+
|
|
157
|
+
except ProcessLookupError:
|
|
158
|
+
return {"success": False, "error": "Process not found", "not_running": True}
|
|
159
|
+
except PermissionError:
|
|
160
|
+
return {"success": False, "error": "Permission denied"}
|
|
161
|
+
except Exception as e:
|
|
162
|
+
return {"success": False, "error": str(e)}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def restart_daemon_process(
|
|
166
|
+
current_pid: int | None, port: int, websocket_port: int
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Restart daemon."""
|
|
169
|
+
stop_result = await stop_daemon_process(current_pid)
|
|
170
|
+
if not stop_result.get("success") and not stop_result.get("not_running"):
|
|
171
|
+
return stop_result
|
|
172
|
+
|
|
173
|
+
# Wait for ports to be free with actual port checking
|
|
174
|
+
import socket
|
|
175
|
+
|
|
176
|
+
def is_port_free(p: int) -> bool:
|
|
177
|
+
"""Check if a port is available by attempting to bind to it."""
|
|
178
|
+
try:
|
|
179
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
180
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
181
|
+
s.bind(("127.0.0.1", p))
|
|
182
|
+
return True
|
|
183
|
+
except OSError:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
for _ in range(10):
|
|
187
|
+
if await asyncio.to_thread(is_port_free, port) and await asyncio.to_thread(
|
|
188
|
+
is_port_free, websocket_port
|
|
189
|
+
):
|
|
190
|
+
break
|
|
191
|
+
await asyncio.sleep(0.5)
|
|
192
|
+
else:
|
|
193
|
+
return {
|
|
194
|
+
"success": False,
|
|
195
|
+
"error": f"Ports {port} and/or {websocket_port} not free after 10 retries",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return await start_daemon_process(port, websocket_port)
|