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,152 @@
1
+ """Antigravity CLI (``agy``) command-hook runtime adapter.
2
+
3
+ Antigravity hooks expose less than other runtimes: tool calls and their
4
+ errors, model-invocation counters, and execution stops. They carry no tool
5
+ results, prompts, assistant text, or token usage, so an Antigravity session
6
+ records what ran but not what came back.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from memgraph_context_graph.adapters._spec import (
14
+ EventContext,
15
+ FieldMap,
16
+ HookConfig,
17
+ RuntimeSpec,
18
+ SpecAdapter,
19
+ SpecPlugin,
20
+ event_user_id,
21
+ json_hook_installer,
22
+ string_or_none,
23
+ turn_end,
24
+ )
25
+ from memgraph_context_graph.events import ErrorOccurredEvent, SessionStartEvent, ToolEndEvent, ToolStartEvent
26
+
27
+ if TYPE_CHECKING:
28
+ from memgraph_context_graph.events import Event
29
+
30
+
31
+ def _workspace(context: EventContext) -> str | None:
32
+ paths = context.payload.get("workspacePaths")
33
+ return str(paths[0]) if isinstance(paths, list) and paths else None
34
+
35
+
36
+ def _tool_call(context: EventContext) -> dict[str, Any]:
37
+ tool_call = context.payload.get("toolCall")
38
+ return tool_call if isinstance(tool_call, dict) else {}
39
+
40
+
41
+ def _step_id(context: EventContext) -> str | None:
42
+ # No tool-call id exists; the trajectory step index is shared by a call's
43
+ # PreToolUse and PostToolUse, so it pairs them within one conversation.
44
+ step = context.payload.get("stepIdx")
45
+ return None if step is None else f"step-{step}"
46
+
47
+
48
+ def _session_start(context: EventContext) -> SessionStartEvent | None:
49
+ # No session-start hook exists. Each turn's first model invocation
50
+ # (invocationNum resets per turn) stands in for it; connectors ignore a
51
+ # start for a session they already hold.
52
+ if context.payload.get("invocationNum") != 0:
53
+ return None
54
+ return SessionStartEvent(
55
+ **context.base(),
56
+ model=string_or_none(context.payload.get("modelName")),
57
+ working_directory=_workspace(context),
58
+ user_id=event_user_id(context),
59
+ )
60
+
61
+
62
+ def _tool_start(context: EventContext) -> ToolStartEvent:
63
+ tool_call = _tool_call(context)
64
+ return ToolStartEvent(
65
+ **context.base(),
66
+ tool_name=str(tool_call.get("name") or ""),
67
+ tool_input=tool_call.get("args"),
68
+ tool_use_id=_step_id(context),
69
+ )
70
+
71
+
72
+ def _tool_end(context: EventContext) -> ToolEndEvent:
73
+ error = string_or_none(context.payload.get("error")) or None
74
+ return ToolEndEvent(
75
+ **context.base(),
76
+ tool_name=str(_tool_call(context).get("name") or ""),
77
+ tool_use_id=_step_id(context),
78
+ is_error=error is not None,
79
+ error_message=error,
80
+ )
81
+
82
+
83
+ def _stop(context: EventContext) -> list[Event]:
84
+ # Stop ends one execution loop. agy has no session-end hook, so a loop that
85
+ # leaves the agent fully idle is the turn end; a failed one is also an error.
86
+ events: list[Event] = []
87
+ error = context.payload.get("error")
88
+ reason = context.payload.get("terminationReason")
89
+ if error:
90
+ events.append(
91
+ ErrorOccurredEvent(
92
+ **context.base(),
93
+ error_type=str(reason or "antigravity_error"),
94
+ error_message=str(error),
95
+ recoverable=True,
96
+ )
97
+ )
98
+ if context.payload.get("fullyIdle"):
99
+ events.extend(turn_end(context, reason=reason))
100
+ return events
101
+
102
+
103
+ _HOOKS = ("PreInvocation", "PreToolUse", "PostToolUse", "Stop")
104
+ SPEC = RuntimeSpec(
105
+ name="antigravity-cli",
106
+ source_sdk="antigravity-cli",
107
+ event_key="hook_event_name",
108
+ hooks=_HOOKS,
109
+ rules={
110
+ "PreInvocation": _session_start,
111
+ "PreToolUse": _tool_start,
112
+ "PostToolUse": _tool_end,
113
+ "Stop": _stop,
114
+ },
115
+ metadata_keys=(
116
+ "workspacePaths",
117
+ "transcriptPath",
118
+ "artifactDirectoryPath",
119
+ "modelName",
120
+ "stepIdx",
121
+ "invocationNum",
122
+ "executionNum",
123
+ "terminationReason",
124
+ "fullyIdle",
125
+ ),
126
+ # hooks.json is keyed by named hook groups; this runtime owns one group.
127
+ # Only tool events take matcher groups: agy rejects the whole file if a
128
+ # lifecycle event is nested, so those are written as flat handlers.
129
+ config=HookConfig(
130
+ path=".agents/hooks.json",
131
+ layout="nested",
132
+ root_key="memgraph-context-graph",
133
+ merge=True,
134
+ inject_event_name=True,
135
+ flat_hooks=frozenset({"PreInvocation", "Stop"}),
136
+ ),
137
+ fields=FieldMap(session_id=("conversationId",)),
138
+ # PostToolUse requires an object; an empty PreToolUse answer leaves agy's
139
+ # own permission prompt in charge, so capture never approves a tool.
140
+ responses={"PostToolUse": {}},
141
+ probe_payload={"hook_event_name": "PreInvocation", "conversationId": "doctor", "invocationNum": 0},
142
+ )
143
+
144
+
145
+ class AntigravityCLIHooksAdapter(SpecAdapter):
146
+ """Convert Antigravity CLI hook payloads into Event Protocol events."""
147
+
148
+ SPEC = SPEC
149
+
150
+
151
+ init = json_hook_installer(SPEC)
152
+ PLUGIN = SpecPlugin(SPEC, AntigravityCLIHooksAdapter, init=init)
@@ -0,0 +1,297 @@
1
+ """Claude Agent SDK adapter for memgraph-context-graph.
2
+
3
+ Translates Claude Agent SDK hook callbacks into the common Event protocol
4
+ and forwards them to the AgentLink hub.
5
+
6
+ Note: this adapter only wires up the ``hooks={...}`` callback API. The SDK's
7
+ separate message-stream API (``query()``/``ClaudeSDKClient.receive_response()``,
8
+ yielding ``UserMessage``/``AssistantMessage``) carries a genuine
9
+ ``parent_tool_use_id`` field that would let a subagent's spawning tool call be
10
+ linked directly rather than inferred -- verified empirically, not just in
11
+ docs. Deliberately not consumed here: doing so would make this one adapter
12
+ strictly better than the other three (Claude Code, Codex, OpenAI Agents SDK),
13
+ which have no equivalent field and stay on the general temporal/agent_type
14
+ inference rule uniformly. Revisit only if that asymmetry becomes worth
15
+ taking on.
16
+
17
+ Usage::
18
+
19
+ from memgraph_context_graph import AgentLink
20
+ from memgraph_context_graph.adapters.claude import ClaudeAdapter
21
+
22
+ link = AgentLink()
23
+ adapter = ClaudeAdapter(link, session_id="s-1")
24
+ hooks = adapter.get_runtime_hooks()
25
+
26
+ # Pass hooks to ClaudeAgentOptions
27
+ options = ClaudeAgentOptions(hooks=hooks)
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from typing import TYPE_CHECKING, Any, cast
33
+
34
+ from memgraph_context_graph.events import (
35
+ AgentEndEvent,
36
+ AgentStartEvent,
37
+ ErrorOccurredEvent,
38
+ MessageEvent,
39
+ SessionEndEvent,
40
+ SessionStartEvent,
41
+ ToolEndEvent,
42
+ ToolStartEvent,
43
+ )
44
+ from memgraph_context_graph.protocols import RuntimeAdapter
45
+
46
+ if TYPE_CHECKING:
47
+ from memgraph_context_graph.link import AgentLink
48
+
49
+ _SOURCE = "claude"
50
+
51
+
52
+ class ClaudeAdapter(RuntimeAdapter):
53
+ """Adapter that converts Claude Agent SDK hooks into memgraph-context-graph events.
54
+
55
+ Args:
56
+ link: The AgentLink hub to emit events to.
57
+ session_id: Session identifier for all events.
58
+ auto_session: If ``True``, emit SessionStartEvent on creation.
59
+ session_kwargs: Extra fields for SessionStartEvent (model, …).
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ link: AgentLink,
65
+ session_id: str,
66
+ *,
67
+ auto_session: bool = True,
68
+ session_kwargs: dict[str, Any] | None = None,
69
+ ) -> None:
70
+ self._link = link
71
+ self._session_id = session_id
72
+ self._tool_use_to_action: dict[str, str] = {}
73
+
74
+ if auto_session:
75
+ kw = session_kwargs or {}
76
+ self._link.emit(
77
+ SessionStartEvent(
78
+ session_id=session_id,
79
+ source_sdk=_SOURCE,
80
+ model=kw.get("model"),
81
+ working_directory=kw.get("working_directory"),
82
+ user_id=kw.get("user_id"),
83
+ metadata=kw.get("metadata", {}),
84
+ )
85
+ )
86
+
87
+ # ------------------------------------------------------------------
88
+ # Runtime adapter interface
89
+ # ------------------------------------------------------------------
90
+
91
+ def get_runtime_hooks(self) -> dict[str, list[Any]]:
92
+ """Return a hooks dict suitable for ``ClaudeAgentOptions(hooks=...)``."""
93
+ try:
94
+ from claude_agent_sdk import HookMatcher
95
+ except ImportError:
96
+
97
+ class HookMatcher: # type: ignore[no-redef]
98
+ def __init__(
99
+ self,
100
+ matcher: str | None = None,
101
+ hooks: list[Any] | None = None,
102
+ timeout: float | None = None,
103
+ ):
104
+ self.matcher = matcher
105
+ self.hooks = hooks or []
106
+ self.timeout = timeout
107
+
108
+ # Our hook callbacks return a permissive dict[str, Any] rather than the
109
+ # SDK's precise AsyncHookJSONOutput/SyncHookJSONOutput shapes, matching
110
+ # the try/except HookMatcher shim above: this module treats the SDK as
111
+ # optional/duck-typed rather than taking a hard type dependency on it.
112
+ return {
113
+ "PreToolUse": [HookMatcher(hooks=[cast("Any", self._pre_tool_use)])],
114
+ "PostToolUse": [HookMatcher(hooks=[cast("Any", self._post_tool_use)])],
115
+ "PostToolUseFailure": [HookMatcher(hooks=[cast("Any", self._post_tool_use_failure)])],
116
+ "UserPromptSubmit": [HookMatcher(hooks=[cast("Any", self._user_prompt_submit)])],
117
+ "SubagentStart": [HookMatcher(hooks=[cast("Any", self._subagent_start)])],
118
+ "SubagentStop": [HookMatcher(hooks=[cast("Any", self._subagent_stop)])],
119
+ "Notification": [HookMatcher(hooks=[cast("Any", self._notification)])],
120
+ "Stop": [HookMatcher(hooks=[cast("Any", self._stop)])],
121
+ }
122
+
123
+ # ------------------------------------------------------------------
124
+ # Hook callbacks (async, matching Claude SDK signature)
125
+ # ------------------------------------------------------------------
126
+
127
+ async def _pre_tool_use(
128
+ self,
129
+ input_data: dict[str, Any],
130
+ tool_use_id: str | None,
131
+ _context: dict[str, Any],
132
+ ) -> dict[str, Any]:
133
+ tool_name = input_data.get("tool_name", "")
134
+ tool_input = input_data.get("tool_input", {})
135
+ actual_id = input_data.get("tool_use_id") or tool_use_id
136
+
137
+ self._link.emit(
138
+ ToolStartEvent(
139
+ session_id=self._session_id,
140
+ source_sdk=_SOURCE,
141
+ tool_name=tool_name,
142
+ tool_input=tool_input,
143
+ tool_use_id=actual_id,
144
+ agent_name=input_data.get("agent_id"),
145
+ metadata={k: input_data.get(k) for k in ("agent_type", "cwd") if input_data.get(k) is not None},
146
+ )
147
+ )
148
+ return {}
149
+
150
+ async def _post_tool_use(
151
+ self,
152
+ input_data: dict[str, Any],
153
+ tool_use_id: str | None,
154
+ _context: dict[str, Any],
155
+ ) -> dict[str, Any]:
156
+ tool_name = input_data.get("tool_name", "")
157
+ actual_id = input_data.get("tool_use_id") or tool_use_id
158
+ tool_response = input_data.get("tool_response")
159
+
160
+ is_error = False
161
+ error_message = None
162
+ content = tool_response
163
+ if isinstance(tool_response, dict):
164
+ is_error = tool_response.get("is_error", False)
165
+ error_message = tool_response.get("error")
166
+ content = tool_response.get("content", tool_response)
167
+
168
+ self._link.emit(
169
+ ToolEndEvent(
170
+ session_id=self._session_id,
171
+ source_sdk=_SOURCE,
172
+ tool_name=tool_name,
173
+ tool_use_id=actual_id,
174
+ result=content,
175
+ is_error=is_error,
176
+ error_message=error_message,
177
+ agent_name=input_data.get("agent_id"),
178
+ )
179
+ )
180
+ return {}
181
+
182
+ async def _post_tool_use_failure(
183
+ self,
184
+ input_data: dict[str, Any],
185
+ tool_use_id: str | None,
186
+ _context: dict[str, Any],
187
+ ) -> dict[str, Any]:
188
+ tool_name = input_data.get("tool_name", "")
189
+ actual_id = input_data.get("tool_use_id") or tool_use_id
190
+ error = input_data.get("error", "Unknown error")
191
+
192
+ self._link.emit(
193
+ ErrorOccurredEvent(
194
+ session_id=self._session_id,
195
+ source_sdk=_SOURCE,
196
+ error_type="tool_failure",
197
+ error_message=str(error),
198
+ error_details={
199
+ "tool_name": tool_name,
200
+ "tool_use_id": actual_id,
201
+ "tool_input": input_data.get("tool_input", {}),
202
+ "is_interrupt": input_data.get("is_interrupt", False),
203
+ },
204
+ recoverable=not input_data.get("is_interrupt", False),
205
+ )
206
+ )
207
+ return {}
208
+
209
+ async def _user_prompt_submit(
210
+ self,
211
+ input_data: dict[str, Any],
212
+ _tool_use_id: str | None,
213
+ _context: dict[str, Any],
214
+ ) -> dict[str, Any]:
215
+ self._link.emit(
216
+ MessageEvent(
217
+ session_id=self._session_id,
218
+ source_sdk=_SOURCE,
219
+ role="user",
220
+ content=input_data.get("prompt", ""),
221
+ agent_name=input_data.get("agent_id"),
222
+ metadata={k: input_data.get(k) for k in ("cwd", "permission_mode") if input_data.get(k) is not None},
223
+ )
224
+ )
225
+ return {}
226
+
227
+ async def _subagent_start(
228
+ self,
229
+ input_data: dict[str, Any],
230
+ _tool_use_id: str | None,
231
+ _context: dict[str, Any],
232
+ ) -> dict[str, Any]:
233
+ self._link.emit(
234
+ AgentStartEvent(
235
+ session_id=self._session_id,
236
+ source_sdk=_SOURCE,
237
+ agent_name=input_data.get("agent_id", ""),
238
+ agent_type=input_data.get("agent_type", ""),
239
+ )
240
+ )
241
+ return {}
242
+
243
+ async def _subagent_stop(
244
+ self,
245
+ input_data: dict[str, Any],
246
+ _tool_use_id: str | None,
247
+ _context: dict[str, Any],
248
+ ) -> dict[str, Any]:
249
+ self._link.emit(
250
+ AgentEndEvent(
251
+ session_id=self._session_id,
252
+ source_sdk=_SOURCE,
253
+ agent_name=input_data.get("agent_id", ""),
254
+ agent_type=input_data.get("agent_type", ""),
255
+ metadata={
256
+ "stop_hook_active": input_data.get("stop_hook_active"),
257
+ "agent_transcript_path": input_data.get("agent_transcript_path"),
258
+ },
259
+ )
260
+ )
261
+ return {}
262
+
263
+ async def _notification(
264
+ self,
265
+ input_data: dict[str, Any],
266
+ _tool_use_id: str | None,
267
+ _context: dict[str, Any],
268
+ ) -> dict[str, Any]:
269
+ self._link.emit(
270
+ MessageEvent(
271
+ session_id=self._session_id,
272
+ source_sdk=_SOURCE,
273
+ role="system",
274
+ content=input_data.get("message", ""),
275
+ agent_name=input_data.get("agent_id"),
276
+ metadata={
277
+ "title": input_data.get("title"),
278
+ "notification_type": input_data.get("notification_type"),
279
+ },
280
+ )
281
+ )
282
+ return {}
283
+
284
+ async def _stop(
285
+ self,
286
+ _input_data: dict[str, Any],
287
+ _tool_use_id: str | None,
288
+ _context: dict[str, Any],
289
+ ) -> dict[str, Any]:
290
+ self._link.emit(
291
+ SessionEndEvent(
292
+ session_id=self._session_id,
293
+ source_sdk=_SOURCE,
294
+ status="completed",
295
+ )
296
+ )
297
+ return {}
@@ -0,0 +1,216 @@
1
+ """Claude Code command-hook runtime adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from memgraph_context_graph.adapters._spec import (
8
+ EventContext,
9
+ HookConfig,
10
+ RuntimeSpec,
11
+ SpecAdapter,
12
+ SpecPlugin,
13
+ pre_tool_use_context,
14
+ session_start,
15
+ tool_start,
16
+ turn_end,
17
+ )
18
+ from memgraph_context_graph.adapters.claude_code_memory import CLAUDE_CODE_MEMORY
19
+ from memgraph_context_graph.events import (
20
+ AgentEndEvent,
21
+ AgentStartEvent,
22
+ ErrorOccurredEvent,
23
+ MessageEvent,
24
+ SessionEndEvent,
25
+ ToolEndEvent,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from memgraph_context_graph.events import Event
30
+
31
+
32
+ def _user_prompt(context: EventContext) -> MessageEvent:
33
+ return MessageEvent(
34
+ **context.base(),
35
+ role="user",
36
+ content=context.value("prompt", ""),
37
+ agent_name=context.optional_text("agent_id"),
38
+ )
39
+
40
+
41
+ def _tool_result_text(tool_response: Any) -> Any:
42
+ """Return the text of a Claude Code tool response, or the response unchanged.
43
+
44
+ Bash responses carry ``stdout``/``stderr``, and Read responses carry
45
+ ``file.content``; other tools' structured responses are kept whole.
46
+ """
47
+ if not isinstance(tool_response, dict):
48
+ return tool_response
49
+ if isinstance(tool_response.get("stdout"), str):
50
+ stderr = tool_response.get("stderr")
51
+ return f"{tool_response['stdout']}\n{stderr}" if stderr else tool_response["stdout"]
52
+ file = tool_response.get("file")
53
+ if isinstance(file, dict) and isinstance(file.get("content"), str):
54
+ return file["content"]
55
+ return tool_response
56
+
57
+
58
+ def _tool_end(context: EventContext) -> ToolEndEvent:
59
+ return ToolEndEvent(
60
+ **context.base(),
61
+ tool_name=context.text("tool_name"),
62
+ tool_use_id=context.optional_text("tool_use_id"),
63
+ result=_tool_result_text(context.value("tool_result")),
64
+ agent_name=context.optional_text("agent_id"),
65
+ )
66
+
67
+
68
+ def _tool_failure(context: EventContext) -> ToolEndEvent:
69
+ return ToolEndEvent(
70
+ **context.base(),
71
+ tool_name=context.text("tool_name"),
72
+ tool_use_id=context.optional_text("tool_use_id"),
73
+ is_error=True,
74
+ error_message=context.optional_text("error"),
75
+ agent_name=context.optional_text("agent_id"),
76
+ )
77
+
78
+
79
+ def _permission(context: EventContext) -> MessageEvent:
80
+ return MessageEvent(
81
+ **context.base(),
82
+ role="system",
83
+ content=context.text("tool_name", "permission_request"),
84
+ agent_name=context.optional_text("agent_id"),
85
+ )
86
+
87
+
88
+ def _permission_denied(context: EventContext) -> ErrorOccurredEvent:
89
+ return ErrorOccurredEvent(
90
+ **context.base(),
91
+ error_type="permission_denied",
92
+ error_message=str(context.payload.get("reason") or "Permission denied"),
93
+ recoverable=True,
94
+ )
95
+
96
+
97
+ def _agent_start(context: EventContext) -> AgentStartEvent:
98
+ agent_type = context.text("agent_type")
99
+ return AgentStartEvent(
100
+ **context.base(),
101
+ agent_name=context.text("agent_id", agent_type),
102
+ agent_type=agent_type,
103
+ )
104
+
105
+
106
+ def _agent_end(context: EventContext) -> AgentEndEvent:
107
+ agent_type = context.text("agent_type")
108
+ return AgentEndEvent(
109
+ **context.base(),
110
+ agent_name=context.text("agent_id", agent_type),
111
+ agent_type=agent_type,
112
+ output=context.payload.get("last_assistant_message"),
113
+ )
114
+
115
+
116
+ def _turn_end(context: EventContext) -> list[Event]:
117
+ # Stop ends one turn; SessionEnd, registered separately, ends the session.
118
+ return turn_end(context, reply=context.payload.get("last_assistant_message"))
119
+
120
+
121
+ def _session_end(context: EventContext) -> SessionEndEvent:
122
+ return SessionEndEvent(**context.base(), status="completed")
123
+
124
+
125
+ def _stop_failure(context: EventContext) -> ErrorOccurredEvent:
126
+ error = str(context.payload.get("error") or "unknown")
127
+ return ErrorOccurredEvent(
128
+ **context.base(),
129
+ error_type=error,
130
+ error_message=str(context.payload.get("error_details") or error or "Claude Code stop failure"),
131
+ recoverable=True,
132
+ )
133
+
134
+
135
+ _HOOKS = (
136
+ "SessionStart",
137
+ "UserPromptSubmit",
138
+ "UserPromptExpansion",
139
+ "PreToolUse",
140
+ "PostToolUse",
141
+ "PostToolUseFailure",
142
+ "PermissionRequest",
143
+ "PermissionDenied",
144
+ "SubagentStart",
145
+ "SubagentStop",
146
+ "Stop",
147
+ "StopFailure",
148
+ "SessionEnd",
149
+ )
150
+ SPEC = RuntimeSpec(
151
+ name="claude-code",
152
+ source_sdk="claude-code",
153
+ event_key="hook_event_name",
154
+ hooks=_HOOKS,
155
+ rules={
156
+ "SessionStart": session_start,
157
+ "UserPromptSubmit": _user_prompt,
158
+ "UserPromptExpansion": _user_prompt,
159
+ "PreToolUse": tool_start,
160
+ "PostToolUse": _tool_end,
161
+ "PostToolUseFailure": _tool_failure,
162
+ "PermissionRequest": _permission,
163
+ "PermissionDenied": _permission_denied,
164
+ "SubagentStart": _agent_start,
165
+ "SubagentStop": _agent_end,
166
+ "Stop": _turn_end,
167
+ "StopFailure": _stop_failure,
168
+ "SessionEnd": _session_end,
169
+ },
170
+ metadata_keys=(
171
+ "cwd",
172
+ "transcript_path",
173
+ "permission_mode",
174
+ "tool_name",
175
+ "tool_input",
176
+ "tool_use_id",
177
+ "tool_response",
178
+ "duration_ms",
179
+ "error",
180
+ "error_details",
181
+ "is_interrupt",
182
+ "reason",
183
+ "stop_hook_active",
184
+ "last_assistant_message",
185
+ "agent_id",
186
+ "agent_type",
187
+ "agent_transcript_path",
188
+ "command_name",
189
+ "command_args",
190
+ "command_source",
191
+ "expansion_type",
192
+ ),
193
+ # Installed by the Claude Code plugin's hooks.json, not a project-local file.
194
+ config=HookConfig(
195
+ layout="nested",
196
+ matchers={
197
+ hook: "*"
198
+ for hook in ("PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest", "PermissionDenied")
199
+ },
200
+ ),
201
+ responses={"Stop": {"continue": True}, "SubagentStop": {"continue": True}},
202
+ context_before_tool=pre_tool_use_context,
203
+ # Grok Build also runs .claude/settings.json hooks, with its own payloads
204
+ # (camelCase duplicates of every field); its grok runtime records those.
205
+ foreign_payload_keys=frozenset({"hookEventName"}),
206
+ probe_payload={"hook_event_name": "Stop", "session_id": "doctor"},
207
+ )
208
+
209
+
210
+ class ClaudeCodeHooksAdapter(SpecAdapter):
211
+ """Convert Claude Code command-hook payloads into Event Protocol events."""
212
+
213
+ SPEC = SPEC
214
+
215
+
216
+ PLUGIN = SpecPlugin(SPEC, ClaudeCodeHooksAdapter, native_memory=CLAUDE_CODE_MEMORY)