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,294 @@
1
+ """
2
+ Gemini CLI installation for Gobby hooks.
3
+
4
+ This module handles installing and uninstalling Gobby hooks
5
+ and workflows for Gemini CLI.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import time
11
+ from pathlib import Path
12
+ from shutil import copy2, which
13
+ from typing import Any
14
+
15
+ from gobby.cli.utils import get_install_dir
16
+
17
+ from .shared import (
18
+ configure_mcp_server_json,
19
+ install_cli_content,
20
+ install_shared_content,
21
+ install_shared_skills,
22
+ remove_mcp_server_json,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def install_gemini(project_path: Path) -> dict[str, Any]:
29
+ """Install Gobby integration for Gemini CLI (hooks, workflows).
30
+
31
+ Args:
32
+ project_path: Path to the project root
33
+
34
+ Returns:
35
+ Dict with installation results including success status and installed items
36
+ """
37
+ hooks_installed: list[str] = []
38
+ result: dict[str, Any] = {
39
+ "success": False,
40
+ "hooks_installed": hooks_installed,
41
+ "workflows_installed": [],
42
+ "commands_installed": [],
43
+ "mcp_configured": False,
44
+ "mcp_already_configured": False,
45
+ "error": None,
46
+ }
47
+
48
+ gemini_path = project_path / ".gemini"
49
+ settings_file = gemini_path / "settings.json"
50
+
51
+ # Ensure .gemini subdirectories exist
52
+ gemini_path.mkdir(parents=True, exist_ok=True)
53
+ hooks_dir = gemini_path / "hooks"
54
+ hooks_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ # Get source files
57
+ install_dir = get_install_dir()
58
+ gemini_install_dir = install_dir / "gemini"
59
+ install_hooks_dir = gemini_install_dir / "hooks"
60
+ source_hooks_template = gemini_install_dir / "hooks-template.json"
61
+
62
+ # Verify source files exist
63
+ dispatcher_file = install_hooks_dir / "hook_dispatcher.py"
64
+ if not dispatcher_file.exists():
65
+ result["error"] = f"Missing hook dispatcher: {dispatcher_file}"
66
+ return result
67
+
68
+ if not source_hooks_template.exists():
69
+ result["error"] = f"Missing hooks template: {source_hooks_template}"
70
+ return result
71
+
72
+ # Copy hook dispatcher
73
+ target_dispatcher = hooks_dir / "hook_dispatcher.py"
74
+ if target_dispatcher.exists():
75
+ target_dispatcher.unlink()
76
+ copy2(dispatcher_file, target_dispatcher)
77
+ target_dispatcher.chmod(0o755)
78
+
79
+ # Install shared content (workflows)
80
+ shared = install_shared_content(gemini_path, project_path)
81
+ # Install CLI-specific content (can override shared)
82
+ cli = install_cli_content("gemini", gemini_path)
83
+
84
+ # Install shared skills (SKILL.md)
85
+ try:
86
+ skills = install_shared_skills(gemini_path / "skills")
87
+ result["commands_installed"].extend([f"{s} (skill)" for s in skills])
88
+ except Exception as e:
89
+ logger.error(f"Failed to install shared skills: {e}")
90
+ # Proceeding despite skill install failure
91
+
92
+ result["workflows_installed"] = shared["workflows"] + cli["workflows"]
93
+ result["commands_installed"] = cli.get("commands", [])
94
+ result["plugins_installed"] = shared.get("plugins", [])
95
+
96
+ # Backup existing settings.json if it exists
97
+ if settings_file.exists():
98
+ timestamp = int(time.time())
99
+ backup_file = gemini_path / f"settings.json.{timestamp}.backup"
100
+ copy2(settings_file, backup_file)
101
+
102
+ # Load existing settings or create empty
103
+ if settings_file.exists():
104
+ try:
105
+ with open(settings_file) as f:
106
+ existing_settings = json.load(f)
107
+ except json.JSONDecodeError:
108
+ # If invalid JSON, treat as empty but warn (backup already made)
109
+ existing_settings = {}
110
+ else:
111
+ existing_settings = {}
112
+
113
+ # Load Gobby hooks from template
114
+ with open(source_hooks_template) as f:
115
+ gobby_settings_str = f.read()
116
+
117
+ # Resolve uv path dynamically to avoid PATH issues in Gemini CLI
118
+ uv_path = which("uv")
119
+ if not uv_path:
120
+ uv_path = "uv" # Fallback
121
+
122
+ # Replace $PROJECT_PATH with absolute project path
123
+ abs_project_path = str(project_path.resolve())
124
+
125
+ # Replace variables in template
126
+ gobby_settings_str = gobby_settings_str.replace("$PROJECT_PATH", abs_project_path)
127
+
128
+ # Also replace "uv run python" with absolute path if found
129
+ # The template uses "uv run python" by default
130
+ if uv_path != "uv":
131
+ gobby_settings_str = gobby_settings_str.replace("uv run python", f"{uv_path} run python")
132
+
133
+ gobby_settings = json.loads(gobby_settings_str)
134
+
135
+ # Ensure hooks section exists
136
+ if "hooks" not in existing_settings:
137
+ existing_settings["hooks"] = {}
138
+
139
+ # Merge Gobby hooks (preserving any existing hooks)
140
+ gobby_hooks = gobby_settings.get("hooks", {})
141
+ for hook_type, hook_config in gobby_hooks.items():
142
+ existing_settings["hooks"][hook_type] = hook_config
143
+ hooks_installed.append(hook_type)
144
+
145
+ # Crucially, ensure hooks are enabled in Gemini CLI
146
+ if "general" not in existing_settings:
147
+ existing_settings["general"] = {}
148
+ existing_settings["general"]["enableHooks"] = True
149
+
150
+ # Write merged settings back
151
+ with open(settings_file, "w") as f:
152
+ json.dump(existing_settings, f, indent=2)
153
+
154
+ # Configure MCP server in global settings (~/.gemini/settings.json)
155
+ global_settings = Path.home() / ".gemini" / "settings.json"
156
+ mcp_result = configure_mcp_server_json(global_settings)
157
+ if mcp_result["success"]:
158
+ result["mcp_configured"] = mcp_result.get("added", False)
159
+ result["mcp_already_configured"] = mcp_result.get("already_configured", False)
160
+ else:
161
+ # MCP config failure is non-fatal, just log it
162
+ logger.warning(f"Failed to configure MCP server: {mcp_result['error']}")
163
+
164
+ # Install agent scripts (used by meeseeks workflow)
165
+ scripts_installed = _install_agent_scripts(install_dir)
166
+ result["scripts_installed"] = scripts_installed
167
+
168
+ result["success"] = True
169
+ return result
170
+
171
+
172
+ def _install_agent_scripts(install_dir: Path) -> list[str]:
173
+ """Install shared agent scripts to ~/.gobby/scripts/.
174
+
175
+ Installs scripts like agent_shutdown.sh used by workflows.
176
+
177
+ Args:
178
+ install_dir: Path to the install source directory
179
+
180
+ Returns:
181
+ List of installed script names
182
+ """
183
+ scripts_installed: list[str] = []
184
+ source_scripts_dir = install_dir / "shared" / "scripts"
185
+ target_scripts_dir = Path.home() / ".gobby" / "scripts"
186
+
187
+ if not source_scripts_dir.exists():
188
+ logger.debug(f"No scripts directory found at {source_scripts_dir}")
189
+ return scripts_installed
190
+
191
+ # Ensure target directory exists
192
+ target_scripts_dir.mkdir(parents=True, exist_ok=True)
193
+
194
+ # Copy all scripts
195
+ for script_file in source_scripts_dir.glob("*.sh"):
196
+ target_file = target_scripts_dir / script_file.name
197
+ copy2(script_file, target_file)
198
+ # Make executable
199
+ target_file.chmod(0o755)
200
+ scripts_installed.append(script_file.name)
201
+ logger.debug(f"Installed script: {script_file.name}")
202
+
203
+ return scripts_installed
204
+
205
+
206
+ def uninstall_gemini(project_path: Path) -> dict[str, Any]:
207
+ """Uninstall Gobby integration from Gemini CLI.
208
+
209
+ Args:
210
+ project_path: Path to the project root
211
+
212
+ Returns:
213
+ Dict with uninstallation results including success status and removed items
214
+ """
215
+ hooks_removed: list[str] = []
216
+ files_removed: list[str] = []
217
+ result: dict[str, Any] = {
218
+ "success": False,
219
+ "hooks_removed": hooks_removed,
220
+ "files_removed": files_removed,
221
+ "mcp_removed": False,
222
+ "error": None,
223
+ }
224
+
225
+ gemini_path = project_path / ".gemini"
226
+ settings_file = gemini_path / "settings.json"
227
+ hooks_dir = gemini_path / "hooks"
228
+
229
+ if not settings_file.exists():
230
+ # No settings file means nothing to uninstall
231
+ result["success"] = True
232
+ return result
233
+
234
+ # Backup settings.json
235
+ timestamp = int(time.time())
236
+ backup_file = gemini_path / f"settings.json.{timestamp}.backup"
237
+ copy2(settings_file, backup_file)
238
+
239
+ # Remove hooks from settings.json
240
+ with open(settings_file) as f:
241
+ settings = json.load(f)
242
+
243
+ if "hooks" in settings:
244
+ hook_types = [
245
+ "SessionStart",
246
+ "SessionEnd",
247
+ "BeforeAgent",
248
+ "AfterAgent",
249
+ "BeforeTool",
250
+ "AfterTool",
251
+ "BeforeToolSelection",
252
+ "BeforeModel",
253
+ "AfterModel",
254
+ "PreCompress",
255
+ "Notification",
256
+ ]
257
+
258
+ for hook_type in hook_types:
259
+ if hook_type in settings["hooks"]:
260
+ del settings["hooks"][hook_type]
261
+ hooks_removed.append(hook_type)
262
+
263
+ # Also remove the "general" section if "enableHooks" was the only entry
264
+ if "general" in settings and settings["general"].get("enableHooks") is True:
265
+ # Check if there are other entries in "general"
266
+ if len(settings["general"]) == 1:
267
+ del settings["general"]
268
+ else:
269
+ del settings["general"]["enableHooks"]
270
+
271
+ with open(settings_file, "w") as f:
272
+ json.dump(settings, f, indent=2)
273
+
274
+ # Remove hook dispatcher
275
+ dispatcher_file = hooks_dir / "hook_dispatcher.py"
276
+ if dispatcher_file.exists():
277
+ dispatcher_file.unlink()
278
+ files_removed.append("hook_dispatcher.py")
279
+
280
+ # Attempt to remove empty hooks directory
281
+ try:
282
+ if hooks_dir.exists() and not any(hooks_dir.iterdir()):
283
+ hooks_dir.rmdir()
284
+ except Exception:
285
+ pass # nosec B110 - best-effort cleanup
286
+
287
+ # Remove MCP server from global settings (~/.gemini/settings.json)
288
+ global_settings = Path.home() / ".gemini" / "settings.json"
289
+ mcp_result = remove_mcp_server_json(global_settings)
290
+ if mcp_result["success"]:
291
+ result["mcp_removed"] = mcp_result.get("removed", False)
292
+
293
+ result["success"] = True
294
+ return result
@@ -0,0 +1,377 @@
1
+ """
2
+ Git hooks installation for Gobby task sync.
3
+
4
+ This module handles installing git hooks for automatic task
5
+ synchronization on commit, merge, and checkout operations.
6
+
7
+ Features:
8
+ - Backs up existing hooks before modification
9
+ - Chains with existing hooks (doesn't overwrite)
10
+ - Integrates with pre-commit framework when available
11
+ - Supports clean uninstallation
12
+ """
13
+
14
+ import logging
15
+ import shutil
16
+ import stat
17
+ import time
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Markers for identifying Gobby hook sections
24
+ GOBBY_HOOK_START = "# >>> GOBBY HOOK START >>>"
25
+ GOBBY_HOOK_END = "# <<< GOBBY HOOK END <<<"
26
+
27
+ # Hook script templates - these get wrapped with markers
28
+ HOOK_TEMPLATES = {
29
+ "pre-commit": """
30
+ # Gobby smart pre-commit wrapper
31
+ # - Runs gobby verification commands (if configured)
32
+ # - Runs pre-commit framework if available
33
+ # - Auto-commits formatting fixes separately
34
+ # - Syncs tasks before commit
35
+
36
+ # Run Gobby verification commands for pre-commit stage
37
+ if command -v gobby >/dev/null 2>&1; then
38
+ gobby hooks run pre-commit 2>/dev/null
39
+ GOBBY_EXIT=$?
40
+ if [ $GOBBY_EXIT -ne 0 ]; then
41
+ echo "Gobby pre-commit verification failed"
42
+ exit $GOBBY_EXIT
43
+ fi
44
+ fi
45
+
46
+ # Record which files have unstaged changes before pre-commit runs
47
+ UNSTAGED_BEFORE=$(git diff --name-only 2>/dev/null | sort)
48
+
49
+ # Run pre-commit if available and config exists
50
+ if command -v pre-commit >/dev/null 2>&1 && [ -f .pre-commit-config.yaml ]; then
51
+ pre-commit run --hook-stage pre-commit
52
+ PRECOMMIT_EXIT=$?
53
+
54
+ if [ $PRECOMMIT_EXIT -ne 0 ]; then
55
+ # Check if files were auto-fixed (new unstaged changes appeared)
56
+ UNSTAGED_AFTER=$(git diff --name-only 2>/dev/null | sort)
57
+
58
+ if [ "$UNSTAGED_BEFORE" != "$UNSTAGED_AFTER" ]; then
59
+ # Find files that were auto-fixed (newly unstaged)
60
+ AUTO_FIXED=$(comm -13 <(echo "$UNSTAGED_BEFORE") <(echo "$UNSTAGED_AFTER") 2>/dev/null)
61
+
62
+ if [ -n "$AUTO_FIXED" ]; then
63
+ echo ""
64
+ echo "Pre-commit auto-fixed files. Creating separate commit..."
65
+
66
+ # Stage only the auto-fixed files (handle filenames with spaces/special chars)
67
+ echo "$AUTO_FIXED" | while IFS= read -r file; do
68
+ [ -n "$file" ] && git add -- "$file"
69
+ done
70
+
71
+ # Commit them with --no-verify to skip hooks
72
+ git commit --no-verify -m "style: auto-format (pre-commit)" >/dev/null
73
+
74
+ echo "Auto-format committed. Please run 'git commit' again for your changes."
75
+ exit 1
76
+ fi
77
+ fi
78
+
79
+ # Pre-commit failed for other reasons
80
+ exit $PRECOMMIT_EXIT
81
+ fi
82
+ fi
83
+
84
+ # Gobby task sync - export tasks before commit
85
+ if command -v gobby >/dev/null 2>&1; then
86
+ gobby tasks sync --export --quiet 2>/dev/null || true
87
+ fi
88
+ """,
89
+ "pre-push": """
90
+ # Gobby verification runner for pre-push
91
+ # Runs configured verification commands (type_check, unit_tests, security, etc.)
92
+ if command -v gobby >/dev/null 2>&1; then
93
+ gobby hooks run pre-push 2>/dev/null
94
+ GOBBY_EXIT=$?
95
+ if [ $GOBBY_EXIT -ne 0 ]; then
96
+ echo "Gobby pre-push verification failed"
97
+ exit $GOBBY_EXIT
98
+ fi
99
+ fi
100
+ """,
101
+ "pre-merge-commit": """
102
+ # Gobby verification runner for pre-merge-commit
103
+ # Runs configured verification commands (code_review, integration tests, etc.)
104
+ if command -v gobby >/dev/null 2>&1; then
105
+ gobby hooks run pre-merge-commit 2>/dev/null
106
+ GOBBY_EXIT=$?
107
+ if [ $GOBBY_EXIT -ne 0 ]; then
108
+ echo "Gobby pre-merge-commit verification failed"
109
+ exit $GOBBY_EXIT
110
+ fi
111
+ fi
112
+ """,
113
+ "post-merge": """
114
+ # Gobby task sync - import tasks after merge/pull
115
+ if command -v gobby >/dev/null 2>&1; then
116
+ gobby tasks sync --import --quiet 2>/dev/null || true
117
+ fi
118
+ """,
119
+ "post-checkout": """
120
+ # Gobby task sync - import tasks on branch switch
121
+ # $3 is 1 if this was a branch checkout (vs file checkout)
122
+ if [ "$3" = "1" ]; then
123
+ if command -v gobby >/dev/null 2>&1; then
124
+ gobby tasks sync --import --quiet 2>/dev/null || true
125
+ fi
126
+ fi
127
+ """,
128
+ }
129
+
130
+
131
+ def _backup_hook(hook_path: Path, hooks_dir: Path) -> str | None:
132
+ """Create a timestamped backup of an existing hook.
133
+
134
+ Args:
135
+ hook_path: Path to the hook file
136
+ hooks_dir: Directory containing hooks
137
+
138
+ Returns:
139
+ Backup path if created, None otherwise
140
+ """
141
+ if not hook_path.exists():
142
+ return None
143
+
144
+ timestamp = int(time.time())
145
+ backup_path = hooks_dir / f"{hook_path.name}.{timestamp}.backup"
146
+
147
+ try:
148
+ shutil.copy2(hook_path, backup_path)
149
+ logger.debug(f"Backed up {hook_path.name} to {backup_path.name}")
150
+ return str(backup_path)
151
+ except OSError as e:
152
+ logger.warning(f"Failed to backup {hook_path.name}: {e}")
153
+ return None
154
+
155
+
156
+ def _has_gobby_hook(content: str) -> bool:
157
+ """Check if content already contains Gobby hook markers."""
158
+ return GOBBY_HOOK_START in content
159
+
160
+
161
+ def _is_precommit_framework_hook(content: str) -> bool:
162
+ """Check if this is a hook generated by the pre-commit framework."""
163
+ return "File generated by pre-commit" in content or "pre_commit" in content
164
+
165
+
166
+ def _wrap_gobby_section(script: str) -> str:
167
+ """Wrap a script section with Gobby markers."""
168
+ return f"{GOBBY_HOOK_START}\n{script.strip()}\n{GOBBY_HOOK_END}\n"
169
+
170
+
171
+ def _remove_gobby_section(content: str) -> str:
172
+ """Remove Gobby hook section from content."""
173
+ lines = content.split("\n")
174
+ result = []
175
+ in_gobby_section = False
176
+
177
+ for line in lines:
178
+ if GOBBY_HOOK_START in line:
179
+ in_gobby_section = True
180
+ continue
181
+ if GOBBY_HOOK_END in line:
182
+ in_gobby_section = False
183
+ continue
184
+ if not in_gobby_section:
185
+ result.append(line)
186
+
187
+ # Clean up multiple blank lines
188
+ cleaned = "\n".join(result)
189
+ while "\n\n\n" in cleaned:
190
+ cleaned = cleaned.replace("\n\n\n", "\n\n")
191
+
192
+ return cleaned.strip() + "\n" if cleaned.strip() else ""
193
+
194
+
195
+ def _check_precommit_installed() -> bool:
196
+ """Check if pre-commit framework is installed and configured."""
197
+ return shutil.which("pre-commit") is not None
198
+
199
+
200
+ def _has_precommit_config(project_path: Path) -> bool:
201
+ """Check if project has a .pre-commit-config.yaml."""
202
+ return (project_path / ".pre-commit-config.yaml").exists()
203
+
204
+
205
+ def install_git_hooks(
206
+ project_path: Path,
207
+ *,
208
+ force: bool = False,
209
+ setup_precommit: bool = True,
210
+ ) -> dict[str, Any]:
211
+ """Install Gobby git hooks to the current repository.
212
+
213
+ Safely installs hooks by:
214
+ 1. Backing up existing hooks
215
+ 2. Chaining with existing hooks (appending Gobby section)
216
+ 3. Optionally setting up pre-commit framework
217
+
218
+ Args:
219
+ project_path: Path to the project root
220
+ force: If True, reinstall even if already present
221
+ setup_precommit: If True, run `pre-commit install` if config exists
222
+
223
+ Returns:
224
+ Dict with installation results including:
225
+ - success: bool
226
+ - installed: list of installed hook names
227
+ - skipped: list of skipped hooks with reasons
228
+ - backups: list of backup file paths
229
+ - precommit_installed: bool if pre-commit was set up
230
+ - error: error message if failed
231
+ """
232
+ result: dict[str, Any] = {
233
+ "success": False,
234
+ "installed": [],
235
+ "skipped": [],
236
+ "backups": [],
237
+ "precommit_installed": False,
238
+ "error": None,
239
+ }
240
+
241
+ git_dir = project_path / ".git"
242
+ if not git_dir.exists():
243
+ result["error"] = "Not a git repository (no .git directory found)"
244
+ return result
245
+
246
+ hooks_dir = git_dir / "hooks"
247
+ hooks_dir.mkdir(parents=True, exist_ok=True)
248
+
249
+ # Install each hook
250
+ for hook_name, gobby_script in HOOK_TEMPLATES.items():
251
+ hook_path = hooks_dir / hook_name
252
+ gobby_section = _wrap_gobby_section(gobby_script)
253
+
254
+ if hook_path.exists():
255
+ content = hook_path.read_text()
256
+
257
+ # Check if already installed
258
+ if _has_gobby_hook(content) and not force:
259
+ result["skipped"].append(f"{hook_name} (already installed)")
260
+ continue
261
+
262
+ # Backup existing hook
263
+ backup_path = _backup_hook(hook_path, hooks_dir)
264
+ if backup_path:
265
+ result["backups"].append(backup_path)
266
+
267
+ # If this is a pre-commit framework hook for pre-commit stage,
268
+ # replace it entirely with our wrapper (which calls pre-commit)
269
+ if hook_name == "pre-commit" and _is_precommit_framework_hook(content):
270
+ new_content = f"#!/usr/bin/env bash\n\n{gobby_section}"
271
+ hook_path.write_text(new_content)
272
+ logger.info("Replaced pre-commit framework hook with Gobby wrapper")
273
+ else:
274
+ # Remove old Gobby section if force reinstalling
275
+ if force and GOBBY_HOOK_START in content:
276
+ content = _remove_gobby_section(content)
277
+
278
+ # Append Gobby section to existing hook
279
+ if content.strip():
280
+ # Ensure shebang is preserved at top
281
+ if content.startswith("#!"):
282
+ lines = content.split("\n", 1)
283
+ shebang = lines[0]
284
+ rest = lines[1] if len(lines) > 1 else ""
285
+ new_content = f"{shebang}\n\n{gobby_section}\n{rest.strip()}\n"
286
+ else:
287
+ new_content = f"#!/usr/bin/env bash\n\n{gobby_section}\n{content}"
288
+ else:
289
+ new_content = f"#!/usr/bin/env bash\n\n{gobby_section}"
290
+
291
+ hook_path.write_text(new_content)
292
+ logger.info(f"Appended Gobby hook to existing {hook_name}")
293
+
294
+ else:
295
+ # Create new hook (use bash for pre-commit process substitution)
296
+ new_content = f"#!/usr/bin/env bash\n\n{gobby_section}"
297
+ hook_path.write_text(new_content)
298
+ logger.info(f"Created new {hook_name} hook")
299
+
300
+ # Ensure executable
301
+ hook_path.chmod(hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
302
+ result["installed"].append(hook_name)
303
+
304
+ # Note: We intentionally DON'T run `pre-commit install` here.
305
+ # Our smart pre-commit hook wrapper calls `pre-commit run` directly,
306
+ # which allows us to handle auto-fixes by creating separate commits.
307
+ # Running `pre-commit install` would overwrite our wrapper.
308
+ #
309
+ # We also don't run `pre-commit install --hook-type pre-push` because
310
+ # our pre-push hook now runs gobby verification commands first, and
311
+ # the pre-commit framework's hook would overwrite ours.
312
+ if setup_precommit and _has_precommit_config(project_path) and _check_precommit_installed():
313
+ result["precommit_installed"] = True
314
+ logger.info(
315
+ "Pre-commit detected - gobby hooks will run verification first, then pre-commit framework"
316
+ )
317
+
318
+ result["success"] = True
319
+ return result
320
+
321
+
322
+ def uninstall_git_hooks(project_path: Path) -> dict[str, Any]:
323
+ """Remove Gobby sections from git hooks.
324
+
325
+ Safely removes only Gobby-added sections, preserving other hook functionality.
326
+
327
+ Args:
328
+ project_path: Path to the project root
329
+
330
+ Returns:
331
+ Dict with uninstallation results
332
+ """
333
+ result: dict[str, Any] = {
334
+ "success": False,
335
+ "removed": [],
336
+ "not_found": [],
337
+ "error": None,
338
+ }
339
+
340
+ git_dir = project_path / ".git"
341
+ if not git_dir.exists():
342
+ result["error"] = "Not a git repository"
343
+ return result
344
+
345
+ hooks_dir = git_dir / "hooks"
346
+ if not hooks_dir.exists():
347
+ result["success"] = True
348
+ return result
349
+
350
+ for hook_name in HOOK_TEMPLATES:
351
+ hook_path = hooks_dir / hook_name
352
+
353
+ if not hook_path.exists():
354
+ result["not_found"].append(hook_name)
355
+ continue
356
+
357
+ content = hook_path.read_text()
358
+
359
+ if not _has_gobby_hook(content):
360
+ result["not_found"].append(hook_name)
361
+ continue
362
+
363
+ # Remove Gobby section
364
+ new_content = _remove_gobby_section(content)
365
+
366
+ if new_content.strip():
367
+ # Hook still has content, keep it
368
+ hook_path.write_text(new_content)
369
+ else:
370
+ # Hook is now empty, remove it
371
+ hook_path.unlink()
372
+
373
+ result["removed"].append(hook_name)
374
+ logger.info(f"Removed Gobby section from {hook_name}")
375
+
376
+ result["success"] = True
377
+ return result