gobby 0.2.9__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 (134) 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 +2 -2
  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 +5 -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/workflows.py +38 -17
  35. gobby/config/app.py +5 -0
  36. gobby/config/skills.py +23 -2
  37. gobby/hooks/broadcaster.py +9 -0
  38. gobby/hooks/event_handlers/_base.py +6 -1
  39. gobby/hooks/event_handlers/_session.py +44 -130
  40. gobby/hooks/events.py +48 -0
  41. gobby/hooks/hook_manager.py +25 -3
  42. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  43. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  44. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  45. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  46. gobby/llm/__init__.py +14 -1
  47. gobby/llm/claude.py +217 -1
  48. gobby/llm/service.py +149 -0
  49. gobby/mcp_proxy/instructions.py +9 -27
  50. gobby/mcp_proxy/models.py +1 -0
  51. gobby/mcp_proxy/registries.py +56 -9
  52. gobby/mcp_proxy/server.py +6 -2
  53. gobby/mcp_proxy/services/tool_filter.py +7 -0
  54. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  55. gobby/mcp_proxy/stdio.py +37 -21
  56. gobby/mcp_proxy/tools/agents.py +7 -0
  57. gobby/mcp_proxy/tools/hub.py +30 -1
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  59. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  60. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  61. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  62. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  63. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  64. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  65. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  66. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  67. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  68. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  69. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  70. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  71. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  72. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  73. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  74. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  75. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  76. gobby/mcp_proxy/tools/workflows/__init__.py +9 -2
  77. gobby/mcp_proxy/tools/workflows/_lifecycle.py +12 -1
  78. gobby/mcp_proxy/tools/workflows/_query.py +45 -26
  79. gobby/mcp_proxy/tools/workflows/_terminal.py +39 -3
  80. gobby/mcp_proxy/tools/worktrees.py +54 -15
  81. gobby/memory/context.py +5 -5
  82. gobby/runner.py +108 -6
  83. gobby/servers/http.py +7 -1
  84. gobby/servers/routes/__init__.py +2 -0
  85. gobby/servers/routes/admin.py +44 -0
  86. gobby/servers/routes/mcp/endpoints/execution.py +18 -25
  87. gobby/servers/routes/mcp/hooks.py +10 -1
  88. gobby/servers/routes/pipelines.py +227 -0
  89. gobby/servers/websocket.py +314 -1
  90. gobby/sessions/analyzer.py +87 -1
  91. gobby/sessions/manager.py +5 -5
  92. gobby/sessions/transcripts/__init__.py +3 -0
  93. gobby/sessions/transcripts/claude.py +5 -0
  94. gobby/sessions/transcripts/codex.py +5 -0
  95. gobby/sessions/transcripts/gemini.py +5 -0
  96. gobby/skills/hubs/__init__.py +25 -0
  97. gobby/skills/hubs/base.py +234 -0
  98. gobby/skills/hubs/claude_plugins.py +328 -0
  99. gobby/skills/hubs/clawdhub.py +289 -0
  100. gobby/skills/hubs/github_collection.py +465 -0
  101. gobby/skills/hubs/manager.py +263 -0
  102. gobby/skills/hubs/skillhub.py +342 -0
  103. gobby/storage/memories.py +4 -4
  104. gobby/storage/migrations.py +95 -3
  105. gobby/storage/pipelines.py +367 -0
  106. gobby/storage/sessions.py +23 -4
  107. gobby/storage/skills.py +1 -1
  108. gobby/storage/tasks/_aggregates.py +2 -2
  109. gobby/storage/tasks/_lifecycle.py +4 -4
  110. gobby/storage/tasks/_models.py +7 -1
  111. gobby/storage/tasks/_queries.py +3 -3
  112. gobby/sync/memories.py +4 -3
  113. gobby/tasks/commits.py +48 -17
  114. gobby/workflows/actions.py +75 -0
  115. gobby/workflows/context_actions.py +246 -5
  116. gobby/workflows/definitions.py +119 -1
  117. gobby/workflows/detection_helpers.py +23 -11
  118. gobby/workflows/enforcement/task_policy.py +18 -0
  119. gobby/workflows/engine.py +20 -1
  120. gobby/workflows/evaluator.py +8 -5
  121. gobby/workflows/lifecycle_evaluator.py +57 -26
  122. gobby/workflows/loader.py +567 -30
  123. gobby/workflows/lobster_compat.py +147 -0
  124. gobby/workflows/pipeline_executor.py +801 -0
  125. gobby/workflows/pipeline_state.py +172 -0
  126. gobby/workflows/pipeline_webhooks.py +206 -0
  127. gobby/workflows/premature_stop.py +5 -0
  128. gobby/worktrees/git.py +135 -20
  129. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  130. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/RECORD +134 -106
  131. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  132. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  133. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  134. {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,373 @@
1
+ """Cursor adapter for hook translation.
2
+
3
+ This adapter translates between Cursor's native hook format and the unified
4
+ HookEvent/HookResponse models.
5
+
6
+ Cursor Hook Types (17 total):
7
+ - sessionStart, sessionEnd: Session lifecycle
8
+ - beforeSubmitPrompt: Before user prompt validation
9
+ - preToolUse, postToolUse, postToolUseFailure: Generic tool lifecycle
10
+ - beforeShellExecution, afterShellExecution: Shell-specific hooks
11
+ - beforeMCPExecution, afterMCPExecution: MCP tool hooks
12
+ - beforeReadFile, afterFileEdit: File operation hooks
13
+ - preCompact: Context compaction
14
+ - stop: Agent stops
15
+ - subagentStart, subagentStop: Subagent lifecycle
16
+ - beforeTabFileRead, afterTabFileEdit: Tab completion hooks
17
+
18
+ Cursor Config Format (.cursor/hooks.json):
19
+ {
20
+ "version": 1,
21
+ "hooks": {
22
+ "preToolUse": [{"command": "./script.sh", "matcher": {...}}],
23
+ ...
24
+ }
25
+ }
26
+
27
+ Key Differences from Claude Code:
28
+ - Uses camelCase event names (not kebab-case)
29
+ - Response uses decision: "allow"/"deny" (not "approve"/"block")
30
+ - Has more granular file/shell/MCP hooks
31
+ - Config requires "version": 1 field
32
+ - Loads Claude Code hooks from .claude/settings.json as fallback
33
+
34
+ Documentation: https://cursor.com/docs/agent/hooks
35
+ """
36
+
37
+ from datetime import UTC, datetime
38
+ from typing import TYPE_CHECKING, Any
39
+
40
+ from gobby.adapters.base import BaseAdapter
41
+ from gobby.hooks.events import HookEvent, HookEventType, HookResponse, SessionSource
42
+
43
+ if TYPE_CHECKING:
44
+ from gobby.hooks.hook_manager import HookManager
45
+
46
+
47
+ class CursorAdapter(BaseAdapter):
48
+ """Adapter for Cursor CLI hook translation.
49
+
50
+ This adapter:
51
+ 1. Translates Cursor's camelCase hook payloads to unified HookEvent
52
+ 2. Translates HookResponse back to Cursor's expected format
53
+ 3. Calls HookManager.handle() with unified HookEvent model
54
+
55
+ Cursor's hooks system is very similar to Claude Code but uses camelCase
56
+ event names and has additional granular hooks for file/shell/MCP operations.
57
+ """
58
+
59
+ source = SessionSource.CURSOR
60
+
61
+ # Event type mapping: Cursor hook names -> unified HookEventType
62
+ # Cursor uses camelCase hook names in the payload's "hook_type" field
63
+ EVENT_MAP: dict[str, HookEventType] = {
64
+ # Session lifecycle
65
+ "sessionStart": HookEventType.SESSION_START,
66
+ "sessionEnd": HookEventType.SESSION_END,
67
+ # Prompt submission
68
+ "beforeSubmitPrompt": HookEventType.BEFORE_AGENT,
69
+ # Generic tool hooks
70
+ "preToolUse": HookEventType.BEFORE_TOOL,
71
+ "postToolUse": HookEventType.AFTER_TOOL,
72
+ "postToolUseFailure": HookEventType.AFTER_TOOL, # Same as AFTER_TOOL with error flag
73
+ # Shell-specific hooks (map to generic BEFORE/AFTER_TOOL with tool_type)
74
+ "beforeShellExecution": HookEventType.BEFORE_TOOL,
75
+ "afterShellExecution": HookEventType.AFTER_TOOL,
76
+ # MCP-specific hooks
77
+ "beforeMCPExecution": HookEventType.BEFORE_TOOL,
78
+ "afterMCPExecution": HookEventType.AFTER_TOOL,
79
+ # File-specific hooks
80
+ "beforeReadFile": HookEventType.BEFORE_TOOL,
81
+ "afterFileEdit": HookEventType.AFTER_TOOL,
82
+ # Compaction and stop
83
+ "preCompact": HookEventType.PRE_COMPACT,
84
+ "stop": HookEventType.STOP,
85
+ # Subagent lifecycle
86
+ "subagentStart": HookEventType.SUBAGENT_START,
87
+ "subagentStop": HookEventType.SUBAGENT_STOP,
88
+ # Tab completion hooks (treated as tool events)
89
+ "beforeTabFileRead": HookEventType.BEFORE_TOOL,
90
+ "afterTabFileEdit": HookEventType.AFTER_TOOL,
91
+ # Response hooks (informational)
92
+ "afterAgentResponse": HookEventType.NOTIFICATION,
93
+ "afterAgentThought": HookEventType.NOTIFICATION,
94
+ }
95
+
96
+ # Map Cursor-specific hook types to their tool_type
97
+ # This helps downstream code identify what kind of tool is being used
98
+ HOOK_TO_TOOL_TYPE: dict[str, str] = {
99
+ "beforeShellExecution": "Bash",
100
+ "afterShellExecution": "Bash",
101
+ "beforeMCPExecution": "mcp_call",
102
+ "afterMCPExecution": "mcp_call",
103
+ "beforeReadFile": "Read",
104
+ "afterFileEdit": "Edit",
105
+ "beforeTabFileRead": "Read",
106
+ "afterTabFileEdit": "Edit",
107
+ }
108
+
109
+ def __init__(self, hook_manager: "HookManager | None" = None):
110
+ """Initialize the Cursor adapter.
111
+
112
+ Args:
113
+ hook_manager: Reference to HookManager for delegation.
114
+ If None, the adapter can only translate (not handle events).
115
+ """
116
+ self._hook_manager = hook_manager
117
+
118
+ def translate_to_hook_event(self, native_event: dict[str, Any]) -> HookEvent:
119
+ """Convert Cursor native event to unified HookEvent.
120
+
121
+ Cursor payloads have the structure:
122
+ {
123
+ "hook_type": "preToolUse", # camelCase hook name
124
+ "input_data": {
125
+ "session_id": "abc123",
126
+ "tool_name": "Shell",
127
+ "tool_input": {"command": "npm install"},
128
+ "tool_use_id": "xyz789",
129
+ "cwd": "/path/to/project",
130
+ "model": "claude-sonnet-4-20250514",
131
+ "agent_message": "Installing dependencies..."
132
+ }
133
+ }
134
+
135
+ Args:
136
+ native_event: Raw payload from Cursor's hook dispatcher
137
+
138
+ Returns:
139
+ Unified HookEvent with normalized fields.
140
+ """
141
+ hook_type = native_event.get("hook_type", "")
142
+ input_data = native_event.get("input_data", {})
143
+
144
+ # Map Cursor hook type to unified event type
145
+ # Fall back to NOTIFICATION for unknown types (fail-open)
146
+ event_type = self.EVENT_MAP.get(hook_type, HookEventType.NOTIFICATION)
147
+
148
+ # Extract session_id
149
+ session_id = input_data.get("session_id", "")
150
+
151
+ # Check for failure flag in postToolUseFailure
152
+ is_failure = hook_type == "postToolUseFailure"
153
+ metadata: dict[str, Any] = {"is_failure": is_failure} if is_failure else {}
154
+
155
+ # Add tool_type for specific hooks
156
+ if hook_type in self.HOOK_TO_TOOL_TYPE:
157
+ metadata["tool_type"] = self.HOOK_TO_TOOL_TYPE[hook_type]
158
+
159
+ # Normalize event data for CLI-agnostic processing
160
+ normalized_data = self._normalize_event_data(input_data, hook_type)
161
+
162
+ return HookEvent(
163
+ event_type=event_type,
164
+ session_id=session_id,
165
+ source=self.source,
166
+ timestamp=datetime.now(UTC),
167
+ machine_id=input_data.get("machine_id"),
168
+ cwd=input_data.get("cwd"),
169
+ data=normalized_data,
170
+ metadata=metadata,
171
+ )
172
+
173
+ def _normalize_event_data(
174
+ self, input_data: dict[str, Any], hook_type: str = ""
175
+ ) -> dict[str, Any]:
176
+ """Normalize Cursor event data for CLI-agnostic processing.
177
+
178
+ This method enriches the input_data with normalized fields so downstream
179
+ code doesn't need to handle Cursor-specific formats.
180
+
181
+ Normalizations performed:
182
+ 1. tool_input.server_name/tool_name → mcp_server/mcp_tool (for MCP calls)
183
+ 2. Infer tool_name from hook_type for specific hooks
184
+
185
+ Args:
186
+ input_data: Raw input data from Cursor
187
+ hook_type: The hook type (used to infer tool_name for specific hooks)
188
+
189
+ Returns:
190
+ Enriched data dict with normalized fields added
191
+ """
192
+ # Start with a copy to avoid mutating original
193
+ data = dict(input_data)
194
+
195
+ # Get tool info
196
+ tool_name = data.get("tool_name", "")
197
+ tool_input = data.get("tool_input", {}) or {}
198
+
199
+ # Infer tool_name from hook type for specific hooks
200
+ if not tool_name and hook_type in self.HOOK_TO_TOOL_TYPE:
201
+ data["tool_name"] = self.HOOK_TO_TOOL_TYPE[hook_type]
202
+
203
+ # Extract MCP info from nested tool_input for MCP calls
204
+ if hook_type in ("beforeMCPExecution", "afterMCPExecution") or tool_name in (
205
+ "call_tool",
206
+ "mcp__gobby__call_tool",
207
+ ):
208
+ if "mcp_server" not in data:
209
+ data["mcp_server"] = tool_input.get("server_name")
210
+ if "mcp_tool" not in data:
211
+ data["mcp_tool"] = tool_input.get("tool_name")
212
+
213
+ # Normalize tool_result → tool_output
214
+ if "tool_result" in data and "tool_output" not in data:
215
+ data["tool_output"] = data["tool_result"]
216
+
217
+ return data
218
+
219
+ # Map Cursor hook types to hookEventName for hookSpecificOutput
220
+ HOOK_EVENT_NAME_MAP: dict[str, str] = {
221
+ "sessionStart": "SessionStart",
222
+ "sessionEnd": "SessionEnd",
223
+ "beforeSubmitPrompt": "UserPromptSubmit",
224
+ "preToolUse": "PreToolUse",
225
+ "postToolUse": "PostToolUse",
226
+ "postToolUseFailure": "PostToolUse",
227
+ "beforeShellExecution": "PreToolUse",
228
+ "afterShellExecution": "PostToolUse",
229
+ "beforeMCPExecution": "PreToolUse",
230
+ "afterMCPExecution": "PostToolUse",
231
+ "beforeReadFile": "PreToolUse",
232
+ "afterFileEdit": "PostToolUse",
233
+ "preCompact": "PreCompact",
234
+ "stop": "Stop",
235
+ "subagentStart": "SubagentStart",
236
+ "subagentStop": "SubagentStop",
237
+ "beforeTabFileRead": "PreToolUse",
238
+ "afterTabFileEdit": "PostToolUse",
239
+ "afterAgentResponse": "Notification",
240
+ "afterAgentThought": "Notification",
241
+ }
242
+
243
+ def translate_from_hook_response(
244
+ self, response: HookResponse, hook_type: str | None = None
245
+ ) -> dict[str, Any]:
246
+ """Convert HookResponse to Cursor's expected format.
247
+
248
+ Cursor expects responses in this format:
249
+ {
250
+ "decision": "allow"/"deny", # Tool decision
251
+ "reason": "...", # Reason if denied (optional)
252
+ "updated_input": {...}, # Modified tool input (optional)
253
+ "user_message": "...", # Message to show user (optional)
254
+ "agent_message": "...", # Message to send to model (optional)
255
+ "permission": "allow"/"deny"/"ask", # For permission hooks
256
+ "followup_message": "...", # Auto-submit message (for stop hook)
257
+ "env": {...}, # Environment variables (sessionStart)
258
+ "additional_context": "...", # Context injection (sessionStart)
259
+ "continue": true/false # Whether to continue (sessionStart)
260
+ }
261
+
262
+ Args:
263
+ response: Unified HookResponse from HookManager.
264
+ hook_type: Original Cursor hook type (e.g., "preToolUse")
265
+ Used to determine response format.
266
+
267
+ Returns:
268
+ Dict in Cursor's expected format.
269
+ """
270
+ # Determine response format based on hook type
271
+ hook_type = hook_type or ""
272
+
273
+ # Base decision - Cursor uses "allow"/"deny"
274
+ should_allow = response.decision not in ("deny", "block")
275
+
276
+ result: dict[str, Any] = {}
277
+
278
+ # Permission-based hooks (beforeShellExecution, beforeReadFile, etc.)
279
+ if hook_type in (
280
+ "beforeShellExecution",
281
+ "beforeReadFile",
282
+ "beforeMCPExecution",
283
+ ):
284
+ result["permission"] = "allow" if should_allow else "deny"
285
+ if response.reason:
286
+ result["user_message"] = response.reason
287
+ if response.context:
288
+ result["agent_message"] = response.context
289
+
290
+ # Decision hooks (preToolUse, subagentStart)
291
+ elif hook_type in ("preToolUse", "subagentStart"):
292
+ result["decision"] = "allow" if should_allow else "deny"
293
+ if response.reason:
294
+ result["reason"] = response.reason
295
+
296
+ # Continuation hooks (stop, subagentStop)
297
+ elif hook_type in ("stop", "subagentStop"):
298
+ if response.context:
299
+ result["followup_message"] = response.context
300
+
301
+ # Session hooks (sessionStart)
302
+ elif hook_type == "sessionStart":
303
+ result["continue"] = should_allow
304
+ if response.reason:
305
+ result["user_message"] = response.reason
306
+
307
+ # Build additional_context from response context and metadata
308
+ additional_context_parts: list[str] = []
309
+ if response.context:
310
+ additional_context_parts.append(response.context)
311
+
312
+ # Add session identifiers from metadata
313
+ if response.metadata:
314
+ gobby_session_id = response.metadata.get("session_id")
315
+ session_ref = response.metadata.get("session_ref")
316
+ external_id = response.metadata.get("external_id")
317
+
318
+ if gobby_session_id:
319
+ context_lines = []
320
+ if session_ref:
321
+ context_lines.append(
322
+ f"Gobby Session ID: {session_ref} (or {gobby_session_id})"
323
+ )
324
+ else:
325
+ context_lines.append(f"Gobby Session ID: {gobby_session_id}")
326
+ if external_id:
327
+ context_lines.append(
328
+ f"CLI-Specific Session ID (external_id): {external_id}"
329
+ )
330
+ if response.metadata.get("machine_id"):
331
+ context_lines.append(f"machine_id: {response.metadata['machine_id']}")
332
+ if response.metadata.get("project_id"):
333
+ context_lines.append(f"project_id: {response.metadata['project_id']}")
334
+ additional_context_parts.append("\n".join(context_lines))
335
+
336
+ if additional_context_parts:
337
+ result["additional_context"] = "\n\n".join(additional_context_parts)
338
+
339
+ # Default format for other hooks
340
+ else:
341
+ result["decision"] = "allow" if should_allow else "deny"
342
+ if response.reason:
343
+ result["reason"] = response.reason
344
+
345
+ # Add context to agent_message for tool hooks if not already set
346
+ if response.context and "agent_message" not in result:
347
+ result["agent_message"] = response.context
348
+
349
+ # Add system_message if present
350
+ if response.system_message:
351
+ result["user_message"] = response.system_message
352
+
353
+ return result
354
+
355
+ def handle_native(
356
+ self, native_event: dict[str, Any], hook_manager: "HookManager"
357
+ ) -> dict[str, Any]:
358
+ """Main entry point for HTTP endpoint.
359
+
360
+ Args:
361
+ native_event: Raw payload from Cursor's hook dispatcher
362
+ hook_manager: HookManager instance for processing.
363
+
364
+ Returns:
365
+ Response dict in Cursor's expected format.
366
+ """
367
+ # Translate to HookEvent
368
+ hook_event = self.translate_to_hook_event(native_event)
369
+
370
+ # Use HookEvent-based handler
371
+ hook_type = native_event.get("hook_type", "")
372
+ hook_response = hook_manager.handle(hook_event)
373
+ return self.translate_from_hook_response(hook_response, hook_type=hook_type)
gobby/adapters/gemini.py CHANGED
@@ -20,8 +20,6 @@ Key differences from Claude Code:
20
20
  - Different tool names (RunShellCommand vs Bash)
21
21
  """
22
22
 
23
- import platform
24
- import uuid
25
23
  from datetime import UTC, datetime
26
24
  from typing import TYPE_CHECKING, Any
27
25
 
@@ -119,28 +117,6 @@ class GeminiAdapter(BaseAdapter):
119
117
  If None, the adapter can only translate (not handle events).
120
118
  """
121
119
  self._hook_manager = hook_manager
122
- # Cache machine_id since Gemini doesn't always send it
123
- self._machine_id: str | None = None
124
-
125
- def _get_machine_id(self) -> str:
126
- """Get or generate a machine identifier.
127
-
128
- Gemini CLI doesn't always send machine_id, so we generate one
129
- based on the platform node (hostname/MAC address).
130
-
131
- Returns:
132
- A stable machine identifier.
133
- """
134
- if self._machine_id is None:
135
- # Use platform.node() which returns hostname or MAC-based ID
136
- node = platform.node()
137
- if node:
138
- # Create a deterministic UUID from the node name
139
- self._machine_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, node))
140
- else:
141
- # Fallback to a random UUID (less ideal but works)
142
- self._machine_id = str(uuid.uuid4())
143
- return self._machine_id
144
120
 
145
121
  def normalize_tool_name(self, gemini_tool_name: str) -> str:
146
122
  """Normalize Gemini tool name to standard format.
@@ -254,8 +230,8 @@ class GeminiAdapter(BaseAdapter):
254
230
  else:
255
231
  timestamp = datetime.now(UTC)
256
232
 
257
- # Get machine_id (Gemini might not send it)
258
- machine_id = input_data.get("machine_id") or self._get_machine_id()
233
+ # Get machine_id from payload (base adapter injects if missing)
234
+ machine_id = input_data.get("machine_id")
259
235
 
260
236
  # Normalize tool name if present (for tool-related hooks)
261
237
  if "tool_name" in input_data: