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/config/app.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for Gobby daemon.
|
|
3
|
+
|
|
4
|
+
Provides YAML-based configuration with CLI overrides,
|
|
5
|
+
configuration hierarchy (CLI > YAML > Defaults), and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator
|
|
17
|
+
|
|
18
|
+
from gobby.config.extensions import (
|
|
19
|
+
HookExtensionsConfig,
|
|
20
|
+
PluginItemConfig,
|
|
21
|
+
PluginsConfig,
|
|
22
|
+
WebhookEndpointConfig,
|
|
23
|
+
WebhooksConfig,
|
|
24
|
+
WebSocketBroadcastConfig,
|
|
25
|
+
)
|
|
26
|
+
from gobby.config.features import (
|
|
27
|
+
ImportMCPServerConfig,
|
|
28
|
+
MetricsConfig,
|
|
29
|
+
ProjectVerificationConfig,
|
|
30
|
+
RecommendToolsConfig,
|
|
31
|
+
TaskDescriptionConfig,
|
|
32
|
+
ToolSummarizerConfig,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Re-export from extracted modules (Strangler Fig pattern for backwards compatibility)
|
|
36
|
+
from gobby.config.llm_providers import LLMProviderConfig, LLMProvidersConfig
|
|
37
|
+
from gobby.config.logging import LoggingSettings
|
|
38
|
+
from gobby.config.persistence import (
|
|
39
|
+
MemoryConfig,
|
|
40
|
+
MemorySyncConfig,
|
|
41
|
+
)
|
|
42
|
+
from gobby.config.servers import MCPClientProxyConfig, WebSocketSettings
|
|
43
|
+
from gobby.config.sessions import (
|
|
44
|
+
ArtifactHandoffConfig,
|
|
45
|
+
ContextInjectionConfig,
|
|
46
|
+
MessageTrackingConfig,
|
|
47
|
+
SessionLifecycleConfig,
|
|
48
|
+
SessionSummaryConfig,
|
|
49
|
+
TitleSynthesisConfig,
|
|
50
|
+
)
|
|
51
|
+
from gobby.config.tasks import (
|
|
52
|
+
CompactHandoffConfig,
|
|
53
|
+
GobbyTasksConfig,
|
|
54
|
+
PatternCriteriaConfig,
|
|
55
|
+
TaskExpansionConfig,
|
|
56
|
+
TaskValidationConfig,
|
|
57
|
+
WorkflowConfig,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Explicit exports for mypy (re-exported symbols from submodules)
|
|
61
|
+
__all__ = [
|
|
62
|
+
# From gobby.config.extensions
|
|
63
|
+
"HookExtensionsConfig",
|
|
64
|
+
"PluginItemConfig",
|
|
65
|
+
"PluginsConfig",
|
|
66
|
+
"WebhookEndpointConfig",
|
|
67
|
+
"WebhooksConfig",
|
|
68
|
+
"WebSocketBroadcastConfig",
|
|
69
|
+
# From gobby.config.features
|
|
70
|
+
"ImportMCPServerConfig",
|
|
71
|
+
"MetricsConfig",
|
|
72
|
+
"ProjectVerificationConfig",
|
|
73
|
+
"RecommendToolsConfig",
|
|
74
|
+
"TaskDescriptionConfig",
|
|
75
|
+
"ToolSummarizerConfig",
|
|
76
|
+
# From gobby.config.llm_providers
|
|
77
|
+
"LLMProviderConfig",
|
|
78
|
+
"LLMProvidersConfig",
|
|
79
|
+
# From gobby.config.logging
|
|
80
|
+
"LoggingSettings",
|
|
81
|
+
# From gobby.config.persistence
|
|
82
|
+
"MemoryConfig",
|
|
83
|
+
"MemorySyncConfig",
|
|
84
|
+
# From gobby.config.servers
|
|
85
|
+
"MCPClientProxyConfig",
|
|
86
|
+
"WebSocketSettings",
|
|
87
|
+
# From gobby.config.sessions
|
|
88
|
+
"ArtifactHandoffConfig",
|
|
89
|
+
"ContextInjectionConfig",
|
|
90
|
+
"MessageTrackingConfig",
|
|
91
|
+
"SessionLifecycleConfig",
|
|
92
|
+
"SessionSummaryConfig",
|
|
93
|
+
"TitleSynthesisConfig",
|
|
94
|
+
# From gobby.config.tasks
|
|
95
|
+
"CompactHandoffConfig",
|
|
96
|
+
"GobbyTasksConfig",
|
|
97
|
+
"PatternCriteriaConfig",
|
|
98
|
+
"TaskExpansionConfig",
|
|
99
|
+
"TaskValidationConfig",
|
|
100
|
+
"WorkflowConfig",
|
|
101
|
+
# Local definitions
|
|
102
|
+
"DaemonConfig",
|
|
103
|
+
"expand_env_vars",
|
|
104
|
+
"load_yaml",
|
|
105
|
+
"apply_cli_overrides",
|
|
106
|
+
"generate_default_config",
|
|
107
|
+
"load_config",
|
|
108
|
+
"save_config",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# Pattern for environment variable substitution:
|
|
112
|
+
# ${VAR} - simple substitution
|
|
113
|
+
# ${VAR:-default} - with default value if VAR is unset or empty
|
|
114
|
+
ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def expand_env_vars(content: str) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Expand environment variables in configuration content.
|
|
120
|
+
|
|
121
|
+
Supports two syntaxes:
|
|
122
|
+
- ${VAR} - replaced with the value of VAR, or left unchanged if unset
|
|
123
|
+
- ${VAR:-default} - replaced with VAR's value, or 'default' if unset/empty
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content: Configuration file content as string
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Content with environment variables expanded
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def replace_match(match: re.Match[str]) -> str:
|
|
133
|
+
var_name = match.group(1)
|
|
134
|
+
default_value = match.group(2) # None if no default specified
|
|
135
|
+
|
|
136
|
+
env_value = os.environ.get(var_name)
|
|
137
|
+
|
|
138
|
+
if env_value is not None and env_value != "":
|
|
139
|
+
return env_value
|
|
140
|
+
elif default_value is not None:
|
|
141
|
+
return default_value
|
|
142
|
+
else:
|
|
143
|
+
# Leave unchanged if no value and no default
|
|
144
|
+
return match.group(0)
|
|
145
|
+
|
|
146
|
+
return ENV_VAR_PATTERN.sub(replace_match, content)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# WebSocketSettings moved to gobby.config.servers (re-exported above)
|
|
150
|
+
# LoggingSettings moved to gobby.config.logging (re-exported above)
|
|
151
|
+
# CompactHandoffConfig moved to gobby.config.tasks (re-exported above)
|
|
152
|
+
|
|
153
|
+
# ContextInjectionConfig, SessionSummaryConfig, TitleSynthesisConfig,
|
|
154
|
+
# MessageTrackingConfig, SessionLifecycleConfig
|
|
155
|
+
# moved to gobby.config.sessions (re-exported above)
|
|
156
|
+
|
|
157
|
+
# ToolSummarizerConfig, RecommendToolsConfig, ImportMCPServerConfig,
|
|
158
|
+
# MetricsConfig, ProjectVerificationConfig
|
|
159
|
+
# moved to gobby.config.features (re-exported above)
|
|
160
|
+
|
|
161
|
+
# WebSocketBroadcastConfig, WebhookEndpointConfig, WebhooksConfig,
|
|
162
|
+
# PluginItemConfig, PluginsConfig, HookExtensionsConfig
|
|
163
|
+
# moved to gobby.config.extensions (re-exported above)
|
|
164
|
+
|
|
165
|
+
# PatternCriteriaConfig, TaskExpansionConfig, TaskValidationConfig, WorkflowConfig,
|
|
166
|
+
# GobbyTasksConfig, CompactHandoffConfig
|
|
167
|
+
# moved to gobby.config.tasks (re-exported above)
|
|
168
|
+
|
|
169
|
+
# MCPClientProxyConfig moved to gobby.config.servers (re-exported above)
|
|
170
|
+
# LLMProviderConfig and LLMProvidersConfig moved to gobby.config.llm_providers (re-exported above)
|
|
171
|
+
# MemoryConfig, MemorySyncConfig
|
|
172
|
+
# moved to gobby.config.persistence (re-exported above)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class DaemonConfig(BaseModel):
|
|
176
|
+
"""
|
|
177
|
+
Main configuration for Gobby daemon.
|
|
178
|
+
|
|
179
|
+
Configuration is loaded with the following priority:
|
|
180
|
+
1. CLI arguments (highest)
|
|
181
|
+
2. YAML file (~/.gobby/config.yaml)
|
|
182
|
+
3. Defaults (lowest)
|
|
183
|
+
|
|
184
|
+
Note: machine_id is stored separately in ~/.gobby/machine_id
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
model_config = {"populate_by_name": True}
|
|
188
|
+
|
|
189
|
+
# Daemon settings
|
|
190
|
+
daemon_port: int = Field(
|
|
191
|
+
default=8765,
|
|
192
|
+
description="Port for daemon to listen on",
|
|
193
|
+
)
|
|
194
|
+
daemon_health_check_interval: float = Field(
|
|
195
|
+
default=10.0,
|
|
196
|
+
description="Daemon health check interval in seconds",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Local storage
|
|
200
|
+
database_path: str = Field(
|
|
201
|
+
default="~/.gobby/gobby-hub.db",
|
|
202
|
+
description="Path to hub database for cross-project queries.",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Sub-configs
|
|
206
|
+
websocket: WebSocketSettings = Field(
|
|
207
|
+
default_factory=WebSocketSettings,
|
|
208
|
+
description="WebSocket server configuration",
|
|
209
|
+
)
|
|
210
|
+
logging: LoggingSettings = Field(
|
|
211
|
+
default_factory=LoggingSettings,
|
|
212
|
+
description="Logging configuration",
|
|
213
|
+
)
|
|
214
|
+
session_summary: SessionSummaryConfig = Field(
|
|
215
|
+
default_factory=SessionSummaryConfig,
|
|
216
|
+
description="Session summary generation configuration",
|
|
217
|
+
)
|
|
218
|
+
compact_handoff: CompactHandoffConfig = Field(
|
|
219
|
+
default_factory=CompactHandoffConfig,
|
|
220
|
+
description="Compact handoff context configuration",
|
|
221
|
+
)
|
|
222
|
+
context_injection: ContextInjectionConfig = Field(
|
|
223
|
+
default_factory=ContextInjectionConfig,
|
|
224
|
+
description="Context injection configuration for subagent spawning",
|
|
225
|
+
)
|
|
226
|
+
artifact_handoff: ArtifactHandoffConfig = Field(
|
|
227
|
+
default_factory=ArtifactHandoffConfig,
|
|
228
|
+
description="Artifact inclusion in handoff context configuration",
|
|
229
|
+
)
|
|
230
|
+
mcp_client_proxy: MCPClientProxyConfig = Field(
|
|
231
|
+
default_factory=MCPClientProxyConfig,
|
|
232
|
+
description="MCP client proxy configuration",
|
|
233
|
+
)
|
|
234
|
+
gobby_tasks: GobbyTasksConfig = Field(
|
|
235
|
+
default_factory=GobbyTasksConfig,
|
|
236
|
+
alias="gobby-tasks",
|
|
237
|
+
serialization_alias="gobby-tasks",
|
|
238
|
+
description="gobby-tasks internal MCP server configuration",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Multi-provider LLM configuration
|
|
242
|
+
llm_providers: LLMProvidersConfig = Field(
|
|
243
|
+
default_factory=LLMProvidersConfig,
|
|
244
|
+
description="Multi-provider LLM configuration",
|
|
245
|
+
)
|
|
246
|
+
title_synthesis: TitleSynthesisConfig = Field(
|
|
247
|
+
default_factory=TitleSynthesisConfig,
|
|
248
|
+
description="Title synthesis configuration",
|
|
249
|
+
)
|
|
250
|
+
recommend_tools: RecommendToolsConfig = Field(
|
|
251
|
+
default_factory=RecommendToolsConfig,
|
|
252
|
+
description="Tool recommendation configuration",
|
|
253
|
+
)
|
|
254
|
+
tool_summarizer: ToolSummarizerConfig = Field(
|
|
255
|
+
default_factory=ToolSummarizerConfig,
|
|
256
|
+
description="Tool description summarization configuration",
|
|
257
|
+
)
|
|
258
|
+
task_description: TaskDescriptionConfig = Field(
|
|
259
|
+
default_factory=TaskDescriptionConfig,
|
|
260
|
+
description="LLM-based task description generation configuration",
|
|
261
|
+
)
|
|
262
|
+
import_mcp_server: ImportMCPServerConfig = Field(
|
|
263
|
+
default_factory=ImportMCPServerConfig,
|
|
264
|
+
description="MCP server import configuration",
|
|
265
|
+
)
|
|
266
|
+
hook_extensions: HookExtensionsConfig = Field(
|
|
267
|
+
default_factory=HookExtensionsConfig,
|
|
268
|
+
description="Hook extensions configuration",
|
|
269
|
+
)
|
|
270
|
+
workflow: WorkflowConfig = Field(
|
|
271
|
+
default_factory=WorkflowConfig,
|
|
272
|
+
description="Workflow engine configuration",
|
|
273
|
+
)
|
|
274
|
+
memory: MemoryConfig = Field(
|
|
275
|
+
default_factory=MemoryConfig,
|
|
276
|
+
description="Memory system configuration",
|
|
277
|
+
)
|
|
278
|
+
memory_sync: MemorySyncConfig = Field(
|
|
279
|
+
default_factory=MemorySyncConfig,
|
|
280
|
+
description="Memory synchronization configuration",
|
|
281
|
+
)
|
|
282
|
+
message_tracking: MessageTrackingConfig = Field(
|
|
283
|
+
default_factory=MessageTrackingConfig,
|
|
284
|
+
description="Session message tracking configuration",
|
|
285
|
+
)
|
|
286
|
+
session_lifecycle: SessionLifecycleConfig = Field(
|
|
287
|
+
default_factory=SessionLifecycleConfig,
|
|
288
|
+
description="Session lifecycle management configuration",
|
|
289
|
+
)
|
|
290
|
+
metrics: MetricsConfig = Field(
|
|
291
|
+
default_factory=MetricsConfig,
|
|
292
|
+
description="Metrics and status endpoint configuration",
|
|
293
|
+
)
|
|
294
|
+
verification_defaults: ProjectVerificationConfig = Field(
|
|
295
|
+
default_factory=ProjectVerificationConfig,
|
|
296
|
+
description="Default verification commands for projects without auto-detected config",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def get_recommend_tools_config(self) -> RecommendToolsConfig:
|
|
300
|
+
"""Get recommend_tools configuration."""
|
|
301
|
+
return self.recommend_tools
|
|
302
|
+
|
|
303
|
+
def get_tool_summarizer_config(self) -> ToolSummarizerConfig:
|
|
304
|
+
"""Get tool_summarizer configuration."""
|
|
305
|
+
return self.tool_summarizer
|
|
306
|
+
|
|
307
|
+
def get_task_description_config(self) -> TaskDescriptionConfig:
|
|
308
|
+
"""Get task_description configuration."""
|
|
309
|
+
return self.task_description
|
|
310
|
+
|
|
311
|
+
def get_import_mcp_server_config(self) -> ImportMCPServerConfig:
|
|
312
|
+
"""Get import_mcp_server configuration."""
|
|
313
|
+
return self.import_mcp_server
|
|
314
|
+
|
|
315
|
+
def get_mcp_client_proxy_config(self) -> MCPClientProxyConfig:
|
|
316
|
+
"""Get MCP client proxy configuration."""
|
|
317
|
+
return self.mcp_client_proxy
|
|
318
|
+
|
|
319
|
+
def get_memory_config(self) -> MemoryConfig:
|
|
320
|
+
"""Get memory configuration."""
|
|
321
|
+
return self.memory
|
|
322
|
+
|
|
323
|
+
def get_memory_sync_config(self) -> MemorySyncConfig:
|
|
324
|
+
"""Get memory sync configuration."""
|
|
325
|
+
return self.memory_sync
|
|
326
|
+
|
|
327
|
+
def get_gobby_tasks_config(self) -> GobbyTasksConfig:
|
|
328
|
+
"""Get gobby-tasks configuration."""
|
|
329
|
+
return self.gobby_tasks
|
|
330
|
+
|
|
331
|
+
def get_metrics_config(self) -> MetricsConfig:
|
|
332
|
+
"""Get metrics configuration."""
|
|
333
|
+
return self.metrics
|
|
334
|
+
|
|
335
|
+
def get_verification_defaults(self) -> ProjectVerificationConfig:
|
|
336
|
+
"""Get default verification commands configuration."""
|
|
337
|
+
return self.verification_defaults
|
|
338
|
+
|
|
339
|
+
@field_validator("daemon_port")
|
|
340
|
+
@classmethod
|
|
341
|
+
def validate_port(cls, v: int) -> int:
|
|
342
|
+
"""Validate port number is in valid range."""
|
|
343
|
+
if not (1024 <= v <= 65535):
|
|
344
|
+
raise ValueError("Port must be between 1024 and 65535")
|
|
345
|
+
return v
|
|
346
|
+
|
|
347
|
+
@field_validator("daemon_health_check_interval")
|
|
348
|
+
@classmethod
|
|
349
|
+
def validate_health_check_interval(cls, v: float) -> float:
|
|
350
|
+
"""Validate health check interval is in valid range."""
|
|
351
|
+
if not (1.0 <= v <= 300.0):
|
|
352
|
+
raise ValueError("daemon_health_check_interval must be between 1.0 and 300.0 seconds")
|
|
353
|
+
return v
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def load_yaml(config_file: str) -> dict[str, Any]:
|
|
357
|
+
"""
|
|
358
|
+
Load YAML or JSON configuration file.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
config_file: Path to YAML or JSON configuration file
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Dictionary with parsed YAML/JSON content
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
ValueError: If YAML/JSON is invalid or file format is wrong
|
|
368
|
+
"""
|
|
369
|
+
config_path = Path(config_file).expanduser()
|
|
370
|
+
|
|
371
|
+
if not config_path.exists():
|
|
372
|
+
return {}
|
|
373
|
+
|
|
374
|
+
# Validate file extension matches format
|
|
375
|
+
file_ext = config_path.suffix.lower()
|
|
376
|
+
if file_ext not in [".yaml", ".yml", ".json"]:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
f"Config file must have .yaml, .yml, or .json extension, got: {file_ext}\n"
|
|
379
|
+
f"File: {config_path}"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
import json
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
with open(config_path) as f:
|
|
386
|
+
content = f.read()
|
|
387
|
+
|
|
388
|
+
# Expand environment variables before parsing
|
|
389
|
+
content = expand_env_vars(content)
|
|
390
|
+
|
|
391
|
+
# Handle JSON files
|
|
392
|
+
if file_ext == ".json":
|
|
393
|
+
return json.loads(content) if content.strip() else {}
|
|
394
|
+
|
|
395
|
+
# Handle YAML files
|
|
396
|
+
data = yaml.safe_load(content)
|
|
397
|
+
return data if data is not None else {}
|
|
398
|
+
|
|
399
|
+
except yaml.YAMLError as e:
|
|
400
|
+
raise ValueError(f"Invalid YAML in config file: {e}") from e
|
|
401
|
+
except json.JSONDecodeError as e:
|
|
402
|
+
raise ValueError(f"Invalid JSON in config file: {e}") from e
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def apply_cli_overrides(
|
|
406
|
+
config_dict: dict[str, Any],
|
|
407
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
408
|
+
) -> dict[str, Any]:
|
|
409
|
+
"""
|
|
410
|
+
Apply CLI argument overrides to config dictionary.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
config_dict: Configuration dictionary
|
|
414
|
+
cli_overrides: Dictionary of CLI overrides
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Configuration dictionary with CLI overrides applied
|
|
418
|
+
"""
|
|
419
|
+
if cli_overrides is None:
|
|
420
|
+
return config_dict
|
|
421
|
+
|
|
422
|
+
# Apply overrides at top level
|
|
423
|
+
for key, value in cli_overrides.items():
|
|
424
|
+
if "." in key:
|
|
425
|
+
# Handle nested keys like "logging.level"
|
|
426
|
+
parts = key.split(".")
|
|
427
|
+
current = config_dict
|
|
428
|
+
for part in parts[:-1]:
|
|
429
|
+
if part not in current:
|
|
430
|
+
current[part] = {}
|
|
431
|
+
current = current[part]
|
|
432
|
+
current[parts[-1]] = value
|
|
433
|
+
else:
|
|
434
|
+
config_dict[key] = value
|
|
435
|
+
|
|
436
|
+
return config_dict
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def generate_default_config(config_file: str) -> None:
|
|
440
|
+
"""
|
|
441
|
+
Generate default configuration file from Pydantic model defaults.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
config_file: Path where to create the config file
|
|
445
|
+
"""
|
|
446
|
+
config_path = Path(config_file).expanduser()
|
|
447
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
448
|
+
|
|
449
|
+
# Use Pydantic model defaults as source of truth
|
|
450
|
+
# mode="json" ensures Path objects are converted to strings for YAML serialization
|
|
451
|
+
default_config = DaemonConfig().model_dump(mode="json", exclude_none=True)
|
|
452
|
+
|
|
453
|
+
with open(config_path, "w") as f:
|
|
454
|
+
yaml.safe_dump(default_config, f, default_flow_style=False, sort_keys=False)
|
|
455
|
+
|
|
456
|
+
# Set restrictive permissions (owner read/write only)
|
|
457
|
+
config_path.chmod(0o600)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def load_config(
|
|
461
|
+
config_file: str | None = None,
|
|
462
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
463
|
+
create_default: bool = False,
|
|
464
|
+
) -> DaemonConfig:
|
|
465
|
+
"""
|
|
466
|
+
Load configuration with hierarchy: CLI > YAML > Defaults.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
config_file: Path to YAML config file (default: ~/.gobby/config.yaml)
|
|
470
|
+
cli_overrides: Dictionary of CLI argument overrides
|
|
471
|
+
create_default: Create default config file if it doesn't exist
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Validated DaemonConfig instance
|
|
475
|
+
|
|
476
|
+
Raises:
|
|
477
|
+
ValueError: If configuration is invalid or required fields are missing
|
|
478
|
+
"""
|
|
479
|
+
if config_file is None:
|
|
480
|
+
config_file = "~/.gobby/config.yaml"
|
|
481
|
+
|
|
482
|
+
config_path = Path(config_file).expanduser()
|
|
483
|
+
|
|
484
|
+
# Create default config if requested and file doesn't exist
|
|
485
|
+
if create_default and not config_path.exists():
|
|
486
|
+
generate_default_config(config_file)
|
|
487
|
+
|
|
488
|
+
# Load YAML configuration
|
|
489
|
+
config_dict = load_yaml(config_file)
|
|
490
|
+
|
|
491
|
+
# Apply CLI argument overrides
|
|
492
|
+
config_dict = apply_cli_overrides(config_dict, cli_overrides)
|
|
493
|
+
|
|
494
|
+
# SAFETY SWITCH: Protect production resources during tests
|
|
495
|
+
# If GOBBY_TEST_PROTECT is set, force safe paths from environment
|
|
496
|
+
if os.environ.get("GOBBY_TEST_PROTECT") == "1":
|
|
497
|
+
# Override database path
|
|
498
|
+
if safe_db := os.environ.get("GOBBY_DATABASE_PATH"):
|
|
499
|
+
config_dict["database_path"] = safe_db
|
|
500
|
+
|
|
501
|
+
# Override logging paths
|
|
502
|
+
logging_config = config_dict.setdefault("logging", {})
|
|
503
|
+
if safe_client := os.environ.get("GOBBY_LOGGING_CLIENT"):
|
|
504
|
+
logging_config["client"] = safe_client
|
|
505
|
+
if safe_error := os.environ.get("GOBBY_LOGGING_CLIENT_ERROR"):
|
|
506
|
+
logging_config["client_error"] = safe_error
|
|
507
|
+
if safe_mcp_server := os.environ.get("GOBBY_LOGGING_MCP_SERVER"):
|
|
508
|
+
logging_config["mcp_server"] = safe_mcp_server
|
|
509
|
+
if safe_mcp_client := os.environ.get("GOBBY_LOGGING_MCP_CLIENT"):
|
|
510
|
+
logging_config["mcp_client"] = safe_mcp_client
|
|
511
|
+
|
|
512
|
+
# Validate and create config object
|
|
513
|
+
try:
|
|
514
|
+
config = DaemonConfig(**config_dict)
|
|
515
|
+
return config
|
|
516
|
+
except Exception as e:
|
|
517
|
+
raise ValueError(
|
|
518
|
+
f"Configuration validation failed: {e}\n"
|
|
519
|
+
f"Please check your configuration file at {config_file}"
|
|
520
|
+
) from e
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def save_config(config: DaemonConfig, config_file: str | None = None) -> None:
|
|
524
|
+
"""
|
|
525
|
+
Save configuration to YAML file.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
config: DaemonConfig instance to save
|
|
529
|
+
config_file: Path to YAML config file (default: ~/.gobby/config.yaml)
|
|
530
|
+
|
|
531
|
+
Raises:
|
|
532
|
+
OSError: If file operations fail
|
|
533
|
+
"""
|
|
534
|
+
if config_file is None:
|
|
535
|
+
config_file = "~/.gobby/config.yaml"
|
|
536
|
+
|
|
537
|
+
config_path = Path(config_file).expanduser()
|
|
538
|
+
|
|
539
|
+
# Ensure directory exists
|
|
540
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
541
|
+
|
|
542
|
+
# Convert config to dict, excluding None values to keep file clean
|
|
543
|
+
# mode="json" ensures Path objects are converted to strings for YAML serialization
|
|
544
|
+
config_dict = config.model_dump(mode="json", exclude_none=True)
|
|
545
|
+
|
|
546
|
+
# Write to YAML file
|
|
547
|
+
with open(config_path, "w") as f:
|
|
548
|
+
yaml.safe_dump(config_dict, f, default_flow_style=False, sort_keys=False)
|
|
549
|
+
|
|
550
|
+
# Set restrictive permissions (owner read/write only)
|
|
551
|
+
config_path.chmod(0o600)
|