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,235 @@
1
+ """
2
+ DaemonClient - HTTP communication with Gobby daemon.
3
+
4
+ This module provides a clean interface for communicating with the Gobby daemon's
5
+ HTTP API. It handles health checks, authentication verification, and HTTP API calls.
6
+
7
+ The DaemonClient is session-agnostic and thread-safe, designed to be shared across
8
+ multiple sessions while maintaining cached health status for performance.
9
+
10
+ Example:
11
+ ```python
12
+ from gobby.utils.daemon_client import DaemonClient
13
+
14
+ client = DaemonClient(host="localhost", port=8765)
15
+
16
+ # Check daemon health
17
+ is_healthy, error = client.check_health()
18
+
19
+ # Call HTTP API endpoint
20
+ response = client.call_http_api("/sessions/register", method="POST", json_data={
21
+ "external_id": "abc123"
22
+ })
23
+ ```
24
+ """
25
+
26
+ import logging
27
+ import threading
28
+ from typing import Any, ClassVar, cast
29
+
30
+ import httpx
31
+
32
+
33
+ class DaemonClient:
34
+ """
35
+ Client for communicating with Gobby daemon HTTP API.
36
+
37
+ Provides methods for:
38
+ - Health checking with caching
39
+ - Authentication verification
40
+ - HTTP API calls
41
+
42
+ Thread-safe and session-agnostic.
43
+
44
+ Attributes:
45
+ url: Base URL for daemon HTTP API
46
+ timeout: Request timeout in seconds
47
+ logger: Logger instance for this client
48
+ """
49
+
50
+ # Status text mapping (class-level constant)
51
+ DAEMON_STATUS_TEXT: ClassVar[dict[str, str]] = {
52
+ "not_running": "Not Running",
53
+ "cannot_access": "Cannot Access",
54
+ "ready": "Ready",
55
+ }
56
+
57
+ def __init__(
58
+ self,
59
+ host: str = "localhost",
60
+ port: int = 8765,
61
+ timeout: float = 5.0,
62
+ logger: logging.Logger | None = None,
63
+ ):
64
+ """
65
+ Initialize DaemonClient.
66
+
67
+ Args:
68
+ host: Daemon host address
69
+ port: Daemon port number
70
+ timeout: HTTP request timeout in seconds
71
+ logger: Optional logger instance (creates one if not provided)
72
+ """
73
+ self.url = f"http://{host}:{port}"
74
+ self.timeout = timeout
75
+ self.logger = logger or logging.getLogger(__name__)
76
+
77
+ # Health status cache (thread-safe)
78
+ self._cache_lock = threading.Lock()
79
+ self._cached_is_ready: bool | None = None
80
+ self._cached_message: str | None = None
81
+ self._cached_status: str | None = None
82
+ self._cached_error: str | None = None
83
+
84
+ def check_health(self) -> tuple[bool, str | None]:
85
+ """
86
+ Check if daemon is available and healthy.
87
+
88
+ Returns:
89
+ Tuple of (is_healthy, error_reason) where:
90
+ - is_healthy: True if daemon is healthy, False otherwise
91
+ - error_reason: None if healthy, otherwise error description
92
+ """
93
+ try:
94
+ response = httpx.get(
95
+ f"{self.url}/admin/status",
96
+ timeout=self.timeout,
97
+ )
98
+ is_healthy = response.status_code == 200
99
+ if is_healthy:
100
+ self.logger.info(f"Daemon health check passed at {self.url}")
101
+ return True, None
102
+ else:
103
+ error_reason = f"HTTP {response.status_code}"
104
+ self.logger.warning(f"Daemon health check failed: status {response.status_code}")
105
+ return False, error_reason
106
+ except Exception as e:
107
+ error_msg = str(e)
108
+ # Check if it's a connection refused (daemon not running)
109
+ if "refused" in error_msg.lower() or "connection" in error_msg.lower():
110
+ self.logger.warning(f"Daemon not running: {e}")
111
+ return False, None # None means daemon not running
112
+ else:
113
+ # Other errors (timeout, DNS, etc.)
114
+ self.logger.error(f"Daemon health check error: {e}")
115
+ return False, error_msg
116
+
117
+ def check_status(self) -> tuple[bool, str | None, str, str | None]:
118
+ """
119
+ Check daemon health status.
120
+
121
+ Returns:
122
+ Tuple of (is_ready, message, status, error_reason) where:
123
+ - is_ready: True if daemon is healthy
124
+ - message: Human-readable status message
125
+ - status: One of: "ready", "not_running", "cannot_access"
126
+ - error_reason: Error details if status != "ready"
127
+ """
128
+ is_healthy, health_error = self.check_health()
129
+
130
+ if not is_healthy:
131
+ if health_error is None:
132
+ return False, "Daemon is not running", "not_running", None
133
+ else:
134
+ return False, f"Cannot access daemon: {health_error}", "cannot_access", health_error
135
+
136
+ return True, "Daemon is ready", "ready", None
137
+
138
+ def call_http_api(
139
+ self,
140
+ endpoint: str,
141
+ method: str = "POST",
142
+ json_data: dict[str, Any] | None = None,
143
+ timeout: float | None = None,
144
+ ) -> Any:
145
+ """
146
+ Call daemon HTTP API endpoint directly (for non-MCP endpoints).
147
+
148
+ Args:
149
+ endpoint: API endpoint path (e.g., "/sessions/register")
150
+ method: HTTP method (default: POST)
151
+ json_data: JSON data to send
152
+ timeout: Request timeout (default: uses self.timeout)
153
+
154
+ Returns:
155
+ Response object (httpx.Response)
156
+ """
157
+ url = f"{self.url}{endpoint}"
158
+ timeout_val = timeout or self.timeout
159
+
160
+ try:
161
+ if method.upper() == "GET":
162
+ response = httpx.get(url, timeout=timeout_val)
163
+ elif method.upper() == "POST":
164
+ response = httpx.post(url, json=json_data, timeout=timeout_val)
165
+ elif method.upper() == "PUT":
166
+ response = httpx.put(url, json=json_data, timeout=timeout_val)
167
+ elif method.upper() == "DELETE":
168
+ response = httpx.delete(url, timeout=timeout_val)
169
+ else:
170
+ raise ValueError(f"Unsupported HTTP method: {method}")
171
+
172
+ return response
173
+
174
+ except Exception as e:
175
+ self.logger.error(f"HTTP API call failed: {method} {endpoint} - {e}")
176
+ raise
177
+
178
+ def call_mcp_tool(
179
+ self,
180
+ server_name: str,
181
+ tool_name: str,
182
+ arguments: dict[str, Any],
183
+ timeout: float | None = None,
184
+ ) -> dict[str, Any]:
185
+ """
186
+ Call an MCP tool via the daemon's HTTP API.
187
+
188
+ Args:
189
+ server_name: Name of the MCP server
190
+ tool_name: Name of the tool to call
191
+ arguments: Tool arguments
192
+ timeout: Request timeout
193
+
194
+ Returns:
195
+ Tool execution result
196
+ """
197
+ endpoint = f"/mcp/{server_name}/tools/{tool_name}"
198
+ response = self.call_http_api(
199
+ endpoint=endpoint,
200
+ method="POST",
201
+ json_data=arguments,
202
+ timeout=timeout,
203
+ )
204
+ response.raise_for_status()
205
+ return cast(dict[str, Any], response.json())
206
+
207
+ def update_status_cache(self) -> None:
208
+ """Update cached daemon status by calling check_status()."""
209
+ with self._cache_lock:
210
+ (
211
+ self._cached_is_ready,
212
+ self._cached_message,
213
+ self._cached_status,
214
+ self._cached_error,
215
+ ) = self.check_status()
216
+
217
+ self.logger.debug(
218
+ f"Daemon status updated: {self.DAEMON_STATUS_TEXT.get(self._cached_status, 'Unknown')}"
219
+ )
220
+
221
+ def get_cached_status(self) -> tuple[bool | None, str | None, str | None, str | None]:
222
+ """
223
+ Get cached daemon status without making HTTP calls.
224
+
225
+ Returns:
226
+ Tuple of (is_ready, message, status, error_reason)
227
+ Values may be None if status hasn't been checked yet.
228
+ """
229
+ with self._cache_lock:
230
+ return (
231
+ self._cached_is_ready,
232
+ self._cached_message,
233
+ self._cached_status,
234
+ self._cached_error,
235
+ )
gobby/utils/git.py ADDED
@@ -0,0 +1,222 @@
1
+ """
2
+ Git metadata extraction utilities for Gobby Client.
3
+
4
+ Provides functions to extract git repository information including:
5
+ - Repository remote URL
6
+ - Current branch name
7
+
8
+ Handles git worktrees, detached HEAD, and missing remotes gracefully.
9
+ """
10
+
11
+ import logging
12
+ import subprocess # nosec B404 - subprocess needed for git commands
13
+ from pathlib import Path
14
+ from typing import TypedDict
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class GitMetadata(TypedDict, total=False):
20
+ """Git repository metadata structure."""
21
+
22
+ github_url: str | None
23
+ git_branch: str | None
24
+
25
+
26
+ def run_git_command(command: list[str], cwd: str | Path, timeout: int = 5) -> str | None:
27
+ """
28
+ Execute a git command safely with timeout protection.
29
+
30
+ Args:
31
+ command: Git command as list of strings (e.g., ["git", "branch", "--show-current"])
32
+ cwd: Working directory where git command should run
33
+ timeout: Command timeout in seconds (default: 5)
34
+
35
+ Returns:
36
+ Command output as string (stripped), or None if command fails
37
+ """
38
+ try:
39
+ result = subprocess.run( # nosec B603 - command passed from internal callers with hardcoded git commands
40
+ command,
41
+ cwd=cwd,
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=timeout,
45
+ check=False, # Don't raise on non-zero exit
46
+ )
47
+
48
+ if result.returncode == 0:
49
+ return result.stdout.strip()
50
+
51
+ logger.debug(f"Git command failed: {' '.join(command)}, stderr: {result.stderr.strip()}")
52
+ return None
53
+
54
+ except subprocess.TimeoutExpired:
55
+ logger.warning(f"Git command timed out after {timeout}s: {' '.join(command)}")
56
+ return None
57
+ except FileNotFoundError:
58
+ logger.warning("Git executable not found in PATH")
59
+ return None
60
+ except Exception as e:
61
+ logger.error(f"Git command error: {' '.join(command)}, error: {e}")
62
+ return None
63
+
64
+
65
+ def get_github_url(cwd: str | Path) -> str | None:
66
+ """
67
+ Extract git repository URL from origin remote.
68
+
69
+ Args:
70
+ cwd: Working directory (git repository path)
71
+
72
+ Returns:
73
+ Remote URL string, or None if not available
74
+ """
75
+ # Try to get origin remote URL
76
+ url = run_git_command(["git", "remote", "get-url", "origin"], cwd)
77
+
78
+ if url:
79
+ # Sanitize URL (remove auth tokens, convert SSH to HTTPS for privacy)
80
+ # Keep original format for now - can sanitize later if needed
81
+ return url
82
+
83
+ # If origin doesn't exist, try to list all remotes and use first one
84
+ remotes = run_git_command(["git", "remote"], cwd)
85
+ if remotes:
86
+ remote_names = remotes.split("\n")
87
+ if remote_names:
88
+ first_remote = remote_names[0]
89
+ url = run_git_command(["git", "remote", "get-url", first_remote], cwd)
90
+ if url:
91
+ logger.debug(f"Using remote '{first_remote}' (origin not found)")
92
+ return url
93
+
94
+ logger.debug("No git remotes found")
95
+ return None
96
+
97
+
98
+ def get_git_branch(cwd: str | Path) -> str | None:
99
+ """
100
+ Get current git branch name.
101
+
102
+ Handles detached HEAD state gracefully.
103
+
104
+ Args:
105
+ cwd: Working directory (git repository path)
106
+
107
+ Returns:
108
+ Branch name string, or None if detached HEAD or error
109
+ """
110
+ branch = run_git_command(["git", "branch", "--show-current"], cwd)
111
+
112
+ if branch:
113
+ return branch
114
+
115
+ # Check if we're in detached HEAD state
116
+ symbolic_ref = run_git_command(["git", "symbolic-ref", "-q", "HEAD"], cwd)
117
+ if symbolic_ref is None:
118
+ logger.debug("Git repository in detached HEAD state")
119
+ return None # Detached HEAD
120
+
121
+ logger.debug("Unable to determine current git branch")
122
+ return None
123
+
124
+
125
+ def get_git_metadata(cwd: str | Path | None = None) -> GitMetadata:
126
+ """
127
+ Extract comprehensive git repository metadata.
128
+
129
+ Extracts:
130
+ - github_url: Remote repository URL (from origin or first remote)
131
+ - git_branch: Current branch name (None if detached HEAD)
132
+
133
+ Handles errors gracefully and works with git worktrees.
134
+
135
+ Args:
136
+ cwd: Working directory to check. Defaults to current directory.
137
+
138
+ Returns:
139
+ GitMetadata dict with available information.
140
+ All fields are optional and will be None if unavailable.
141
+
142
+ Example:
143
+ >>> metadata = get_git_metadata("/path/to/repo")
144
+ >>> metadata["github_url"]
145
+ 'https://github.com/user/repo.git'
146
+ >>> metadata["git_branch"]
147
+ 'main'
148
+ """
149
+ if cwd is None:
150
+ cwd = Path.cwd()
151
+ else:
152
+ cwd = Path(cwd)
153
+
154
+ # Verify path exists
155
+ if not cwd.exists():
156
+ logger.warning(f"Path does not exist: {cwd}")
157
+ return GitMetadata()
158
+
159
+ # Check if directory is in a git repository
160
+ is_git_repo = run_git_command(["git", "rev-parse", "--git-dir"], cwd)
161
+ if not is_git_repo:
162
+ logger.debug(f"Not a git repository: {cwd}")
163
+ return GitMetadata()
164
+
165
+ # Extract metadata
166
+ metadata = GitMetadata()
167
+
168
+ try:
169
+ metadata["github_url"] = get_github_url(cwd)
170
+ metadata["git_branch"] = get_git_branch(cwd)
171
+
172
+ logger.debug(
173
+ f"Git metadata extracted: repo={metadata.get('github_url')}, "
174
+ f"branch={metadata.get('git_branch')}"
175
+ )
176
+
177
+ except Exception as e:
178
+ logger.error(f"Error extracting git metadata: {e}")
179
+
180
+ return metadata
181
+
182
+
183
+ def normalize_commit_sha(sha: str, cwd: str | Path | None = None) -> str | None:
184
+ """
185
+ Normalize a commit SHA to dynamic short format.
186
+
187
+ Uses git rev-parse --short which returns the minimum characters
188
+ needed for uniqueness (typically 7, more in large repos).
189
+
190
+ Args:
191
+ sha: Short or full commit SHA
192
+ cwd: Working directory for git commands (defaults to current directory)
193
+
194
+ Returns:
195
+ Shortened SHA (7+ chars), or None if SHA cannot be resolved
196
+ """
197
+ if not sha or len(sha) < 4:
198
+ return None
199
+
200
+ if cwd is None:
201
+ cwd = Path.cwd()
202
+
203
+ # Use git rev-parse --short to get canonical short form
204
+ result = run_git_command(["git", "rev-parse", "--short", sha], cwd=cwd)
205
+ return result if result else None
206
+
207
+
208
+ def is_valid_sha_format(sha: str) -> bool:
209
+ """
210
+ Check if string looks like a valid SHA format (hex, >= 4 chars).
211
+
212
+ This is a format check only - does not verify the SHA exists in any repo.
213
+
214
+ Args:
215
+ sha: String to validate
216
+
217
+ Returns:
218
+ True if string could be a valid SHA format
219
+ """
220
+ if not sha or len(sha) < 4:
221
+ return False
222
+ return all(c in "0123456789abcdefABCDEF" for c in sha)
gobby/utils/id.py ADDED
@@ -0,0 +1,38 @@
1
+ """ID generation utilities."""
2
+
3
+ import hashlib
4
+ import uuid
5
+
6
+
7
+ def generate_prefixed_id(prefix: str, content: str | None = None, length: int = 8) -> str:
8
+ """
9
+ Generate a prefixed ID (e.g., 'mm-a1b2c3d4').
10
+
11
+ If content is provided, the ID is deterministic based on the content hash.
12
+ If content is None, a random UUID is used.
13
+
14
+ Args:
15
+ prefix: The prefix for the ID (e.g., 'mm', 'sk')
16
+ content: Optional content to hash for deterministic IDs
17
+ length: Length of the hash part (default: 8)
18
+
19
+ Returns:
20
+ Formatted ID string
21
+ Raises:
22
+ ValueError: If prefix is empty or length is invalid
23
+ """
24
+ if not prefix:
25
+ raise ValueError("prefix cannot be empty")
26
+ if length <= 0:
27
+ raise ValueError("length must be positive")
28
+ if length > 64: # SHA-256 produces 64 hex characters
29
+ raise ValueError("length cannot exceed 64")
30
+
31
+ if content is not None:
32
+ hash_obj = hashlib.sha256(content.encode("utf-8"))
33
+ hash_hex = hash_obj.hexdigest()[:length]
34
+ else:
35
+ # Use random UUID if no content provided
36
+ hash_hex = uuid.uuid4().hex[:length]
37
+
38
+ return f"{prefix}-{hash_hex}"
@@ -0,0 +1,161 @@
1
+ """JSON extraction utilities for parsing LLM responses.
2
+
3
+ This module provides robust JSON extraction from text that may contain
4
+ markdown code blocks, preamble text, or other non-JSON content.
5
+
6
+ Also provides typed JSON decoding using msgspec for structured LLM responses.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ from typing import Any
14
+
15
+ import msgspec
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def extract_json_from_text(text: str) -> str | None:
21
+ """
22
+ Extract JSON from text, handling markdown code blocks and mixed content.
23
+
24
+ Uses json.JSONDecoder.raw_decode() which properly handles all JSON
25
+ edge cases (nested strings, escapes, backticks in strings, etc.)
26
+ rather than brittle regex patterns.
27
+
28
+ Args:
29
+ text: Raw text that may contain JSON, possibly wrapped in markdown
30
+ code blocks or with preamble/postamble text.
31
+
32
+ Returns:
33
+ Extracted JSON string, or None if no valid JSON found.
34
+
35
+ Examples:
36
+ >>> extract_json_from_text('{"key": "value"}')
37
+ '{"key": "value"}'
38
+
39
+ >>> extract_json_from_text('Here is the result:\\n```json\\n{"key": "value"}\\n```')
40
+ '{"key": "value"}'
41
+
42
+ >>> extract_json_from_text('No JSON here')
43
+ None
44
+ """
45
+ if not text:
46
+ return None
47
+
48
+ decoder = json.JSONDecoder()
49
+
50
+ # Build list of positions to try, prioritizing code block content
51
+ positions_to_try: list[int] = []
52
+
53
+ # Look for ```json marker first (most specific)
54
+ code_block_idx = text.find("```json")
55
+ if code_block_idx != -1:
56
+ brace_pos = text.find("{", code_block_idx + 7)
57
+ if brace_pos != -1:
58
+ positions_to_try.append(brace_pos)
59
+
60
+ # Then try plain ``` marker
61
+ if not positions_to_try:
62
+ code_block_idx = text.find("```")
63
+ if code_block_idx != -1:
64
+ brace_pos = text.find("{", code_block_idx + 3)
65
+ if brace_pos != -1:
66
+ positions_to_try.append(brace_pos)
67
+
68
+ # Finally try raw JSON (first { in text)
69
+ first_brace = text.find("{")
70
+ if first_brace != -1 and first_brace not in positions_to_try:
71
+ positions_to_try.append(first_brace)
72
+
73
+ # Try each position until we find valid JSON
74
+ for pos in positions_to_try:
75
+ try:
76
+ # raw_decode returns (obj, end_idx) where end_idx is absolute position
77
+ _, end_idx = decoder.raw_decode(text, pos)
78
+ return text[pos:end_idx]
79
+ except json.JSONDecodeError:
80
+ continue
81
+
82
+ return None
83
+
84
+
85
+ def extract_json_object(text: str) -> dict[str, Any] | None:
86
+ """
87
+ Extract and parse a JSON object from text.
88
+
89
+ Convenience wrapper that extracts JSON string and parses it.
90
+
91
+ Args:
92
+ text: Raw text that may contain JSON.
93
+
94
+ Returns:
95
+ Parsed JSON dict, or None if no valid JSON found.
96
+ """
97
+ json_str = extract_json_from_text(text)
98
+ if json_str is None:
99
+ return None
100
+
101
+ try:
102
+ result = json.loads(json_str)
103
+ if isinstance(result, dict):
104
+ return result
105
+ logger.warning(f"Extracted JSON is not an object: {type(result)}")
106
+ return None
107
+ except json.JSONDecodeError as e:
108
+ logger.warning(f"Failed to parse extracted JSON: {e}")
109
+ return None
110
+
111
+
112
+ def decode_llm_response[T](
113
+ text: str,
114
+ response_type: type[T],
115
+ *,
116
+ strict: bool = True,
117
+ ) -> T | None:
118
+ """
119
+ Extract JSON from LLM response and decode to a typed struct.
120
+
121
+ Uses msgspec for efficient, type-safe JSON decoding with clear error messages.
122
+ Combines extract_json_from_text() with msgspec.json.decode().
123
+
124
+ Args:
125
+ text: Raw LLM response text (may contain markdown code blocks, preamble, etc.)
126
+ response_type: The msgspec.Struct or other type to decode to
127
+ strict: If True (default), type mismatches raise errors.
128
+ If False, allows coercion (e.g., "5" -> 5 for int fields).
129
+ Configure via llm_providers.json_strict in config.yaml,
130
+ or override per-workflow with llm_json_strict variable.
131
+
132
+ Returns:
133
+ Decoded response of type T, or None if extraction/decoding fails.
134
+
135
+ Examples:
136
+ >>> class TaskResult(msgspec.Struct):
137
+ ... status: str
138
+ ... count: int
139
+ >>> result = decode_llm_response('{"status": "ok", "count": 5}', TaskResult)
140
+ >>> result.status
141
+ 'ok'
142
+
143
+ >>> # With strict=False, string "5" coerces to int 5
144
+ >>> result = decode_llm_response('{"status": "ok", "count": "5"}', TaskResult, strict=False)
145
+ >>> result.count
146
+ 5
147
+ """
148
+ json_str = extract_json_from_text(text)
149
+ if json_str is None:
150
+ logger.debug("No JSON found in LLM response")
151
+ return None
152
+
153
+ try:
154
+ # msgspec.json.decode returns Any at runtime when using TypeVar
155
+ return msgspec.json.decode(json_str.encode(), type=response_type, strict=strict)
156
+ except msgspec.ValidationError as e:
157
+ logger.warning(f"Invalid LLM response structure: {e}")
158
+ return None
159
+ except msgspec.DecodeError as e:
160
+ logger.warning(f"Failed to decode LLM response JSON: {e}")
161
+ return None