gobby 0.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
@@ -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]