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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +2 -2
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +5 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/skills.py +23 -2
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/_base.py +6 -1
- gobby/hooks/event_handlers/_session.py +44 -130
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +25 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +217 -1
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +56 -9
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +9 -2
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +12 -1
- gobby/mcp_proxy/tools/workflows/_query.py +45 -26
- gobby/mcp_proxy/tools/workflows/_terminal.py +39 -3
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/context.py +5 -5
- gobby/runner.py +108 -6
- gobby/servers/http.py +7 -1
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +44 -0
- gobby/servers/routes/mcp/endpoints/execution.py +18 -25
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +87 -1
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +95 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +1 -1
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- gobby/workflows/actions.py +75 -0
- gobby/workflows/context_actions.py +246 -5
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +20 -1
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +57 -26
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/RECORD +134 -106
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/adapters/cursor.py
ADDED
|
@@ -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 (
|
|
258
|
-
machine_id = input_data.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:
|