gobby 0.2.8__py3-none-any.whl → 0.2.11__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 (168) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +5 -28
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +64 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,242 @@
1
+ """
2
+ GitHub Copilot CLI installation for Gobby hooks.
3
+
4
+ This module handles installing and uninstalling Gobby hooks for Copilot CLI.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import tempfile
11
+ import time
12
+ from pathlib import Path
13
+ from shutil import copy2
14
+ from typing import Any
15
+
16
+ from gobby.cli.utils import get_install_dir
17
+
18
+ from .shared import install_shared_content
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def install_copilot(project_path: Path) -> dict[str, Any]:
24
+ """Install Gobby integration for Copilot CLI (hooks, workflows).
25
+
26
+ Args:
27
+ project_path: Path to the project root
28
+
29
+ Returns:
30
+ Dict with installation results including success status and installed items
31
+ """
32
+ hooks_installed: list[str] = []
33
+ result: dict[str, Any] = {
34
+ "success": False,
35
+ "hooks_installed": hooks_installed,
36
+ "workflows_installed": [],
37
+ "error": None,
38
+ }
39
+
40
+ copilot_path = project_path / ".copilot"
41
+ hooks_file = copilot_path / "hooks.json"
42
+
43
+ # Ensure .copilot subdirectories exist
44
+ copilot_path.mkdir(parents=True, exist_ok=True)
45
+ hooks_dir = copilot_path / "hooks"
46
+ hooks_dir.mkdir(parents=True, exist_ok=True)
47
+
48
+ # Get source files
49
+ install_dir = get_install_dir()
50
+ copilot_install_dir = install_dir / "copilot"
51
+ install_hooks_dir = copilot_install_dir / "hooks"
52
+
53
+ # Hook files to copy
54
+ hook_files = {
55
+ "hook_dispatcher.py": True, # Make executable
56
+ }
57
+
58
+ source_hooks_template = copilot_install_dir / "hooks-template.json"
59
+
60
+ # Verify all source files exist
61
+ missing_files = []
62
+ for filename in hook_files.keys():
63
+ source_file = install_hooks_dir / filename
64
+ if not source_file.exists():
65
+ missing_files.append(str(source_file))
66
+
67
+ if not source_hooks_template.exists():
68
+ missing_files.append(str(source_hooks_template))
69
+
70
+ if missing_files:
71
+ result["error"] = f"Missing source files: {missing_files}"
72
+ return result
73
+
74
+ # Copy hook files
75
+ try:
76
+ for filename, make_executable in hook_files.items():
77
+ source_file = install_hooks_dir / filename
78
+ target_file = hooks_dir / filename
79
+
80
+ if target_file.exists():
81
+ target_file.unlink()
82
+
83
+ copy2(source_file, target_file)
84
+ if make_executable:
85
+ target_file.chmod(0o755)
86
+ except OSError as e:
87
+ logger.error(f"Failed to copy hook files: {e}")
88
+ result["error"] = f"Failed to copy hook files: {e}"
89
+ return result
90
+
91
+ # Install shared content (workflows) to .gobby/
92
+ try:
93
+ gobby_path = project_path / ".gobby"
94
+ shared = install_shared_content(gobby_path, project_path)
95
+ result["workflows_installed"] = shared.get("workflows", [])
96
+ except Exception as e:
97
+ logger.warning(f"Failed to install shared content: {e}")
98
+ # Non-fatal - continue with hooks installation
99
+
100
+ # Backup existing hooks.json if it exists
101
+ backup_file = None
102
+ if hooks_file.exists():
103
+ timestamp = int(time.time())
104
+ backup_file = copilot_path / f"hooks.json.{timestamp}.backup"
105
+ try:
106
+ copy2(hooks_file, backup_file)
107
+ except OSError as e:
108
+ logger.error(f"Failed to create backup of hooks.json: {e}")
109
+ result["error"] = f"Failed to create backup: {e}"
110
+ return result
111
+
112
+ # Load existing hooks or create empty
113
+ existing_hooks: dict[str, Any] = {"hooks": {}}
114
+ if hooks_file.exists():
115
+ try:
116
+ with open(hooks_file) as f:
117
+ existing_hooks = json.load(f)
118
+ except json.JSONDecodeError as e:
119
+ logger.warning(f"Failed to parse hooks.json, starting fresh: {e}")
120
+ except OSError as e:
121
+ logger.error(f"Failed to read hooks.json: {e}")
122
+ result["error"] = f"Failed to read hooks.json: {e}"
123
+ return result
124
+
125
+ # Ensure structure
126
+ if "hooks" not in existing_hooks:
127
+ existing_hooks["hooks"] = {}
128
+
129
+ # Load Gobby hooks from template
130
+ try:
131
+ with open(source_hooks_template) as f:
132
+ gobby_hooks_str = f.read()
133
+ except OSError as e:
134
+ logger.error(f"Failed to read hooks template: {e}")
135
+ result["error"] = f"Failed to read hooks template: {e}"
136
+ return result
137
+
138
+ # Replace $PROJECT_PATH with absolute project path
139
+ abs_project_path = str(project_path.resolve())
140
+ gobby_hooks_str = gobby_hooks_str.replace("$PROJECT_PATH", abs_project_path)
141
+
142
+ try:
143
+ gobby_hooks = json.loads(gobby_hooks_str)
144
+ except json.JSONDecodeError as e:
145
+ logger.error(f"Failed to parse hooks template: {e}")
146
+ result["error"] = f"Failed to parse hooks template: {e}"
147
+ return result
148
+
149
+ # Merge Gobby hooks
150
+ new_hooks = gobby_hooks.get("hooks", {})
151
+ for hook_type, hook_config in new_hooks.items():
152
+ existing_hooks["hooks"][hook_type] = hook_config
153
+ hooks_installed.append(hook_type)
154
+
155
+ # Write merged hooks back using atomic write
156
+ try:
157
+ fd, temp_path = tempfile.mkstemp(dir=str(copilot_path), suffix=".tmp", prefix="hooks_")
158
+ try:
159
+ with os.fdopen(fd, "w") as f:
160
+ json.dump(existing_hooks, f, indent=2)
161
+ f.flush()
162
+ os.fsync(f.fileno())
163
+ # Atomic replace
164
+ os.replace(temp_path, hooks_file)
165
+ except Exception:
166
+ if os.path.exists(temp_path):
167
+ os.unlink(temp_path)
168
+ raise
169
+ except OSError as e:
170
+ logger.error(f"Failed to write hooks.json: {e}")
171
+ if backup_file and backup_file.exists():
172
+ try:
173
+ copy2(backup_file, hooks_file)
174
+ logger.info("Restored hooks.json from backup after write failure")
175
+ except OSError as restore_error:
176
+ logger.error(f"Failed to restore from backup: {restore_error}")
177
+ result["error"] = f"Failed to write hooks.json: {e}"
178
+ return result
179
+
180
+ result["success"] = True
181
+ return result
182
+
183
+
184
+ def uninstall_copilot(project_path: Path) -> dict[str, Any]:
185
+ """Uninstall Gobby integration from Copilot CLI.
186
+
187
+ Args:
188
+ project_path: Path to the project root
189
+
190
+ Returns:
191
+ Dict with uninstallation results
192
+ """
193
+ result: dict[str, Any] = {
194
+ "success": False,
195
+ "hooks_removed": [],
196
+ "files_removed": [],
197
+ "error": None,
198
+ }
199
+
200
+ copilot_path = project_path / ".copilot"
201
+ hooks_file = copilot_path / "hooks.json"
202
+ hooks_dir = copilot_path / "hooks"
203
+
204
+ # Remove hook dispatcher
205
+ dispatcher = hooks_dir / "hook_dispatcher.py"
206
+ if dispatcher.exists():
207
+ try:
208
+ dispatcher.unlink()
209
+ result["files_removed"].append(str(dispatcher))
210
+ except OSError as e:
211
+ logger.warning(f"Failed to remove {dispatcher}: {e}")
212
+
213
+ # Remove hooks from hooks.json
214
+ if hooks_file.exists():
215
+ try:
216
+ with open(hooks_file) as f:
217
+ hooks_config = json.load(f)
218
+
219
+ # Remove all Gobby hooks (those that reference hook_dispatcher.py)
220
+ if "hooks" in hooks_config:
221
+ hooks_to_remove = []
222
+ for hook_type, hook_list in hooks_config["hooks"].items():
223
+ if isinstance(hook_list, list):
224
+ for hook in hook_list:
225
+ cmd = hook.get("command", "")
226
+ if "hook_dispatcher.py" in cmd:
227
+ hooks_to_remove.append(hook_type)
228
+ break
229
+
230
+ for hook_type in hooks_to_remove:
231
+ del hooks_config["hooks"][hook_type]
232
+ result["hooks_removed"].append(hook_type)
233
+
234
+ # Write back
235
+ with open(hooks_file, "w") as f:
236
+ json.dump(hooks_config, f, indent=2)
237
+
238
+ except (json.JSONDecodeError, OSError) as e:
239
+ logger.warning(f"Failed to update hooks.json: {e}")
240
+
241
+ result["success"] = True
242
+ return result
@@ -0,0 +1,244 @@
1
+ """
2
+ Cursor installation for Gobby hooks.
3
+
4
+ This module handles installing and uninstalling Gobby hooks for Cursor.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import os
10
+ import tempfile
11
+ import time
12
+ from pathlib import Path
13
+ from shutil import copy2
14
+ from typing import Any
15
+
16
+ from gobby.cli.utils import get_install_dir
17
+
18
+ from .shared import install_shared_content
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def install_cursor(project_path: Path) -> dict[str, Any]:
24
+ """Install Gobby integration for Cursor (hooks, workflows).
25
+
26
+ Args:
27
+ project_path: Path to the project root
28
+
29
+ Returns:
30
+ Dict with installation results including success status and installed items
31
+ """
32
+ hooks_installed: list[str] = []
33
+ result: dict[str, Any] = {
34
+ "success": False,
35
+ "hooks_installed": hooks_installed,
36
+ "workflows_installed": [],
37
+ "error": None,
38
+ }
39
+
40
+ cursor_path = project_path / ".cursor"
41
+ hooks_file = cursor_path / "hooks.json"
42
+
43
+ # Ensure .cursor subdirectories exist
44
+ cursor_path.mkdir(parents=True, exist_ok=True)
45
+ hooks_dir = cursor_path / "hooks"
46
+ hooks_dir.mkdir(parents=True, exist_ok=True)
47
+
48
+ # Get source files
49
+ install_dir = get_install_dir()
50
+ cursor_install_dir = install_dir / "cursor"
51
+ install_hooks_dir = cursor_install_dir / "hooks"
52
+
53
+ # Hook files to copy
54
+ hook_files = {
55
+ "hook_dispatcher.py": True, # Make executable
56
+ }
57
+
58
+ source_hooks_template = cursor_install_dir / "hooks-template.json"
59
+
60
+ # Verify all source files exist
61
+ missing_files = []
62
+ for filename in hook_files.keys():
63
+ source_file = install_hooks_dir / filename
64
+ if not source_file.exists():
65
+ missing_files.append(str(source_file))
66
+
67
+ if not source_hooks_template.exists():
68
+ missing_files.append(str(source_hooks_template))
69
+
70
+ if missing_files:
71
+ result["error"] = f"Missing source files: {missing_files}"
72
+ return result
73
+
74
+ # Copy hook files
75
+ try:
76
+ for filename, make_executable in hook_files.items():
77
+ source_file = install_hooks_dir / filename
78
+ target_file = hooks_dir / filename
79
+
80
+ if target_file.exists():
81
+ target_file.unlink()
82
+
83
+ copy2(source_file, target_file)
84
+ if make_executable:
85
+ target_file.chmod(0o755)
86
+ except OSError as e:
87
+ logger.error(f"Failed to copy hook files: {e}")
88
+ result["error"] = f"Failed to copy hook files: {e}"
89
+ return result
90
+
91
+ # Install shared content (workflows) to .gobby/
92
+ try:
93
+ gobby_path = project_path / ".gobby"
94
+ shared = install_shared_content(gobby_path, project_path)
95
+ result["workflows_installed"] = shared.get("workflows", [])
96
+ except Exception as e:
97
+ logger.warning(f"Failed to install shared content: {e}")
98
+ # Non-fatal - continue with hooks installation
99
+
100
+ # Backup existing hooks.json if it exists
101
+ backup_file = None
102
+ if hooks_file.exists():
103
+ timestamp = int(time.time())
104
+ backup_file = cursor_path / f"hooks.json.{timestamp}.backup"
105
+ try:
106
+ copy2(hooks_file, backup_file)
107
+ except OSError as e:
108
+ logger.error(f"Failed to create backup of hooks.json: {e}")
109
+ result["error"] = f"Failed to create backup: {e}"
110
+ return result
111
+
112
+ # Load existing hooks or create empty
113
+ existing_hooks: dict[str, Any] = {"version": 1, "hooks": {}}
114
+ if hooks_file.exists():
115
+ try:
116
+ with open(hooks_file) as f:
117
+ existing_hooks = json.load(f)
118
+ except json.JSONDecodeError as e:
119
+ logger.warning(f"Failed to parse hooks.json, starting fresh: {e}")
120
+ except OSError as e:
121
+ logger.error(f"Failed to read hooks.json: {e}")
122
+ result["error"] = f"Failed to read hooks.json: {e}"
123
+ return result
124
+
125
+ # Ensure structure
126
+ if "version" not in existing_hooks:
127
+ existing_hooks["version"] = 1
128
+ if "hooks" not in existing_hooks:
129
+ existing_hooks["hooks"] = {}
130
+
131
+ # Load Gobby hooks from template
132
+ try:
133
+ with open(source_hooks_template) as f:
134
+ gobby_hooks_str = f.read()
135
+ except OSError as e:
136
+ logger.error(f"Failed to read hooks template: {e}")
137
+ result["error"] = f"Failed to read hooks template: {e}"
138
+ return result
139
+
140
+ # Replace $PROJECT_PATH with absolute project path
141
+ abs_project_path = str(project_path.resolve())
142
+ gobby_hooks_str = gobby_hooks_str.replace("$PROJECT_PATH", abs_project_path)
143
+
144
+ try:
145
+ gobby_hooks = json.loads(gobby_hooks_str)
146
+ except json.JSONDecodeError as e:
147
+ logger.error(f"Failed to parse hooks template: {e}")
148
+ result["error"] = f"Failed to parse hooks template: {e}"
149
+ return result
150
+
151
+ # Merge Gobby hooks
152
+ new_hooks = gobby_hooks.get("hooks", {})
153
+ for hook_type, hook_config in new_hooks.items():
154
+ existing_hooks["hooks"][hook_type] = hook_config
155
+ hooks_installed.append(hook_type)
156
+
157
+ # Write merged hooks back using atomic write
158
+ try:
159
+ fd, temp_path = tempfile.mkstemp(dir=str(cursor_path), suffix=".tmp", prefix="hooks_")
160
+ try:
161
+ with os.fdopen(fd, "w") as f:
162
+ json.dump(existing_hooks, f, indent=2)
163
+ f.flush()
164
+ os.fsync(f.fileno())
165
+ # Atomic replace
166
+ os.replace(temp_path, hooks_file)
167
+ except Exception:
168
+ if os.path.exists(temp_path):
169
+ os.unlink(temp_path)
170
+ raise
171
+ except OSError as e:
172
+ logger.error(f"Failed to write hooks.json: {e}")
173
+ if backup_file and backup_file.exists():
174
+ try:
175
+ copy2(backup_file, hooks_file)
176
+ logger.info("Restored hooks.json from backup after write failure")
177
+ except OSError as restore_error:
178
+ logger.error(f"Failed to restore from backup: {restore_error}")
179
+ result["error"] = f"Failed to write hooks.json: {e}"
180
+ return result
181
+
182
+ result["success"] = True
183
+ return result
184
+
185
+
186
+ def uninstall_cursor(project_path: Path) -> dict[str, Any]:
187
+ """Uninstall Gobby integration from Cursor.
188
+
189
+ Args:
190
+ project_path: Path to the project root
191
+
192
+ Returns:
193
+ Dict with uninstallation results
194
+ """
195
+ result: dict[str, Any] = {
196
+ "success": False,
197
+ "hooks_removed": [],
198
+ "files_removed": [],
199
+ "error": None,
200
+ }
201
+
202
+ cursor_path = project_path / ".cursor"
203
+ hooks_file = cursor_path / "hooks.json"
204
+ hooks_dir = cursor_path / "hooks"
205
+
206
+ # Remove hook dispatcher
207
+ dispatcher = hooks_dir / "hook_dispatcher.py"
208
+ if dispatcher.exists():
209
+ try:
210
+ dispatcher.unlink()
211
+ result["files_removed"].append(str(dispatcher))
212
+ except OSError as e:
213
+ logger.warning(f"Failed to remove {dispatcher}: {e}")
214
+
215
+ # Remove hooks from hooks.json
216
+ if hooks_file.exists():
217
+ try:
218
+ with open(hooks_file) as f:
219
+ hooks_config = json.load(f)
220
+
221
+ # Remove all Gobby hooks (those that reference hook_dispatcher.py)
222
+ if "hooks" in hooks_config:
223
+ hooks_to_remove = []
224
+ for hook_type, hook_list in hooks_config["hooks"].items():
225
+ if isinstance(hook_list, list):
226
+ for hook in hook_list:
227
+ cmd = hook.get("command", "")
228
+ if "hook_dispatcher.py" in cmd:
229
+ hooks_to_remove.append(hook_type)
230
+ break
231
+
232
+ for hook_type in hooks_to_remove:
233
+ del hooks_config["hooks"][hook_type]
234
+ result["hooks_removed"].append(hook_type)
235
+
236
+ # Write back
237
+ with open(hooks_file, "w") as f:
238
+ json.dump(hooks_config, f, indent=2)
239
+
240
+ except (json.JSONDecodeError, OSError) as e:
241
+ logger.warning(f"Failed to update hooks.json: {e}")
242
+
243
+ result["success"] = True
244
+ return result
@@ -43,6 +43,9 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
43
43
  target_workflows = project_path / ".gobby" / "workflows"
44
44
  target_workflows.mkdir(parents=True, exist_ok=True)
45
45
  for item in shared_workflows.iterdir():
46
+ # Skip deprecated workflows - they are kept for reference only
47
+ if item.name == "deprecated":
48
+ continue
46
49
  if item.is_file():
47
50
  copy2(item, target_workflows / item.name)
48
51
  installed["workflows"].append(item.name)