memgraph-context-graph 0.5.0__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 (33) hide show
  1. memgraph_context_graph/__init__.py +71 -0
  2. memgraph_context_graph/adapters/__init__.py +0 -0
  3. memgraph_context_graph/adapters/_identity.py +520 -0
  4. memgraph_context_graph/adapters/_opencode_plugin.js +112 -0
  5. memgraph_context_graph/adapters/_spec.py +441 -0
  6. memgraph_context_graph/adapters/antigravity_cli.py +152 -0
  7. memgraph_context_graph/adapters/claude.py +297 -0
  8. memgraph_context_graph/adapters/claude_code.py +216 -0
  9. memgraph_context_graph/adapters/claude_code_memory.py +181 -0
  10. memgraph_context_graph/adapters/codex.py +118 -0
  11. memgraph_context_graph/adapters/codex_memory.py +185 -0
  12. memgraph_context_graph/adapters/copilot_cli.py +236 -0
  13. memgraph_context_graph/adapters/cursor.py +189 -0
  14. memgraph_context_graph/adapters/grok.py +187 -0
  15. memgraph_context_graph/adapters/openai.py +240 -0
  16. memgraph_context_graph/adapters/opencode.py +168 -0
  17. memgraph_context_graph/cli.py +845 -0
  18. memgraph_context_graph/events.py +210 -0
  19. memgraph_context_graph/hooks/__init__.py +1 -0
  20. memgraph_context_graph/hooks/cli.py +184 -0
  21. memgraph_context_graph/hooks/runner.py +311 -0
  22. memgraph_context_graph/hooks/runtime_plugin.py +85 -0
  23. memgraph_context_graph/link.py +50 -0
  24. memgraph_context_graph/mcp_server.py +102 -0
  25. memgraph_context_graph/memory_backend.py +144 -0
  26. memgraph_context_graph/protocols.py +67 -0
  27. memgraph_context_graph/spaces.py +214 -0
  28. memgraph_context_graph/tools.py +130 -0
  29. memgraph_context_graph-0.5.0.dist-info/METADATA +718 -0
  30. memgraph_context_graph-0.5.0.dist-info/RECORD +33 -0
  31. memgraph_context_graph-0.5.0.dist-info/WHEEL +4 -0
  32. memgraph_context_graph-0.5.0.dist-info/entry_points.txt +11 -0
  33. memgraph_context_graph-0.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,236 @@
