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