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,65 @@
1
+ """
2
+ Antigravity agent installation for Gobby MCP.
3
+
4
+ This module handles installing Gobby MCP server configuration
5
+ for the Antigravity agent (internal tool).
6
+
7
+ Note: Antigravity does not currently support hooks, so only MCP
8
+ configuration is installed.
9
+ """
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .shared import configure_mcp_server_json, install_shared_skills
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def install_antigravity(project_path: Path) -> dict[str, Any]:
21
+ """Install Gobby integration for Antigravity agent (MCP only).
22
+
23
+ Antigravity does not support hooks, so this only configures
24
+ the MCP server in ~/.gemini/antigravity/mcp_config.json.
25
+
26
+ Args:
27
+ project_path: Path to the project root (unused, kept for API compatibility)
28
+
29
+ Returns:
30
+ Dict with installation results including success status
31
+ """
32
+ result: dict[str, Any] = {
33
+ "success": False,
34
+ "hooks_installed": [],
35
+ "workflows_installed": [],
36
+ "commands_installed": [],
37
+ "skills_installed": [],
38
+ "mcp_configured": False,
39
+ "mcp_already_configured": False,
40
+ "error": None,
41
+ }
42
+
43
+ # Configure MCP server in Antigravity's MCP config (~/.gemini/antigravity/mcp_config.json)
44
+ mcp_config = Path.home() / ".gemini" / "antigravity" / "mcp_config.json"
45
+
46
+ # Install shared skills to ~/.antigravity/skills/ (Standard Antigravity location)
47
+ try:
48
+ skills_path = Path.home() / ".antigravity" / "skills"
49
+ skills = install_shared_skills(skills_path)
50
+ result["commands_installed"].extend([f"{s} (skill)" for s in skills])
51
+ except Exception as e:
52
+ logger.error(f"Failed to install shared skills: {e}")
53
+ # Proceeding despite skill install failure
54
+
55
+ mcp_result = configure_mcp_server_json(mcp_config)
56
+
57
+ if mcp_result["success"]:
58
+ result["mcp_configured"] = mcp_result.get("added", False)
59
+ result["mcp_already_configured"] = mcp_result.get("already_configured", False)
60
+ result["success"] = True
61
+ else:
62
+ result["error"] = mcp_result.get("error", "Unknown error configuring MCP")
63
+ logger.error(f"Failed to configure MCP server: {result['error']}")
64
+
65
+ return result
@@ -0,0 +1,363 @@
1
+ """
2
+ Claude Code installation for Gobby hooks.
3
+
4
+ This module handles installing and uninstalling Gobby hooks
5
+ and workflows for Claude Code CLI.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import tempfile
12
+ import time
13
+ from pathlib import Path
14
+ from shutil import copy2
15
+ from typing import Any
16
+
17
+ from gobby.cli.utils import get_install_dir
18
+
19
+ from .shared import (
20
+ configure_mcp_server_json,
21
+ install_cli_content,
22
+ install_shared_content,
23
+ install_shared_skills,
24
+ remove_mcp_server_json,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def install_claude(project_path: Path) -> dict[str, Any]:
31
+ """Install Gobby integration for Claude Code (hooks, workflows).
32
+
33
+ Args:
34
+ project_path: Path to the project root
35
+
36
+ Returns:
37
+ Dict with installation results including success status and installed items
38
+ """
39
+ hooks_installed: list[str] = []
40
+ result: dict[str, Any] = {
41
+ "success": False,
42
+ "hooks_installed": hooks_installed,
43
+ "workflows_installed": [],
44
+ "commands_installed": [],
45
+ "mcp_configured": False,
46
+ "mcp_already_configured": False,
47
+ "error": None,
48
+ }
49
+
50
+ claude_path = project_path / ".claude"
51
+ settings_file = claude_path / "settings.json"
52
+
53
+ # Ensure .claude subdirectories exist
54
+ claude_path.mkdir(parents=True, exist_ok=True)
55
+ hooks_dir = claude_path / "hooks"
56
+ hooks_dir.mkdir(parents=True, exist_ok=True)
57
+
58
+ # Get source files
59
+ install_dir = get_install_dir()
60
+ claude_install_dir = install_dir / "claude"
61
+ install_hooks_dir = claude_install_dir / "hooks"
62
+
63
+ # Hook files to copy
64
+ hook_files = {
65
+ "hook_dispatcher.py": True, # Make executable
66
+ "validate_settings.py": True, # Make executable
67
+ }
68
+
69
+ source_hooks_template = claude_install_dir / "hooks-template.json"
70
+
71
+ # Verify all source files exist
72
+ missing_files = []
73
+ for filename in hook_files.keys():
74
+ source_file = install_hooks_dir / filename
75
+ if not source_file.exists():
76
+ missing_files.append(str(source_file))
77
+
78
+ if not source_hooks_template.exists():
79
+ missing_files.append(str(source_hooks_template))
80
+
81
+ if missing_files:
82
+ result["error"] = f"Missing source files: {missing_files}"
83
+ return result
84
+
85
+ # Copy hook files
86
+ try:
87
+ for filename, make_executable in hook_files.items():
88
+ source_file = install_hooks_dir / filename
89
+ target_file = hooks_dir / filename
90
+
91
+ if target_file.exists():
92
+ target_file.unlink()
93
+
94
+ copy2(source_file, target_file)
95
+ if make_executable:
96
+ target_file.chmod(0o755)
97
+ except OSError as e:
98
+ logger.error(f"Failed to copy hook files: {e}")
99
+ result["error"] = f"Failed to copy hook files: {e}"
100
+ return result
101
+
102
+ # Install shared content (workflows)
103
+ try:
104
+ shared = install_shared_content(claude_path, project_path)
105
+ except Exception as e:
106
+ logger.error(f"Failed to install shared content: {e}")
107
+ result["error"] = f"Failed to install shared content: {e}"
108
+ return result
109
+
110
+ # Install CLI-specific content (can override shared)
111
+ try:
112
+ cli = install_cli_content("claude", claude_path)
113
+ except Exception as e:
114
+ logger.error(f"Failed to install CLI content: {e}")
115
+ result["error"] = f"Failed to install CLI content: {e}"
116
+ return result
117
+
118
+ result["workflows_installed"] = shared["workflows"] + cli["workflows"]
119
+ result["commands_installed"] = cli.get("commands", [])
120
+ result["plugins_installed"] = shared.get("plugins", [])
121
+
122
+ # Install shared skills (SKILL.md)
123
+ try:
124
+ skills = install_shared_skills(claude_path / "skills")
125
+ result["commands_installed"].extend([f"{s} (skill)" for s in skills])
126
+ except Exception as e:
127
+ logger.error(f"Failed to install shared skills: {e}")
128
+ result["error"] = f"Failed to install shared skills: {e}"
129
+ # Proceeding despite skill install failure
130
+
131
+ # Backup existing settings.json if it exists
132
+ backup_file = None
133
+ if settings_file.exists():
134
+ timestamp = int(time.time())
135
+ backup_file = claude_path / f"settings.json.{timestamp}.backup"
136
+ try:
137
+ copy2(settings_file, backup_file)
138
+ except OSError as e:
139
+ logger.error(f"Failed to create backup of settings.json: {e}")
140
+ result["error"] = f"Failed to create backup: {e}"
141
+ return result
142
+
143
+ # Verify backup exists
144
+ if not backup_file.exists():
145
+ logger.error("Backup file was not created successfully")
146
+ result["error"] = "Backup file was not created successfully"
147
+ return result
148
+
149
+ # Load existing settings or create empty
150
+ existing_settings: dict[str, Any] = {}
151
+ if settings_file.exists():
152
+ try:
153
+ with open(settings_file) as f:
154
+ existing_settings = json.load(f)
155
+ except json.JSONDecodeError as e:
156
+ logger.error(f"Failed to parse settings.json: {e}")
157
+ result["error"] = f"Failed to parse settings.json: {e}"
158
+ return result
159
+ except OSError as e:
160
+ logger.error(f"Failed to read settings.json: {e}")
161
+ result["error"] = f"Failed to read settings.json: {e}"
162
+ return result
163
+
164
+ # Load Gobby hooks from template
165
+ try:
166
+ with open(source_hooks_template) as f:
167
+ gobby_settings_str = f.read()
168
+ except OSError as e:
169
+ logger.error(f"Failed to read hooks template: {e}")
170
+ result["error"] = f"Failed to read hooks template: {e}"
171
+ return result
172
+
173
+ # Replace $PROJECT_PATH with absolute project path
174
+ abs_project_path = str(project_path.resolve())
175
+ gobby_settings_str = gobby_settings_str.replace("$PROJECT_PATH", abs_project_path)
176
+
177
+ try:
178
+ gobby_settings = json.loads(gobby_settings_str)
179
+ except json.JSONDecodeError as e:
180
+ logger.error(f"Failed to parse hooks template: {e}")
181
+ result["error"] = f"Failed to parse hooks template: {e}"
182
+ return result
183
+
184
+ # Ensure hooks section exists
185
+ if "hooks" not in existing_settings:
186
+ existing_settings["hooks"] = {}
187
+
188
+ # Merge Gobby hooks
189
+ gobby_hooks = gobby_settings.get("hooks", {})
190
+ for hook_type, hook_config in gobby_hooks.items():
191
+ existing_settings["hooks"][hook_type] = hook_config
192
+ hooks_installed.append(hook_type)
193
+
194
+ # Write merged settings back using atomic write
195
+ try:
196
+ fd, temp_path = tempfile.mkstemp(dir=str(claude_path), suffix=".tmp", prefix="settings_")
197
+ try:
198
+ with os.fdopen(fd, "w") as f:
199
+ json.dump(existing_settings, f, indent=2)
200
+ f.flush()
201
+ os.fsync(f.fileno())
202
+ # Atomic replace
203
+ os.replace(temp_path, settings_file)
204
+ except Exception:
205
+ # Clean up temp file if it still exists
206
+ if os.path.exists(temp_path):
207
+ os.unlink(temp_path)
208
+ raise
209
+ except OSError as e:
210
+ logger.error(f"Failed to write settings.json: {e}")
211
+ # Attempt to restore from backup if we have one
212
+ if backup_file and backup_file.exists():
213
+ try:
214
+ copy2(backup_file, settings_file)
215
+ logger.info("Restored settings.json from backup after write failure")
216
+ except OSError as restore_error:
217
+ logger.error(f"Failed to restore from backup: {restore_error}")
218
+ result["error"] = f"Failed to write settings.json: {e}"
219
+ return result
220
+
221
+ # Configure MCP server in global settings (~/.claude.json)
222
+ # Note: Claude Code uses ~/.claude.json for user-scoped MCP servers
223
+ global_settings = Path.home() / ".claude.json"
224
+ mcp_result = configure_mcp_server_json(global_settings)
225
+ if mcp_result["success"]:
226
+ result["mcp_configured"] = mcp_result.get("added", False)
227
+ result["mcp_already_configured"] = mcp_result.get("already_configured", False)
228
+ else:
229
+ # MCP config failure is non-fatal, just log it
230
+ logger.warning(f"Failed to configure MCP server: {mcp_result['error']}")
231
+
232
+ result["success"] = True
233
+ return result
234
+
235
+
236
+ def uninstall_claude(project_path: Path) -> dict[str, Any]:
237
+ """Uninstall Gobby integration from Claude Code.
238
+
239
+ Args:
240
+ project_path: Path to the project root
241
+
242
+ Returns:
243
+ Dict with uninstallation results including success status and removed items
244
+ """
245
+ hooks_removed: list[str] = []
246
+ files_removed: list[str] = []
247
+
248
+ result: dict[str, Any] = {
249
+ "success": False,
250
+ "hooks_removed": hooks_removed,
251
+ "files_removed": files_removed,
252
+ "mcp_removed": False,
253
+ "error": None,
254
+ }
255
+
256
+ claude_path = project_path / ".claude"
257
+ settings_file = claude_path / "settings.json"
258
+ hooks_dir = claude_path / "hooks"
259
+
260
+ if not settings_file.exists():
261
+ result["error"] = f"Settings file not found: {settings_file}"
262
+ return result
263
+
264
+ # Backup settings.json with verification
265
+ timestamp = int(time.time())
266
+ backup_file = claude_path / f"settings.json.{timestamp}.backup"
267
+ try:
268
+ copy2(settings_file, backup_file)
269
+ except OSError as e:
270
+ logger.error(f"Failed to create backup of settings.json: {e}")
271
+ result["error"] = f"Failed to create backup: {e}"
272
+ return result
273
+
274
+ # Verify backup exists before proceeding
275
+ if not backup_file.exists():
276
+ logger.error("Backup file was not created successfully")
277
+ result["error"] = "Backup file was not created successfully"
278
+ return result
279
+
280
+ # Read and parse settings.json
281
+ try:
282
+ with open(settings_file) as f:
283
+ settings = json.load(f)
284
+ except json.JSONDecodeError as e:
285
+ logger.error(f"Failed to parse settings.json: {e}")
286
+ result["error"] = f"Failed to parse settings.json: {e}"
287
+ return result
288
+ except OSError as e:
289
+ logger.error(f"Failed to read settings.json: {e}")
290
+ result["error"] = f"Failed to read settings.json: {e}"
291
+ return result
292
+
293
+ if "hooks" in settings:
294
+ hook_types = [
295
+ "SessionStart",
296
+ "SessionEnd",
297
+ "UserPromptSubmit",
298
+ "PreToolUse",
299
+ "PostToolUse",
300
+ "PreCompact",
301
+ "Notification",
302
+ "Stop",
303
+ "SubagentStart",
304
+ "SubagentStop",
305
+ "PermissionRequest",
306
+ ]
307
+
308
+ for hook_type in hook_types:
309
+ if hook_type in settings["hooks"]:
310
+ del settings["hooks"][hook_type]
311
+ hooks_removed.append(hook_type)
312
+
313
+ # Write to temp file and atomically replace
314
+ try:
315
+ # Create temp file in same directory for atomic replace
316
+ fd, temp_path = tempfile.mkstemp(
317
+ dir=str(claude_path), suffix=".tmp", prefix="settings_"
318
+ )
319
+ try:
320
+ with os.fdopen(fd, "w") as f:
321
+ json.dump(settings, f, indent=2)
322
+ f.flush()
323
+ os.fsync(f.fileno())
324
+ # Atomic replace
325
+ os.replace(temp_path, settings_file)
326
+ except Exception:
327
+ # Clean up temp file if it still exists
328
+ if os.path.exists(temp_path):
329
+ os.unlink(temp_path)
330
+ raise
331
+ except OSError as e:
332
+ logger.error(f"Failed to write settings.json: {e}")
333
+ # Attempt to restore from backup
334
+ try:
335
+ copy2(backup_file, settings_file)
336
+ logger.info("Restored settings.json from backup after write failure")
337
+ except OSError as restore_error:
338
+ logger.error(f"Failed to restore from backup: {restore_error}")
339
+ result["error"] = f"Failed to write settings.json: {e}"
340
+ return result
341
+
342
+ # Remove hook files
343
+ hook_files = [
344
+ "hook_dispatcher.py",
345
+ "validate_settings.py",
346
+ "README.md",
347
+ "HOOK_SCHEMAS.md",
348
+ ]
349
+
350
+ for filename in hook_files:
351
+ file_path = hooks_dir / filename
352
+ if file_path.exists():
353
+ file_path.unlink()
354
+ files_removed.append(filename)
355
+
356
+ # Remove MCP server from global settings (~/.claude.json)
357
+ global_settings = Path.home() / ".claude.json"
358
+ mcp_result = remove_mcp_server_json(global_settings)
359
+ if mcp_result["success"]:
360
+ result["mcp_removed"] = mcp_result.get("removed", False)
361
+
362
+ result["success"] = True
363
+ return result
@@ -0,0 +1,192 @@
1
+ """
2
+ Codex CLI installation for Gobby hooks.
3
+
4
+ This module handles installing and uninstalling Gobby notify integration
5
+ for OpenAI Codex CLI.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import re
11
+ from pathlib import Path
12
+ from shutil import copy2
13
+ from typing import Any
14
+
15
+ from gobby.cli.utils import get_install_dir
16
+
17
+ from .shared import (
18
+ configure_mcp_server_toml,
19
+ install_cli_content,
20
+ install_shared_content,
21
+ install_shared_skills,
22
+ remove_mcp_server_toml,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def install_codex_notify() -> dict[str, Any]:
29
+ """Install Codex notify script and configure ~/.codex/config.toml.
30
+
31
+ Returns:
32
+ Dict with installation results including success status and installed items
33
+ """
34
+ files_installed: list[str] = []
35
+ result: dict[str, Any] = {
36
+ "success": False,
37
+ "files_installed": files_installed,
38
+ "workflows_installed": [],
39
+ "commands_installed": [],
40
+ "config_updated": False,
41
+ "mcp_configured": False,
42
+ "mcp_already_configured": False,
43
+ "error": None,
44
+ }
45
+
46
+ install_dir = get_install_dir()
47
+ source_notify = install_dir / "codex" / "hooks" / "hook_dispatcher.py"
48
+ if not source_notify.exists():
49
+ result["error"] = f"Missing source file: {source_notify}"
50
+ return result
51
+
52
+ # Install hook dispatcher to ~/.gobby/hooks/codex/hook_dispatcher.py
53
+ notify_dir = Path.home() / ".gobby" / "hooks" / "codex"
54
+ notify_dir.mkdir(parents=True, exist_ok=True)
55
+ target_notify = notify_dir / "hook_dispatcher.py"
56
+
57
+ if target_notify.exists():
58
+ target_notify.unlink()
59
+
60
+ copy2(source_notify, target_notify)
61
+ target_notify.chmod(0o755)
62
+ files_installed.append(str(target_notify))
63
+
64
+ # Install shared content - workflows to ~/.gobby
65
+ codex_home = Path.home() / ".codex"
66
+ gobby_home = Path.home() # workflows go to ~/.gobby/workflows/
67
+
68
+ shared = install_shared_content(codex_home, gobby_home)
69
+ # Install CLI-specific content (can override shared)
70
+ cli = install_cli_content("codex", codex_home)
71
+
72
+ # Install shared skills (SKILL.md)
73
+ try:
74
+ skills = install_shared_skills(codex_home / "skills")
75
+ result["commands_installed"].extend([f"{s} (skill)" for s in skills])
76
+ except Exception as e:
77
+ logger.error(f"Failed to install shared skills: {e}")
78
+ # Proceeding despite skill install failure
79
+
80
+ result["workflows_installed"] = shared["workflows"] + cli["workflows"]
81
+ result["commands_installed"] = cli.get("commands", [])
82
+ result["plugins_installed"] = shared.get("plugins", [])
83
+
84
+ # Update ~/.codex/config.toml
85
+ codex_config_dir = codex_home
86
+
87
+ codex_config_dir.mkdir(parents=True, exist_ok=True)
88
+ codex_config_path = codex_config_dir / "config.toml"
89
+
90
+ notify_command = ["python3", str(target_notify)]
91
+ notify_line = f"notify = {json.dumps(notify_command)}"
92
+
93
+ try:
94
+ if codex_config_path.exists():
95
+ existing = codex_config_path.read_text(encoding="utf-8")
96
+ else:
97
+ existing = ""
98
+
99
+ pattern = re.compile(r"(?m)^\s*notify\s*=.*$")
100
+ if pattern.search(existing):
101
+ updated = pattern.sub(notify_line, existing)
102
+ else:
103
+ updated = (existing.rstrip() + "\n\n" if existing.strip() else "") + notify_line + "\n"
104
+
105
+ if updated != existing:
106
+ if codex_config_path.exists():
107
+ backup_path = codex_config_path.with_suffix(".toml.bak")
108
+ backup_path.write_text(existing, encoding="utf-8")
109
+
110
+ codex_config_path.write_text(updated, encoding="utf-8")
111
+ result["config_updated"] = True
112
+
113
+ # Configure MCP server in global config (~/.codex/config.toml)
114
+ mcp_result = configure_mcp_server_toml(codex_config_path)
115
+ if mcp_result["success"]:
116
+ result["mcp_configured"] = mcp_result.get("added", False)
117
+ result["mcp_already_configured"] = mcp_result.get("already_configured", False)
118
+ else:
119
+ # MCP config failure is non-fatal, just log it
120
+ logger.warning(f"Failed to configure MCP server: {mcp_result['error']}")
121
+
122
+ result["success"] = True
123
+ return result
124
+
125
+ except Exception as e:
126
+ result["error"] = f"Failed to update Codex config: {e}"
127
+ return result
128
+
129
+
130
+ def uninstall_codex_notify() -> dict[str, Any]:
131
+ """Uninstall Codex notify script and remove from ~/.codex/config.toml.
132
+
133
+ Returns:
134
+ Dict with uninstallation results including success status and removed items
135
+ """
136
+ files_removed: list[str] = []
137
+ result: dict[str, Any] = {
138
+ "success": False,
139
+ "files_removed": files_removed,
140
+ "config_updated": False,
141
+ "mcp_removed": False,
142
+ "error": None,
143
+ }
144
+
145
+ # Remove hook dispatcher from ~/.gobby/hooks/codex/hook_dispatcher.py
146
+ notify_file = Path.home() / ".gobby" / "hooks" / "codex" / "hook_dispatcher.py"
147
+ if notify_file.exists():
148
+ notify_file.unlink()
149
+ files_removed.append(str(notify_file))
150
+
151
+ # Try to remove empty parent directories
152
+ notify_dir = notify_file.parent
153
+ try:
154
+ if notify_dir.exists() and not any(notify_dir.iterdir()):
155
+ notify_dir.rmdir()
156
+ except Exception:
157
+ pass # nosec B110 - best-effort cleanup, directory removal is non-critical
158
+
159
+ # Update ~/.codex/config.toml to remove notify line
160
+ codex_config_path = Path.home() / ".codex" / "config.toml"
161
+
162
+ try:
163
+ if codex_config_path.exists():
164
+ existing = codex_config_path.read_text(encoding="utf-8")
165
+
166
+ # Remove notify = [...] line
167
+ pattern = re.compile(r"(?m)^\s*notify\s*=.*$\n?")
168
+ if pattern.search(existing):
169
+ updated = pattern.sub("", existing)
170
+
171
+ # Clean up multiple blank lines
172
+ updated = re.sub(r"\n{3,}", "\n\n", updated)
173
+
174
+ if updated != existing:
175
+ # Backup before modifying
176
+ backup_path = codex_config_path.with_suffix(".toml.bak")
177
+ backup_path.write_text(existing, encoding="utf-8")
178
+
179
+ codex_config_path.write_text(updated, encoding="utf-8")
180
+ result["config_updated"] = True
181
+
182
+ # Remove MCP server from config
183
+ mcp_result = remove_mcp_server_toml(codex_config_path)
184
+ if mcp_result["success"]:
185
+ result["mcp_removed"] = mcp_result.get("removed", False)
186
+
187
+ result["success"] = True
188
+ return result
189
+
190
+ except Exception as e:
191
+ result["error"] = f"Failed to update Codex config: {e}"
192
+ return result