1
+ """GitHub Copilot CLI command-hook runtime adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import json
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from memgraph_context_graph.adapters._spec import (
10
+ EventContext,
11
+ HookConfig,
12
+ RuntimeSpec,
13
+ SpecAdapter,
14
+ SpecPlugin,
15
+ extract_tool_result,
16
+ json_hook_installer,
17
+ session_start,
18
+ tool_start,
19
+ turn_end,
20
+ )
21
+ from memgraph_context_graph.events import (
22
+ AgentEndEvent,
23
+ AgentStartEvent,
24
+ ErrorOccurredEvent,
25
+ MessageEvent,
26
+ SessionEndEvent,
27
+ ToolEndEvent,
28
+ ToolStartEvent,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from memgraph_context_graph.events import Event
33
+
34
+
35
+ def _session_end(context: EventContext) -> SessionEndEvent:
36
+ reason = str(
37
+ context.payload.get("reason")
38
+ or context.payload.get("stop_reason")
39
+ or context.payload.get("stopReason")
40
+ or "completed"
41
+ )
42
+ return SessionEndEvent(**context.base(), status="completed" if reason in {"complete", "end_turn"} else reason)
43
+
44
+
45
+ def _message(context: EventContext) -> MessageEvent:
46
+ return MessageEvent(**context.base(), role="user", content=context.value("prompt", ""))
47
+
48
+
49
+ def _tool_input(context: EventContext) -> Any:
50
+ # Native Copilot payloads may carry toolArgs as a JSON-encoded string.
51
+ tool_input = context.value("tool_input")
52
+ if isinstance(tool_input, str):
53
+ try:
54
+ return json.loads(tool_input)
55
+ except ValueError:
56
+ return tool_input
57
+ return tool_input
58
+
59
+
60
+ def _tool_start(context: EventContext) -> ToolStartEvent:
61
+ return dataclasses.replace(tool_start(context), tool_input=_tool_input(context))
62
+
63
+
64
+ def _tool_end(context: EventContext) -> ToolEndEvent:
65
+ tool_result = context.value("tool_result")
66
+ result, is_error, error_message = extract_tool_result(tool_result)
67
+ result_type = (
68
+ tool_result.get("resultType", tool_result.get("result_type")) if isinstance(tool_result, dict) else None
69
+ )
70
+ if result_type not in (None, "success"):
71
+ is_error = True
72
+ error_message = error_message or f"Copilot tool result: {result_type}"
73
+ return _with_tool_input(
74
+ context,
75
+ ToolEndEvent(
76
+ **context.base(),
77
+ tool_name=context.text("tool_name"),
78
+ tool_use_id=context.optional_text("tool_use_id"),
79
+ result=result,
80
+ is_error=is_error,
81
+ error_message=error_message,
82
+ ),
83
+ )
84
+
85
+
86
+ def _tool_failure(context: EventContext) -> ToolEndEvent:
87
+ return _with_tool_input(
88
+ context,
89
+ ToolEndEvent(
90
+ **context.base(),
91
+ tool_name=context.text("tool_name"),
92
+ tool_use_id=context.optional_text("tool_use_id"),
93
+ is_error=True,
94
+ error_message=str(context.payload.get("error") or "Tool failed"),
95
+ ),
96
+ )
97
+
98
+
99
+ def _with_tool_input(context: EventContext, event: ToolEndEvent) -> ToolEndEvent:
100
+ # Copilot sends no tool call id, so keep the input on the result to tell
101
+ # repeated calls of the same tool apart.
102
+ tool_input = _tool_input(context)
103
+ if tool_input is not None:
104
+ event.metadata["tool_input"] = tool_input
105
+ return event
106
+
107
+
108
+ def _permission(context: EventContext) -> MessageEvent:
109
+ return MessageEvent(**context.base(), role="system", content=context.text("tool_name", "permission_request"))
110
+
111
+
112
+ def _agent_name(context: EventContext) -> str:
113
+ # subagentStart carries only agentName; subagentStop adds agentId and
114
+ # agentType. agentName is the one key both share, so it names the Agent.
115
+ return str(context.payload.get("agentName") or context.payload.get("agent_name") or context.text("agent_type"))
116
+
117
+
118
+ def _agent_start(context: EventContext) -> AgentStartEvent:
119
+ agent_name = _agent_name(context)
120
+ return AgentStartEvent(**context.base(), agent_name=agent_name, agent_type=agent_name)
121
+
122
+
123
+ def _agent_end(context: EventContext) -> AgentEndEvent:
124
+ agent_name = _agent_name(context)
125
+ output = context.payload.get("last_assistant_message", context.payload.get("response"))
126
+ return AgentEndEvent(
127
+ **context.base(),
128
+ agent_name=agent_name,
129
+ agent_type=context.text("agent_type", agent_name),
130
+ output=None if output is None else str(output),
131
+ )
132
+
133
+
134
+ def _turn_end(context: EventContext) -> list[Event]:
135
+ # agentStop ends one turn; sessionEnd ends the session. Its payload carries
136
+ # no reply text.
137
+ return turn_end(context, reason=context.payload.get("stopReason", context.payload.get("stop_reason")))
138
+
139
+
140
+ def _error(context: EventContext) -> ErrorOccurredEvent:
141
+ error = context.payload.get("error")
142
+ error_dict = error if isinstance(error, dict) else {}
143
+ message = error_dict.get("message", error)
144
+ details = {"context": context.payload.get("error_context", context.payload.get("errorContext"))}
145
+ if error_dict.get("stack") is not None:
146
+ details["stack"] = error_dict["stack"]
147
+ return ErrorOccurredEvent(
148
+ **context.base(),
149
+ error_type=str(error_dict.get("name") or "copilot_error"),
150
+ error_message=str(message or "Copilot CLI error"),
151
+ error_details=details,
152
+ recoverable=bool(context.payload.get("recoverable", True)),
153
+ )
154
+
155
+
156
+ _HOOKS = (
157
+ "sessionStart",
158
+ "sessionEnd",
159
+ "userPromptSubmitted",
160
+ "preToolUse",
161
+ "postToolUse",
162
+ "postToolUseFailure",
163
+ "permissionRequest",
164
+ "subagentStart",
165
+ "subagentStop",
166
+ "agentStop",
167
+ "errorOccurred",
168
+ )
169
+ SPEC = RuntimeSpec(
170
+ name="copilot-cli",
171
+ source_sdk="copilot-cli",
172
+ event_key="hook_event_name",
173
+ hooks=_HOOKS,
174
+ rules={
175
+ "sessionStart": session_start,
176
+ "sessionEnd": _session_end,
177
+ "userPromptSubmitted": _message,
178
+ "preToolUse": _tool_start,
179
+ "postToolUse": _tool_end,
180
+ "postToolUseFailure": _tool_failure,
181
+ "permissionRequest": _permission,
182
+ "subagentStart": _agent_start,
183
+ "subagentStop": _agent_end,
184
+ "agentStop": _turn_end,
185
+ "errorOccurred": _error,
186
+ },
187
+ # VS Code-compatible hook files use PascalCase event names.
188
+ event_aliases={
189
+ **{hook[0].upper() + hook[1:]: hook for hook in _HOOKS},
190
+ "UserPromptSubmit": "userPromptSubmitted",
191
+ },
192
+ metadata_keys=(
193
+ "cwd",
194
+ "timestamp",
195
+ "source",
196
+ "transcript_path",
197
+ "reason",
198
+ "stop_reason",
199
+ "tool_name",
200
+ "tool_input",
201
+ "agent_id",
202
+ "agent_type",
203
+ "agent_name",
204
+ "agent_display_name",
205
+ "agent_description",
206
+ "agentName",
207
+ "agentDisplayName",
208
+ "agentDescription",
209
+ "error_context",
210
+ ),
211
+ config=HookConfig(
212
+ path=".github/hooks/memgraph-context-graph.json",
213
+ layout="flat",
214
+ version=1,
215
+ merge=True,
216
+ timeout_key="timeoutSec",
217
+ inject_event_name=True,
218
+ matchers={
219
+ "preToolUse": ".*",
220
+ "postToolUse": ".*",
221
+ "permissionRequest": ".*",
222
+ "subagentStart": ".*",
223
+ },
224
+ ),
225
+ probe_payload={"hook_event_name": "sessionEnd", "sessionId": "doctor", "reason": "complete"},
226
+ )
227
+
228
+
229
+ class CopilotCLIHooksAdapter(SpecAdapter):
230
+ """Convert Copilot CLI hook payloads into Event Protocol events."""
231
+
232
+ SPEC = SPEC
233
+
234
+
235
+ init = json_hook_installer(SPEC)
236
+ PLUGIN = SpecPlugin(SPEC, CopilotCLIHooksAdapter, init=init)
@@ -0,0 +1,189 @@
1
+ """Cursor command-hook runtime adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from memgraph_context_graph.adapters._spec import (
8
+ EventContext,
9
+ FieldMap,
10
+ HookConfig,
11
+ RuntimeSpec,
12
+ SpecAdapter,
13
+ SpecPlugin,
14
+ event_user_id,
15
+ json_hook_installer,
16
+ tool_end,
17
+ tool_start,
18
+ turn_end,
19
+ )
20
+ from memgraph_context_graph.events import (
21
+ AgentEndEvent,
22
+ AgentStartEvent,
23
+ MessageEvent,
24
+ SessionEndEvent,
25
+ SessionStartEvent,
26
+ ToolEndEvent,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from memgraph_context_graph.events import Event
31
+
32
+
33
+ def _session_start(context: EventContext) -> SessionStartEvent:
34
+ workspace_roots = context.payload.get("workspace_roots")
35
+ cwd = workspace_roots[0] if isinstance(workspace_roots, list) and workspace_roots else context.value("cwd")
36
+ return SessionStartEvent(
37
+ **context.base(),
38
+ model=context.optional_text("model"),
39
+ working_directory=None if cwd is None else str(cwd),
40
+ user_id=event_user_id(context),
41
+ )
42
+
43
+
44
+ def _session_end(context: EventContext) -> SessionEndEvent:
45
+ return SessionEndEvent(**context.base(), status=str(context.payload.get("reason") or "completed"))
46
+
47
+
48
+ def _user_message(context: EventContext) -> MessageEvent:
49
+ return MessageEvent(
50
+ **context.base(),
51
+ role="user",
52
+ content=context.value("prompt", ""),
53
+ model=context.optional_text("model"),
54
+ )
55
+
56
+
57
+ def _assistant_message(context: EventContext) -> MessageEvent:
58
+ return MessageEvent(
59
+ **context.base(),
60
+ role="assistant",
61
+ content=context.payload.get("text", ""),
62
+ model=context.optional_text("model"),
63
+ )
64
+
65
+
66
+ def _tool_failure(context: EventContext) -> ToolEndEvent:
67
+ return ToolEndEvent(
68
+ **context.base(),
69
+ tool_name=context.text("tool_name"),
70
+ tool_use_id=context.optional_text("tool_use_id"),
71
+ is_error=True,
72
+ error_message=str(context.payload.get("error_message") or "Cursor tool failed"),
73
+ )
74
+
75
+
76
+ def _agent_start(context: EventContext) -> AgentStartEvent:
77
+ agent_type = context.text("agent_type")
78
+ # Cursor's stop payload omits subagent_id. Use subagent_type as the common
79
+ # lifecycle key and preserve the start-only ID in metadata.
80
+ return AgentStartEvent(
81
+ **context.base(),
82
+ agent_name=agent_type,
83
+ agent_type=agent_type,
84
+ )
85
+
86
+
87
+ def _agent_end(context: EventContext) -> AgentEndEvent:
88
+ agent_type = context.text("agent_type")
89
+ return AgentEndEvent(
90
+ **context.base(),
91
+ agent_name=agent_type,
92
+ agent_type=agent_type,
93
+ output=str(context.payload.get("summary")) if context.payload.get("summary") is not None else None,
94
+ )
95
+
96
+
97
+ def _turn_end(context: EventContext) -> list[Event]:
98
+ # stop ends one agent loop; sessionEnd ends the conversation.
99
+ return turn_end(context, reason=context.payload.get("status"))
100
+
101
+
102
+ SPEC = RuntimeSpec(
103
+ name="cursor",
104
+ source_sdk="cursor",
105
+ event_key="hook_event_name",
106
+ hooks=(
107
+ "sessionStart",
108
+ "sessionEnd",
109
+ "beforeSubmitPrompt",
110
+ "preToolUse",
111
+ "postToolUse",
112
+ "postToolUseFailure",
113
+ "subagentStart",
114
+ "subagentStop",
115
+ "afterAgentResponse",
116
+ "stop",
117
+ ),
118
+ rules={
119
+ "sessionStart": _session_start,
120
+ "sessionEnd": _session_end,
121
+ "beforeSubmitPrompt": _user_message,
122
+ "preToolUse": tool_start,
123
+ "postToolUse": tool_end,
124
+ "postToolUseFailure": _tool_failure,
125
+ "subagentStart": _agent_start,
126
+ "subagentStop": _agent_end,
127
+ "afterAgentResponse": _assistant_message,
128
+ "stop": _turn_end,
129
+ },
130
+ metadata_keys=(
131
+ "generation_id",
132
+ "model_id",
133
+ "model_params",
134
+ "cursor_version",
135
+ "workspace_roots",
136
+ "user_email",
137
+ "transcript_path",
138
+ "tool_name",
139
+ "tool_input",
140
+ "tool_use_id",
141
+ "duration",
142
+ "failure_type",
143
+ "is_interrupt",
144
+ "task",
145
+ "subagent_id",
146
+ "subagent_type",
147
+ "description",
148
+ "status",
149
+ "duration_ms",
150
+ "agent_transcript_path",
151
+ ),
152
+ config=HookConfig(
153
+ path=".cursor/hooks.json",
154
+ layout="flat",
155
+ version=1,
156
+ merge=True,
157
+ timeout_key="timeout",
158
+ matchers={
159
+ "preToolUse": "*",
160
+ "postToolUse": "*",
161
+ "postToolUseFailure": "*",
162
+ "subagentStart": "*",
163
+ "subagentStop": "*",
164
+ },
165
+ ),
166
+ fields=FieldMap(session_id=("conversation_id", "session_id")),
167
+ # Cursor blocks a permission hook whose output doesn't match its schema, so
168
+ # these hooks must answer. "allow" is the weakest vote: any other hook's
169
+ # "deny" or "ask" still wins, so capture never loosens the user's policy.
170
+ responses={
171
+ "preToolUse": {"permission": "allow"},
172
+ "subagentStart": {"permission": "allow"},
173
+ "beforeSubmitPrompt": {"continue": True},
174
+ },
175
+ # Grok Build also runs .cursor/hooks.json hooks, with its own payloads
176
+ # (camelCase duplicates of every field); its grok runtime records those.
177
+ foreign_payload_keys=frozenset({"hookEventName"}),
178
+ probe_payload={"hook_event_name": "sessionEnd", "session_id": "doctor", "reason": "completed"},
179
+ )
180
+
181
+
182
+ class CursorHooksAdapter(SpecAdapter):
183
+ """Convert Cursor hook payloads into Event Protocol events."""
184
+
185
+ SPEC = SPEC
186
+
187
+
188
+ init = json_hook_installer(SPEC)
189
+ PLUGIN = SpecPlugin(SPEC, CursorHooksAdapter, init=init)
@@ -0,0 +1,187 @@
1
+ """Grok Build (``grok``) command-hook runtime adapter.
2
+
3
+ Grok sends each payload field in both camelCase and Claude Code's snake_case,
4
+ so the shared field aliases resolve most of it; only its typed tool results
5
+ and per-turn ``Stop`` need Grok-specific rules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from memgraph_context_graph.adapters._spec import (
13
+ EventContext,
14
+ FieldMap,
15
+ HookConfig,
16
+ RuntimeSpec,
17
+ SpecAdapter,
18
+ SpecPlugin,
19
+ json_hook_installer,
20
+ session_start,
21
+ tool_start,
22
+ turn_end,
23
+ )
24
+ from memgraph_context_graph.events import (
25
+ AgentEndEvent,
26
+ AgentStartEvent,
27
+ ErrorOccurredEvent,
28
+ MessageEvent,
29
+ SessionEndEvent,
30
+ ToolEndEvent,
31
+ )
32
+
33
+ if TYPE_CHECKING:
34
+ from memgraph_context_graph.events import Event
35
+
36
+
37
+ def _user_prompt(context: EventContext) -> MessageEvent:
38
+ return MessageEvent(**context.base(), role="user", content=context.value("prompt", ""))
39
+
40
+
41
+ def _tool_result(tool_response: Any) -> tuple[Any, bool]:
42
+ """Return a typed Grok tool result's model-facing text and whether it failed.
43
+
44
+ Results are tagged by ``type``: shell results carry ``output_for_prompt``
45
+ and ``exit_code``, file reads carry ``FileContent.content``. Other types
46
+ are kept whole.
47
+ """
48
+ if not isinstance(tool_response, dict):
49
+ return tool_response, False
50
+ exit_code = tool_response.get("exit_code")
51
+ text = tool_response.get("output_for_prompt")
52
+ if text is None:
53
+ file_content = tool_response.get("FileContent")
54
+ text = file_content.get("content") if isinstance(file_content, dict) else None
55
+ return (tool_response if text is None else text), exit_code not in (None, 0)
56
+
57
+
58
+ def _tool_end(context: EventContext) -> ToolEndEvent:
59
+ result, is_error = _tool_result(context.value("tool_result"))
60
+ return ToolEndEvent(
61
+ **context.base(),
62
+ tool_name=context.text("tool_name"),
63
+ tool_use_id=context.optional_text("tool_use_id"),
64
+ result=result,
65
+ is_error=is_error,
66
+ agent_name=context.optional_text("agent_id"),
67
+ )
68
+
69
+
70
+ def _tool_failure(context: EventContext) -> ToolEndEvent:
71
+ return ToolEndEvent(
72
+ **context.base(),
73
+ tool_name=context.text("tool_name"),
74
+ tool_use_id=context.optional_text("tool_use_id"),
75
+ is_error=True,
76
+ error_message=str(context.payload.get("error") or "Grok tool failed"),
77
+ agent_name=context.optional_text("agent_id"),
78
+ )
79
+
80
+
81
+ def _permission_denied(context: EventContext) -> ErrorOccurredEvent:
82
+ return ErrorOccurredEvent(
83
+ **context.base(),
84
+ error_type="permission_denied",
85
+ error_message=str(context.payload.get("reason") or context.text("tool_name", "Permission denied")),
86
+ recoverable=True,
87
+ )
88
+
89
+
90
+ def _turn_end(context: EventContext) -> list[Event]:
91
+ # Stop fires at the end of every turn (reason "end_turn") and once more at
92
+ # shutdown, right after SessionEnd; only the former ends a turn.
93
+ reason = context.payload.get("reason")
94
+ if reason == "shutdown":
95
+ return []
96
+ reply = context.payload.get("lastAssistantMessage", context.payload.get("last_assistant_message"))
97
+ return turn_end(context, reply=reply, reason=reason)
98
+
99
+
100
+ def _stop_failure(context: EventContext) -> ErrorOccurredEvent:
101
+ error = str(context.payload.get("error") or "unknown")
102
+ return ErrorOccurredEvent(
103
+ **context.base(),
104
+ error_type=error,
105
+ error_message=str(context.payload.get("reason") or error),
106
+ recoverable=True,
107
+ )
108
+
109
+
110
+ def _session_end(context: EventContext) -> SessionEndEvent:
111
+ return SessionEndEvent(**context.base(), status="completed")
112
+
113
+
114
+ def _agent_start(context: EventContext) -> AgentStartEvent:
115
+ agent_type = context.text("agent_type")
116
+ return AgentStartEvent(**context.base(), agent_name=context.text("agent_id", agent_type), agent_type=agent_type)
117
+
118
+
119
+ def _agent_end(context: EventContext) -> AgentEndEvent:
120
+ agent_type = context.text("agent_type")
121
+ return AgentEndEvent(
122
+ **context.base(),
123
+ agent_name=context.text("agent_id", agent_type),
124
+ agent_type=agent_type,
125
+ output=context.payload.get("lastAssistantMessage", context.payload.get("last_assistant_message")),
126
+ )
127
+
128
+
129
+ _HOOKS = (
130
+ "SessionStart",
131
+ "UserPromptSubmit",
132
+ "PreToolUse",
133
+ "PostToolUse",
134
+ "PostToolUseFailure",
135
+ "PermissionDenied",
136
+ "SubagentStart",
137
+ "SubagentStop",
138
+ "Stop",
139
+ "StopFailure",
140
+ "SessionEnd",
141
+ )
142
+ SPEC = RuntimeSpec(
143
+ name="grok",
144
+ source_sdk="grok",
145
+ event_key="hook_event_name",
146
+ hooks=_HOOKS,
147
+ rules={
148
+ "SessionStart": session_start,
149
+ "UserPromptSubmit": _user_prompt,
150
+ "PreToolUse": tool_start,
151
+ "PostToolUse": _tool_end,
152
+ "PostToolUseFailure": _tool_failure,
153
+ "PermissionDenied": _permission_denied,
154
+ "SubagentStart": _agent_start,
155
+ "SubagentStop": _agent_end,
156
+ "Stop": _turn_end,
157
+ "StopFailure": _stop_failure,
158
+ "SessionEnd": _session_end,
159
+ },
160
+ metadata_keys=(
161
+ "cwd",
162
+ "workspaceRoot",
163
+ "permissionMode",
164
+ "source",
165
+ "reason",
166
+ "promptId",
167
+ "transcriptPath",
168
+ "durationMs",
169
+ "isBackgrounded",
170
+ "toolInputTruncated",
171
+ "toolResultTruncated",
172
+ "stopHookActive",
173
+ ),
174
+ config=HookConfig(path=".grok/hooks/memgraph-context-graph.json", layout="nested", merge=True),
175
+ fields=FieldMap(tool_result=("tool_response", "toolResult")),
176
+ probe_payload={"hook_event_name": "SessionEnd", "session_id": "doctor", "reason": "shutdown"},
177
+ )
178
+
179
+
180
+ class GrokHooksAdapter(SpecAdapter):
181
+ """Convert Grok Build hook payloads into Event Protocol events."""
182
+
183
+ SPEC = SPEC
184
+
185
+
186
+ init = json_hook_installer(SPEC)
187
+ PLUGIN = SpecPlugin(SPEC, GrokHooksAdapter, init=init)