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.
- memgraph_context_graph/__init__.py +71 -0
- memgraph_context_graph/adapters/__init__.py +0 -0
- memgraph_context_graph/adapters/_identity.py +520 -0
- memgraph_context_graph/adapters/_opencode_plugin.js +112 -0
- memgraph_context_graph/adapters/_spec.py +441 -0
- memgraph_context_graph/adapters/antigravity_cli.py +152 -0
- memgraph_context_graph/adapters/claude.py +297 -0
- memgraph_context_graph/adapters/claude_code.py +216 -0
- memgraph_context_graph/adapters/claude_code_memory.py +181 -0
- memgraph_context_graph/adapters/codex.py +118 -0
- memgraph_context_graph/adapters/codex_memory.py +185 -0
- memgraph_context_graph/adapters/copilot_cli.py +236 -0
- memgraph_context_graph/adapters/cursor.py +189 -0
- memgraph_context_graph/adapters/grok.py +187 -0
- memgraph_context_graph/adapters/openai.py +240 -0
- memgraph_context_graph/adapters/opencode.py +168 -0
- memgraph_context_graph/cli.py +845 -0
- memgraph_context_graph/events.py +210 -0
- memgraph_context_graph/hooks/__init__.py +1 -0
- memgraph_context_graph/hooks/cli.py +184 -0
- memgraph_context_graph/hooks/runner.py +311 -0
- memgraph_context_graph/hooks/runtime_plugin.py +85 -0
- memgraph_context_graph/link.py +50 -0
- memgraph_context_graph/mcp_server.py +102 -0
- memgraph_context_graph/memory_backend.py +144 -0
- memgraph_context_graph/protocols.py +67 -0
- memgraph_context_graph/spaces.py +214 -0
- memgraph_context_graph/tools.py +130 -0
- memgraph_context_graph-0.5.0.dist-info/METADATA +718 -0
- memgraph_context_graph-0.5.0.dist-info/RECORD +33 -0
- memgraph_context_graph-0.5.0.dist-info/WHEEL +4 -0
- memgraph_context_graph-0.5.0.dist-info/entry_points.txt +11 -0
- 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)
|