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,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM providers configuration module.
|
|
3
|
+
|
|
4
|
+
Contains LLM-related Pydantic config models:
|
|
5
|
+
- LLMProviderConfig: Single provider config (models, auth_mode)
|
|
6
|
+
- LLMProvidersConfig: Multi-provider config (claude, codex, gemini, litellm)
|
|
7
|
+
|
|
8
|
+
Extracted from app.py using Strangler Fig pattern for code decomposition.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
__all__ = ["LLMProviderConfig", "LLMProvidersConfig"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LLMProviderConfig(BaseModel):
|
|
19
|
+
"""Configuration for a single LLM provider."""
|
|
20
|
+
|
|
21
|
+
models: str = Field(
|
|
22
|
+
description="Comma-separated list of available models for this provider",
|
|
23
|
+
)
|
|
24
|
+
auth_mode: Literal["subscription", "api_key", "adc"] = Field(
|
|
25
|
+
default="subscription",
|
|
26
|
+
description="Authentication mode: 'subscription' (CLI-based), 'api_key' (BYOK), 'adc' (Google ADC)",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get_models_list(self) -> list[str]:
|
|
30
|
+
"""Return models as a list."""
|
|
31
|
+
return [m.strip() for m in self.models.split(",") if m.strip()]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LLMProvidersConfig(BaseModel):
|
|
35
|
+
"""
|
|
36
|
+
Configuration for multiple LLM providers.
|
|
37
|
+
|
|
38
|
+
Example YAML:
|
|
39
|
+
```yaml
|
|
40
|
+
llm_providers:
|
|
41
|
+
json_strict: true # Strict JSON validation for LLM responses (default)
|
|
42
|
+
claude:
|
|
43
|
+
models: claude-haiku-4-5,claude-sonnet-4-5,claude-opus-4-5
|
|
44
|
+
codex:
|
|
45
|
+
models: gpt-4o-mini,gpt-5-mini,gpt-5
|
|
46
|
+
auth_mode: subscription
|
|
47
|
+
gemini:
|
|
48
|
+
models: gemini-2.0-flash,gemini-2.5-pro
|
|
49
|
+
auth_mode: adc
|
|
50
|
+
litellm:
|
|
51
|
+
models: gpt-4o-mini,mistral-large
|
|
52
|
+
auth_mode: api_key
|
|
53
|
+
api_keys:
|
|
54
|
+
OPENAI_API_KEY: sk-...
|
|
55
|
+
MISTRAL_API_KEY: ...
|
|
56
|
+
```
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
json_strict: bool = Field(
|
|
60
|
+
default=True,
|
|
61
|
+
description="Strict JSON validation for LLM responses. "
|
|
62
|
+
"When True (default), type mismatches raise errors. "
|
|
63
|
+
"When False, allows coercion (e.g., '5' -> 5). "
|
|
64
|
+
"Can be overridden per-workflow via llm_json_strict variable.",
|
|
65
|
+
)
|
|
66
|
+
claude: LLMProviderConfig | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Claude provider configuration",
|
|
69
|
+
)
|
|
70
|
+
codex: LLMProviderConfig | None = Field(
|
|
71
|
+
default=None,
|
|
72
|
+
description="Codex (OpenAI) provider configuration",
|
|
73
|
+
)
|
|
74
|
+
gemini: LLMProviderConfig | None = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
description="Gemini provider configuration",
|
|
77
|
+
)
|
|
78
|
+
litellm: LLMProviderConfig | None = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
description="LiteLLM provider configuration",
|
|
81
|
+
)
|
|
82
|
+
api_keys: dict[str, str] = Field(
|
|
83
|
+
default_factory=dict,
|
|
84
|
+
description="API keys for BYOK providers (key name -> key value)",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def get_enabled_providers(self) -> list[str]:
|
|
88
|
+
"""Return list of enabled provider names."""
|
|
89
|
+
providers = []
|
|
90
|
+
if self.claude:
|
|
91
|
+
providers.append("claude")
|
|
92
|
+
if self.codex:
|
|
93
|
+
providers.append("codex")
|
|
94
|
+
if self.gemini:
|
|
95
|
+
providers.append("gemini")
|
|
96
|
+
if self.litellm:
|
|
97
|
+
providers.append("litellm")
|
|
98
|
+
return providers
|
gobby/config/logging.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging configuration module.
|
|
3
|
+
|
|
4
|
+
Contains logging-related Pydantic config models:
|
|
5
|
+
- LoggingSettings: Log levels, formats, file paths, rotation settings
|
|
6
|
+
|
|
7
|
+
Extracted from app.py using Strangler Fig pattern for code decomposition.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
13
|
+
|
|
14
|
+
__all__ = ["LoggingSettings"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LoggingSettings(BaseModel):
|
|
18
|
+
"""Logging configuration."""
|
|
19
|
+
|
|
20
|
+
level: Literal["debug", "info", "warning", "error"] = Field(
|
|
21
|
+
default="info",
|
|
22
|
+
description="Log level",
|
|
23
|
+
)
|
|
24
|
+
format: Literal["text", "json"] = Field(
|
|
25
|
+
default="text",
|
|
26
|
+
description="Log format (text or json)",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Log file paths
|
|
30
|
+
client: str = Field(
|
|
31
|
+
default="~/.gobby/logs/gobby.log",
|
|
32
|
+
description="Daemon main log file path",
|
|
33
|
+
)
|
|
34
|
+
client_error: str = Field(
|
|
35
|
+
default="~/.gobby/logs/gobby-error.log",
|
|
36
|
+
description="Daemon error log file path",
|
|
37
|
+
)
|
|
38
|
+
hook_manager: str = Field(
|
|
39
|
+
default="~/.gobby/logs/hook-manager.log",
|
|
40
|
+
description="Claude Code hook manager log file path",
|
|
41
|
+
)
|
|
42
|
+
mcp_server: str = Field(
|
|
43
|
+
default="~/.gobby/logs/mcp-server.log",
|
|
44
|
+
description="MCP server log file path",
|
|
45
|
+
)
|
|
46
|
+
mcp_client: str = Field(
|
|
47
|
+
default="~/.gobby/logs/mcp-client.log",
|
|
48
|
+
description="MCP client connection log file path",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
max_size_mb: int = Field(
|
|
52
|
+
default=10,
|
|
53
|
+
description="Maximum log file size in MB",
|
|
54
|
+
)
|
|
55
|
+
backup_count: int = Field(
|
|
56
|
+
default=5,
|
|
57
|
+
description="Number of backup log files to keep",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@field_validator("max_size_mb", "backup_count")
|
|
61
|
+
@classmethod
|
|
62
|
+
def validate_positive(cls, v: int) -> int:
|
|
63
|
+
"""Validate value is positive."""
|
|
64
|
+
if v <= 0:
|
|
65
|
+
raise ValueError("Value must be positive")
|
|
66
|
+
return v
|
gobby/config/mcp.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Configuration Manager for persistent server configuration.
|
|
3
|
+
|
|
4
|
+
Manages MCP server configurations stored in ~/.gobby/.mcp.json,
|
|
5
|
+
providing thread-safe read/write operations with validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from gobby.mcp_proxy.manager import MCPServerConfig
|
|
14
|
+
|
|
15
|
+
__all__ = ["MCPConfigManager", "MCPServerConfig"]
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MCPConfigManager:
|
|
21
|
+
"""
|
|
22
|
+
Manages persistent MCP server configurations in ~/.gobby/.mcp.json.
|
|
23
|
+
|
|
24
|
+
Provides thread-safe operations for reading, writing, adding, and removing
|
|
25
|
+
MCP server configurations with automatic validation and file locking.
|
|
26
|
+
|
|
27
|
+
Configuration file format:
|
|
28
|
+
{
|
|
29
|
+
"servers": [
|
|
30
|
+
{
|
|
31
|
+
"name": "context7",
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"transport": "stdio",
|
|
34
|
+
"command": "uvx",
|
|
35
|
+
"args": ["context7-mcp"],
|
|
36
|
+
"env": null
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "supabase",
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"transport": "stdio",
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "@supabase/mcp-server@latest"],
|
|
44
|
+
"env": null
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, config_path: str | None = None):
|
|
51
|
+
"""
|
|
52
|
+
Initialize MCP configuration manager.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config_path: Path to MCP config file (default: ~/.gobby/.mcp.json)
|
|
56
|
+
"""
|
|
57
|
+
if config_path is None:
|
|
58
|
+
config_path = "~/.gobby/.mcp.json"
|
|
59
|
+
|
|
60
|
+
self.config_path = Path(config_path).expanduser()
|
|
61
|
+
|
|
62
|
+
# Ensure parent directory exists
|
|
63
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
# Initialize config file if it doesn't exist
|
|
66
|
+
if not self.config_path.exists():
|
|
67
|
+
self._write_config({"servers": []})
|
|
68
|
+
|
|
69
|
+
def _read_config(self) -> dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Read MCP configuration from file.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Configuration dictionary
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If config file is invalid JSON
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
with open(self.config_path) as f:
|
|
81
|
+
content = f.read()
|
|
82
|
+
|
|
83
|
+
if not content.strip():
|
|
84
|
+
return {"servers": []}
|
|
85
|
+
|
|
86
|
+
config = json.loads(content)
|
|
87
|
+
|
|
88
|
+
# Validate structure
|
|
89
|
+
if not isinstance(config, dict):
|
|
90
|
+
raise ValueError("Config must be a JSON object")
|
|
91
|
+
|
|
92
|
+
if "servers" not in config:
|
|
93
|
+
config["servers"] = []
|
|
94
|
+
|
|
95
|
+
if not isinstance(config["servers"], list):
|
|
96
|
+
raise ValueError("'servers' must be an array")
|
|
97
|
+
|
|
98
|
+
return config
|
|
99
|
+
|
|
100
|
+
except json.JSONDecodeError as e:
|
|
101
|
+
raise ValueError(f"Invalid JSON in MCP config file: {e}") from e
|
|
102
|
+
|
|
103
|
+
def _write_config(self, config: dict[str, Any]) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Write MCP configuration to file.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
config: Configuration dictionary to write
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
OSError: If file operations fail
|
|
112
|
+
"""
|
|
113
|
+
# Write to temporary file first for atomic write
|
|
114
|
+
temp_path = self.config_path.with_suffix(".tmp")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
with open(temp_path, "w") as f:
|
|
118
|
+
json.dump(config, f, indent=2)
|
|
119
|
+
|
|
120
|
+
# Atomic rename
|
|
121
|
+
temp_path.replace(self.config_path)
|
|
122
|
+
|
|
123
|
+
# Set restrictive permissions (owner read/write only)
|
|
124
|
+
self.config_path.chmod(0o600)
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
# Clean up temp file if it exists
|
|
128
|
+
if temp_path.exists():
|
|
129
|
+
temp_path.unlink()
|
|
130
|
+
raise OSError(f"Failed to write MCP config: {e}") from e
|
|
131
|
+
|
|
132
|
+
def load_servers(self) -> list[MCPServerConfig]:
|
|
133
|
+
"""
|
|
134
|
+
Load MCP server configurations from file.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
List of MCPServerConfig objects
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValueError: If configuration is invalid
|
|
141
|
+
"""
|
|
142
|
+
config = self._read_config()
|
|
143
|
+
servers = []
|
|
144
|
+
|
|
145
|
+
for server_dict in config.get("servers", []):
|
|
146
|
+
try:
|
|
147
|
+
# Validate required fields
|
|
148
|
+
if "name" not in server_dict:
|
|
149
|
+
logger.warning(f"Skipping MCP server config without 'name': {server_dict}")
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Create MCPServerConfig with defaults for optional fields
|
|
153
|
+
# File-based configs use "global" as project_id since they're system-wide
|
|
154
|
+
server_config = MCPServerConfig(
|
|
155
|
+
name=server_dict["name"],
|
|
156
|
+
project_id=server_dict.get("project_id", "global"),
|
|
157
|
+
enabled=server_dict.get("enabled", True),
|
|
158
|
+
transport=server_dict.get("transport", "http"),
|
|
159
|
+
url=server_dict.get("url"),
|
|
160
|
+
headers=server_dict.get("headers"),
|
|
161
|
+
command=server_dict.get("command"),
|
|
162
|
+
args=server_dict.get("args"),
|
|
163
|
+
env=server_dict.get("env"),
|
|
164
|
+
requires_oauth=server_dict.get("requires_oauth", False),
|
|
165
|
+
oauth_provider=server_dict.get("oauth_provider"),
|
|
166
|
+
tools=server_dict.get("tools"),
|
|
167
|
+
description=server_dict.get("description"),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Validate configuration
|
|
171
|
+
server_config.validate()
|
|
172
|
+
|
|
173
|
+
servers.append(server_config)
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(
|
|
177
|
+
f"Failed to load MCP server config '{server_dict.get('name', 'unknown')}': {e}"
|
|
178
|
+
)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
return servers
|
|
182
|
+
|
|
183
|
+
def save_servers(self, servers: list[MCPServerConfig]) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Save MCP server configurations to file.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
servers: List of MCPServerConfig objects to save
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ValueError: If server configuration is invalid
|
|
192
|
+
OSError: If file operations fail
|
|
193
|
+
"""
|
|
194
|
+
# Convert MCPServerConfig objects to dictionaries
|
|
195
|
+
server_dicts = []
|
|
196
|
+
|
|
197
|
+
for server in servers:
|
|
198
|
+
# Validate before saving
|
|
199
|
+
server.validate()
|
|
200
|
+
|
|
201
|
+
server_dict = {
|
|
202
|
+
"name": server.name,
|
|
203
|
+
"enabled": server.enabled,
|
|
204
|
+
"transport": server.transport,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Add transport-specific fields
|
|
208
|
+
if server.transport in ("http", "websocket", "sse"):
|
|
209
|
+
server_dict["url"] = server.url
|
|
210
|
+
if server.headers:
|
|
211
|
+
server_dict["headers"] = server.headers
|
|
212
|
+
if server.requires_oauth:
|
|
213
|
+
server_dict["requires_oauth"] = server.requires_oauth
|
|
214
|
+
if server.oauth_provider:
|
|
215
|
+
server_dict["oauth_provider"] = server.oauth_provider
|
|
216
|
+
|
|
217
|
+
elif server.transport == "stdio":
|
|
218
|
+
server_dict["command"] = server.command
|
|
219
|
+
if server.args:
|
|
220
|
+
server_dict["args"] = server.args
|
|
221
|
+
if server.env:
|
|
222
|
+
server_dict["env"] = server.env
|
|
223
|
+
|
|
224
|
+
# Add tool metadata if available
|
|
225
|
+
if server.tools:
|
|
226
|
+
server_dict["tools"] = server.tools
|
|
227
|
+
|
|
228
|
+
# Add description if available
|
|
229
|
+
if server.description:
|
|
230
|
+
server_dict["description"] = server.description
|
|
231
|
+
|
|
232
|
+
server_dicts.append(server_dict)
|
|
233
|
+
|
|
234
|
+
# Write to file
|
|
235
|
+
config = {"servers": server_dicts}
|
|
236
|
+
self._write_config(config)
|
|
237
|
+
|
|
238
|
+
def add_server(self, server: MCPServerConfig) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Add MCP server configuration.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
server: MCPServerConfig to add
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ValueError: If server with same name already exists or config is invalid
|
|
247
|
+
OSError: If file operations fail
|
|
248
|
+
"""
|
|
249
|
+
# Validate server config
|
|
250
|
+
server.validate()
|
|
251
|
+
|
|
252
|
+
# Load existing servers
|
|
253
|
+
servers = self.load_servers()
|
|
254
|
+
|
|
255
|
+
# Check for duplicate name
|
|
256
|
+
if any(s.name == server.name for s in servers):
|
|
257
|
+
raise ValueError(f"MCP server '{server.name}' already exists in configuration")
|
|
258
|
+
|
|
259
|
+
# Add new server
|
|
260
|
+
servers.append(server)
|
|
261
|
+
|
|
262
|
+
# Save updated configuration
|
|
263
|
+
self.save_servers(servers)
|
|
264
|
+
|
|
265
|
+
def remove_server(self, name: str) -> None:
|
|
266
|
+
"""
|
|
267
|
+
Remove MCP server configuration by name.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name: Server name to remove
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
ValueError: If server not found
|
|
274
|
+
OSError: If file operations fail
|
|
275
|
+
"""
|
|
276
|
+
# Load existing servers
|
|
277
|
+
servers = self.load_servers()
|
|
278
|
+
|
|
279
|
+
# Find and remove server
|
|
280
|
+
original_count = len(servers)
|
|
281
|
+
servers = [s for s in servers if s.name != name]
|
|
282
|
+
|
|
283
|
+
if len(servers) == original_count:
|
|
284
|
+
raise ValueError(f"MCP server '{name}' not found in configuration")
|
|
285
|
+
|
|
286
|
+
# Save updated configuration
|
|
287
|
+
self.save_servers(servers)
|
|
288
|
+
|
|
289
|
+
def update_server(self, server: MCPServerConfig) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Update existing MCP server configuration.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
server: MCPServerConfig with updated values
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
ValueError: If server not found or config is invalid
|
|
298
|
+
OSError: If file operations fail
|
|
299
|
+
"""
|
|
300
|
+
# Validate server config
|
|
301
|
+
server.validate()
|
|
302
|
+
|
|
303
|
+
# Load existing servers
|
|
304
|
+
servers = self.load_servers()
|
|
305
|
+
|
|
306
|
+
# Find and update server
|
|
307
|
+
found = False
|
|
308
|
+
for i, s in enumerate(servers):
|
|
309
|
+
if s.name == server.name:
|
|
310
|
+
servers[i] = server
|
|
311
|
+
found = True
|
|
312
|
+
break
|
|
313
|
+
|
|
314
|
+
if not found:
|
|
315
|
+
raise ValueError(f"MCP server '{server.name}' not found in configuration")
|
|
316
|
+
|
|
317
|
+
# Save updated configuration
|
|
318
|
+
self.save_servers(servers)
|
|
319
|
+
|
|
320
|
+
def get_server(self, name: str) -> MCPServerConfig | None:
|
|
321
|
+
"""
|
|
322
|
+
Get MCP server configuration by name.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
name: Server name to find
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
MCPServerConfig if found, None otherwise
|
|
329
|
+
"""
|
|
330
|
+
servers = self.load_servers()
|
|
331
|
+
|
|
332
|
+
for server in servers:
|
|
333
|
+
if server.name == name:
|
|
334
|
+
return server
|
|
335
|
+
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def list_servers(self) -> list[str]:
|
|
339
|
+
"""
|
|
340
|
+
Get list of configured MCP server names.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
List of server names
|
|
344
|
+
"""
|
|
345
|
+
servers = self.load_servers()
|
|
346
|
+
return [s.name for s in servers]
|