eagle-mem 4.10.1 → 4.10.3
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.
- package/CHANGELOG.md +130 -0
- package/README.md +20 -99
- package/architecture.html +40 -19
- package/hooks/pre-tool-use.sh +2 -6
- package/integrations/__pycache__/google_antigravity_hook.cpython-314.pyc +0 -0
- package/integrations/google_antigravity_hook.py +416 -0
- package/lib/codex-hooks.sh +17 -0
- package/lib/common.sh +8 -1
- package/lib/hooks-posttool.sh +10 -0
- package/lib/hooks.sh +17 -0
- package/package.json +5 -3
- package/scripts/grok-bootstrap.sh +26 -4
- package/scripts/install.sh +9 -1
- package/scripts/memories.sh +79 -1
- package/scripts/session.sh +2 -1
- package/scripts/uninstall.sh +7 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Google Antigravity (AGY) SDK integration module for Eagle Mem.
|
|
3
|
+
Enables Antigravity agents to automatically record session summaries, observations,
|
|
4
|
+
and tasks, enforce release-boundary guardrails, and survive context compaction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import inspect
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
# Setup dedicated logger
|
|
16
|
+
logging.basicConfig(level=logging.INFO)
|
|
17
|
+
logger = logging.getLogger("eagle_mem.antigravity")
|
|
18
|
+
|
|
19
|
+
# --- Import and Mock Fallback for google-antigravity SDK ---
|
|
20
|
+
try:
|
|
21
|
+
from google.antigravity import types
|
|
22
|
+
from google.antigravity.hooks import hooks
|
|
23
|
+
HAS_ANTIGRAVITY = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
HAS_ANTIGRAVITY = False
|
|
26
|
+
|
|
27
|
+
# Mock fallback to prevent import errors and allow standalone execution/testing
|
|
28
|
+
class MockHooksRegistry:
|
|
29
|
+
def on_session_start(self, func):
|
|
30
|
+
return func
|
|
31
|
+
def on_session_end(self, func):
|
|
32
|
+
return func
|
|
33
|
+
def pre_turn(self, func):
|
|
34
|
+
return func
|
|
35
|
+
def post_turn(self, func):
|
|
36
|
+
return func
|
|
37
|
+
def pre_tool_call_decide(self, func):
|
|
38
|
+
return func
|
|
39
|
+
def post_tool_call(self, func):
|
|
40
|
+
return func
|
|
41
|
+
def on_tool_error(self, func):
|
|
42
|
+
return func
|
|
43
|
+
def on_compaction(self, func):
|
|
44
|
+
return func
|
|
45
|
+
def on_interaction(self, func):
|
|
46
|
+
return func
|
|
47
|
+
|
|
48
|
+
async def inject_agent_context(self, context: str):
|
|
49
|
+
logger.info(f"[Mock SDK] Injected context:\n{context[:100]}...")
|
|
50
|
+
|
|
51
|
+
class MockTypes:
|
|
52
|
+
class HookResult:
|
|
53
|
+
def __init__(self, allow: bool = True):
|
|
54
|
+
self.allow = allow
|
|
55
|
+
class ToolCallResult:
|
|
56
|
+
def __init__(self, tool_call: Any, output: str):
|
|
57
|
+
self.tool_call = tool_call
|
|
58
|
+
self.output = output
|
|
59
|
+
class ToolCall:
|
|
60
|
+
def __init__(self, name: str, arguments: Dict[str, Any]):
|
|
61
|
+
self.name = name
|
|
62
|
+
self.arguments = arguments
|
|
63
|
+
|
|
64
|
+
hooks = MockHooksRegistry()
|
|
65
|
+
types = MockTypes()
|
|
66
|
+
|
|
67
|
+
# --- Asynchronous Subprocess Helpers ---
|
|
68
|
+
|
|
69
|
+
async def run_cmd_async(cmd: List[str]) -> str:
|
|
70
|
+
"""Runs a shell command asynchronously and returns stdout."""
|
|
71
|
+
# Prioritize workspace-local bin/eagle-mem if running from workspace or if it exists in cwd or relative to this script
|
|
72
|
+
if cmd and cmd[0] == "eagle-mem":
|
|
73
|
+
local_paths = [
|
|
74
|
+
os.path.join(os.getcwd(), "bin", "eagle-mem"),
|
|
75
|
+
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "bin", "eagle-mem")
|
|
76
|
+
]
|
|
77
|
+
for path in local_paths:
|
|
78
|
+
if os.path.exists(path):
|
|
79
|
+
cmd = [path] + cmd[1:]
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
proc = await asyncio.create_subprocess_exec(
|
|
84
|
+
*cmd,
|
|
85
|
+
stdout=asyncio.subprocess.PIPE,
|
|
86
|
+
stderr=asyncio.subprocess.PIPE
|
|
87
|
+
)
|
|
88
|
+
stdout, stderr = await proc.communicate()
|
|
89
|
+
if proc.returncode == 0:
|
|
90
|
+
return stdout.decode('utf-8', errors='ignore')
|
|
91
|
+
else:
|
|
92
|
+
err_msg = stderr.decode('utf-8', errors='ignore').strip()
|
|
93
|
+
logger.warning(f"Command {' '.join(cmd)} failed with code {proc.returncode}: {err_msg}")
|
|
94
|
+
return ""
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
# Fallback if command still fails (e.g. if local_bin substitution didn't happen or is broken)
|
|
97
|
+
logger.error(f"Executable not found for: {cmd[0]}")
|
|
98
|
+
return ""
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Error running {' '.join(cmd)}: {e}")
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
async def run_hook_async(script_name: str, input_data: Dict[str, Any]) -> str:
|
|
104
|
+
"""Runs an Eagle Mem bash hook script asynchronously, piping JSON via stdin."""
|
|
105
|
+
# Resolve the physical path of the hook script
|
|
106
|
+
# First check ~/.eagle-mem/hooks/, then fall back to workspace hooks/
|
|
107
|
+
home_dir = os.path.expanduser("~")
|
|
108
|
+
hook_path = os.path.join(home_dir, ".eagle-mem", "hooks", script_name)
|
|
109
|
+
if not os.path.exists(hook_path):
|
|
110
|
+
hook_path = os.path.join(os.getcwd(), "hooks", script_name)
|
|
111
|
+
|
|
112
|
+
if not os.path.exists(hook_path):
|
|
113
|
+
logger.error(f"Eagle Mem hook script not found: {script_name}")
|
|
114
|
+
return ""
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
proc = await asyncio.create_subprocess_exec(
|
|
118
|
+
"bash", hook_path,
|
|
119
|
+
stdin=asyncio.subprocess.PIPE,
|
|
120
|
+
stdout=asyncio.subprocess.PIPE,
|
|
121
|
+
stderr=asyncio.subprocess.PIPE
|
|
122
|
+
)
|
|
123
|
+
input_json = json.dumps(input_data).encode('utf-8')
|
|
124
|
+
stdout, stderr = await proc.communicate(input=input_json)
|
|
125
|
+
if proc.returncode == 0:
|
|
126
|
+
return stdout.decode('utf-8', errors='ignore')
|
|
127
|
+
else:
|
|
128
|
+
err_msg = stderr.decode('utf-8', errors='ignore').strip()
|
|
129
|
+
logger.warning(f"Hook script {script_name} failed with code {proc.returncode}: {err_msg}")
|
|
130
|
+
return ""
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.error(f"Error executing hook {script_name}: {e}")
|
|
133
|
+
return ""
|
|
134
|
+
|
|
135
|
+
# --- Utility Helpers ---
|
|
136
|
+
|
|
137
|
+
def get_session_id() -> str:
|
|
138
|
+
"""Retrieves or generates a persistent session ID for the current context."""
|
|
139
|
+
# Use environment variables if present (matches Claude/Codex)
|
|
140
|
+
session_id = os.environ.get("EAGLE_SESSION_ID") or os.environ.get("SESSION_ID")
|
|
141
|
+
if not session_id:
|
|
142
|
+
# Generate a unique hex session ID
|
|
143
|
+
import uuid
|
|
144
|
+
session_id = f"agy-{uuid.uuid4().hex[:16]}"
|
|
145
|
+
return session_id
|
|
146
|
+
|
|
147
|
+
def map_tool_name(agy_name: str) -> str:
|
|
148
|
+
"""Maps Google Antigravity SDK tool names to Eagle Mem canonical tool names."""
|
|
149
|
+
name = agy_name.lower()
|
|
150
|
+
if "command" in name or "bash" in name or "shell" in name:
|
|
151
|
+
return "Bash"
|
|
152
|
+
elif "read" in name or "view" in name:
|
|
153
|
+
return "Read"
|
|
154
|
+
elif "write" in name or "create" in name:
|
|
155
|
+
return "Write"
|
|
156
|
+
elif "edit" in name or "replace" in name or "patch" in name:
|
|
157
|
+
return "Edit"
|
|
158
|
+
return agy_name
|
|
159
|
+
|
|
160
|
+
# --- Eagle Mem Antigravity Hook Implementation ---
|
|
161
|
+
|
|
162
|
+
class EagleMemAntigravityHook:
|
|
163
|
+
"""
|
|
164
|
+
Natively compatible Google Antigravity SDK lifecycle hooks class for Eagle Mem.
|
|
165
|
+
Translates and routes events directly to Eagle Mem's native bash scripts.
|
|
166
|
+
"""
|
|
167
|
+
def __init__(self, agent_name: str = "antigravity"):
|
|
168
|
+
self.agent_name = agent_name
|
|
169
|
+
|
|
170
|
+
async def on_session_start(self):
|
|
171
|
+
"""Fires when the agent session starts. Injects overview, memories, tasks, and history."""
|
|
172
|
+
logger.info("Eagle Mem Hook: Session starting. Syncing context...")
|
|
173
|
+
session_id = get_session_id()
|
|
174
|
+
cwd = os.getcwd()
|
|
175
|
+
|
|
176
|
+
input_data = {
|
|
177
|
+
"session_id": session_id,
|
|
178
|
+
"cwd": cwd,
|
|
179
|
+
"source": "startup",
|
|
180
|
+
"model": "gemini",
|
|
181
|
+
"agent": self.agent_name
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Execute session-start.sh via stdin
|
|
185
|
+
stdout = await run_hook_async("session-start.sh", input_data)
|
|
186
|
+
context_injected = False
|
|
187
|
+
|
|
188
|
+
if stdout.strip():
|
|
189
|
+
try:
|
|
190
|
+
output_json = json.loads(stdout)
|
|
191
|
+
context = output_json.get("hookSpecificOutput", {}).get("additionalContext", "")
|
|
192
|
+
if context.strip():
|
|
193
|
+
res = hooks.inject_agent_context(context)
|
|
194
|
+
if inspect.isawaitable(res):
|
|
195
|
+
await res
|
|
196
|
+
context_injected = True
|
|
197
|
+
logger.info("Eagle Mem Hook: Injected session briefing.")
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.warning(f"Eagle Mem Hook: Failed to parse session-start JSON: {e}")
|
|
200
|
+
|
|
201
|
+
# Robust fallback if native sessionstart failed or returned empty
|
|
202
|
+
if not context_injected:
|
|
203
|
+
logger.info("Eagle Mem Hook: Running fallback search context...")
|
|
204
|
+
timeline_task = run_cmd_async(["eagle-mem", "search", "--timeline"])
|
|
205
|
+
overview_task = run_cmd_async(["eagle-mem", "search", "--overview"])
|
|
206
|
+
timeline, overview = await asyncio.gather(timeline_task, overview_task)
|
|
207
|
+
|
|
208
|
+
fallback_context = ""
|
|
209
|
+
if overview.strip():
|
|
210
|
+
fallback_context += f"=== Eagle Mem Project Overview ===\n{overview.strip()}\n\n"
|
|
211
|
+
if timeline.strip():
|
|
212
|
+
fallback_context += f"=== Eagle Mem Session Timeline ===\n{timeline.strip()}\n"
|
|
213
|
+
|
|
214
|
+
if fallback_context.strip():
|
|
215
|
+
res = hooks.inject_agent_context(fallback_context)
|
|
216
|
+
if inspect.isawaitable(res):
|
|
217
|
+
await res
|
|
218
|
+
logger.info("Eagle Mem Hook: Injected fallback context.")
|
|
219
|
+
|
|
220
|
+
async def pre_tool_call_decide(self, tool_call: Any) -> Any:
|
|
221
|
+
"""Fires before a tool execution. Blocks dangerous tasks or injects co-edits/recalls."""
|
|
222
|
+
tool_name = getattr(tool_call, "name", "")
|
|
223
|
+
tool_args = getattr(tool_call, "arguments", getattr(tool_call, "args", {}))
|
|
224
|
+
if not isinstance(tool_args, dict):
|
|
225
|
+
tool_args = {}
|
|
226
|
+
|
|
227
|
+
mapped_tool = map_tool_name(tool_name)
|
|
228
|
+
session_id = get_session_id()
|
|
229
|
+
cwd = os.getcwd()
|
|
230
|
+
|
|
231
|
+
# Build stdin payload matching PreToolUse schema
|
|
232
|
+
tool_input = {}
|
|
233
|
+
if mapped_tool == "Bash":
|
|
234
|
+
cmd = tool_args.get("CommandLine", tool_args.get("command", ""))
|
|
235
|
+
tool_input["command"] = cmd
|
|
236
|
+
else:
|
|
237
|
+
fp = tool_args.get("AbsolutePath", tool_args.get("TargetFile", tool_args.get("file_path", tool_args.get("path", ""))))
|
|
238
|
+
tool_input["file_path"] = fp
|
|
239
|
+
|
|
240
|
+
input_data = {
|
|
241
|
+
"session_id": session_id,
|
|
242
|
+
"cwd": cwd,
|
|
243
|
+
"tool_name": mapped_tool,
|
|
244
|
+
"tool_input": tool_input,
|
|
245
|
+
"agent": self.agent_name
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
stdout = await run_hook_async("pre-tool-use.sh", input_data)
|
|
249
|
+
if not stdout.strip():
|
|
250
|
+
return types.HookResult(allow=True)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
output_json = json.loads(stdout)
|
|
254
|
+
specific_output = output_json.get("hookSpecificOutput", {})
|
|
255
|
+
|
|
256
|
+
# 1. Check for deny decisions (safety guards/release boundaries)
|
|
257
|
+
if specific_output.get("permissionDecision") == "deny":
|
|
258
|
+
reason = specific_output.get("permissionDecisionReason", "Blocked by Eagle Mem guardrail.")
|
|
259
|
+
# Print clearly to the user's console
|
|
260
|
+
print(f"\n\033[1;31m[Eagle Mem Blocked]\033[0m {reason}\n", file=sys.stderr, flush=True)
|
|
261
|
+
return types.HookResult(allow=False)
|
|
262
|
+
|
|
263
|
+
# 2. Check for advisory additionalContext (co-edits, stuck loops, decision recalls)
|
|
264
|
+
additional_context = specific_output.get("additionalContext", "")
|
|
265
|
+
if additional_context.strip():
|
|
266
|
+
res = hooks.inject_agent_context(additional_context)
|
|
267
|
+
if inspect.isawaitable(res):
|
|
268
|
+
await res
|
|
269
|
+
logger.info(f"Eagle Mem Hook: Injected pre-tool context.")
|
|
270
|
+
|
|
271
|
+
# 3. Check for updatedInput (RTK rewriting)
|
|
272
|
+
updated_input = specific_output.get("updatedInput", {})
|
|
273
|
+
if updated_input and mapped_tool == "Bash" and "command" in updated_input:
|
|
274
|
+
rewritten_cmd = updated_input["command"]
|
|
275
|
+
logger.info(f"Eagle Mem Hook: Rewrote command to: {rewritten_cmd}")
|
|
276
|
+
if hasattr(tool_call, "arguments") and isinstance(tool_call.arguments, dict):
|
|
277
|
+
if "CommandLine" in tool_call.arguments:
|
|
278
|
+
tool_call.arguments["CommandLine"] = rewritten_cmd
|
|
279
|
+
elif "command" in tool_call.arguments:
|
|
280
|
+
tool_call.arguments["command"] = rewritten_cmd
|
|
281
|
+
elif hasattr(tool_call, "args") and isinstance(tool_call.args, dict):
|
|
282
|
+
if "CommandLine" in tool_call.args:
|
|
283
|
+
tool_call.args["CommandLine"] = rewritten_cmd
|
|
284
|
+
elif "command" in tool_call.args:
|
|
285
|
+
tool_call.args["command"] = rewritten_cmd
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.warning(f"Eagle Mem Hook: Failed to parse pre-tool JSON: {e}")
|
|
289
|
+
|
|
290
|
+
return types.HookResult(allow=True)
|
|
291
|
+
|
|
292
|
+
async def post_tool_call(self, tool_call_result: Any):
|
|
293
|
+
"""Fires after a tool completes. Records observations and triggers curation in the background."""
|
|
294
|
+
tool_call = getattr(tool_call_result, "tool_call", None)
|
|
295
|
+
tool_name = getattr(tool_call, "name", "") if tool_call else ""
|
|
296
|
+
tool_args = getattr(tool_call, "arguments", getattr(tool_call, "args", {})) if tool_call else {}
|
|
297
|
+
if not isinstance(tool_args, dict):
|
|
298
|
+
tool_args = {}
|
|
299
|
+
|
|
300
|
+
mapped_tool = map_tool_name(tool_name)
|
|
301
|
+
session_id = get_session_id()
|
|
302
|
+
cwd = os.getcwd()
|
|
303
|
+
|
|
304
|
+
tool_input = {}
|
|
305
|
+
if mapped_tool == "Bash":
|
|
306
|
+
cmd = tool_args.get("CommandLine", tool_args.get("command", ""))
|
|
307
|
+
tool_input["command"] = cmd
|
|
308
|
+
else:
|
|
309
|
+
fp = tool_args.get("AbsolutePath", tool_args.get("TargetFile", tool_args.get("file_path", tool_args.get("path", ""))))
|
|
310
|
+
tool_input["file_path"] = fp
|
|
311
|
+
|
|
312
|
+
output_str = getattr(tool_call_result, "output", getattr(tool_call_result, "result", ""))
|
|
313
|
+
if not isinstance(output_str, str):
|
|
314
|
+
output_str = str(output_str)
|
|
315
|
+
|
|
316
|
+
input_data = {
|
|
317
|
+
"session_id": session_id,
|
|
318
|
+
"cwd": cwd,
|
|
319
|
+
"tool_name": mapped_tool,
|
|
320
|
+
"tool_input": tool_input,
|
|
321
|
+
"tool_response": {"stdout": output_str},
|
|
322
|
+
"agent": self.agent_name
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# 1. Trigger post-tool-use.sh in background
|
|
326
|
+
asyncio.create_task(run_hook_async("post-tool-use.sh", input_data))
|
|
327
|
+
|
|
328
|
+
# 2. Concurrently run eagle-mem curate in background to index changes
|
|
329
|
+
asyncio.create_task(run_cmd_async(["eagle-mem", "curate"]))
|
|
330
|
+
|
|
331
|
+
async def post_turn(self, final_response: str):
|
|
332
|
+
"""Fires after each response. Records observaciones and saves sessions in the background."""
|
|
333
|
+
session_id = get_session_id()
|
|
334
|
+
cwd = os.getcwd()
|
|
335
|
+
|
|
336
|
+
input_data = {
|
|
337
|
+
"session_id": session_id,
|
|
338
|
+
"cwd": cwd,
|
|
339
|
+
"last_assistant_message": final_response,
|
|
340
|
+
"agent": self.agent_name
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# 1. Trigger Stop hook (stop.sh) in background to capture transcript/summary
|
|
344
|
+
asyncio.create_task(run_hook_async("stop.sh", input_data))
|
|
345
|
+
|
|
346
|
+
# 2. Concurrently trigger session save CLI in background
|
|
347
|
+
summary = final_response[:200] if final_response else ""
|
|
348
|
+
asyncio.create_task(run_cmd_async([
|
|
349
|
+
"eagle-mem", "session", "save",
|
|
350
|
+
"--summary", summary,
|
|
351
|
+
"--agent", self.agent_name
|
|
352
|
+
]))
|
|
353
|
+
|
|
354
|
+
async def on_compaction(self, data: Any):
|
|
355
|
+
"""Fires when context is compacted. Queries active tasks and injects them to survive compaction."""
|
|
356
|
+
logger.info("Eagle Mem Hook: Compaction event. Preserving active tasks...")
|
|
357
|
+
|
|
358
|
+
# Query active tasks from SQLite FTS5 index
|
|
359
|
+
tasks_text = await run_cmd_async(["eagle-mem", "tasks"])
|
|
360
|
+
if tasks_text.strip():
|
|
361
|
+
compaction_banner = (
|
|
362
|
+
f"\n=== Eagle Mem: Context Compaction Task Survival ===\n"
|
|
363
|
+
f"The following active tasks have survived compaction:\n"
|
|
364
|
+
f"{tasks_text.strip()}\n"
|
|
365
|
+
f"==================================================\n"
|
|
366
|
+
)
|
|
367
|
+
res = hooks.inject_agent_context(compaction_banner)
|
|
368
|
+
if inspect.isawaitable(res):
|
|
369
|
+
await res
|
|
370
|
+
logger.info("Eagle Mem Hook: Tasks injected for compaction survival.")
|
|
371
|
+
|
|
372
|
+
async def on_session_end(self):
|
|
373
|
+
"""Fires when the agent session closes. Cleans up runtime lock files."""
|
|
374
|
+
logger.info("Eagle Mem Hook: Session ending. Pruning locks...")
|
|
375
|
+
session_id = get_session_id()
|
|
376
|
+
cwd = os.getcwd()
|
|
377
|
+
|
|
378
|
+
input_data = {
|
|
379
|
+
"session_id": session_id,
|
|
380
|
+
"cwd": cwd,
|
|
381
|
+
"agent": self.agent_name
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# 1. Trigger session-end.sh in background
|
|
385
|
+
asyncio.create_task(run_hook_async("session-end.sh", input_data))
|
|
386
|
+
|
|
387
|
+
# 2. Concurrently trigger prune CLI in background
|
|
388
|
+
asyncio.create_task(run_cmd_async(["eagle-mem", "prune"]))
|
|
389
|
+
|
|
390
|
+
# --- Expose Bound Methods as Global Functions ---
|
|
391
|
+
|
|
392
|
+
_global_hook = EagleMemAntigravityHook()
|
|
393
|
+
|
|
394
|
+
@hooks.on_session_start
|
|
395
|
+
async def on_session_start():
|
|
396
|
+
await _global_hook.on_session_start()
|
|
397
|
+
|
|
398
|
+
@hooks.pre_tool_call_decide
|
|
399
|
+
async def pre_tool_call_decide(tool_call: Any) -> Any:
|
|
400
|
+
return await _global_hook.pre_tool_call_decide(tool_call)
|
|
401
|
+
|
|
402
|
+
@hooks.post_tool_call
|
|
403
|
+
async def post_tool_call(tool_call_result: Any):
|
|
404
|
+
await _global_hook.post_tool_call(tool_call_result)
|
|
405
|
+
|
|
406
|
+
@hooks.post_turn
|
|
407
|
+
async def post_turn(final_response: str):
|
|
408
|
+
await _global_hook.post_turn(final_response)
|
|
409
|
+
|
|
410
|
+
@hooks.on_compaction
|
|
411
|
+
async def on_compaction(data: Any):
|
|
412
|
+
await _global_hook.on_compaction(data)
|
|
413
|
+
|
|
414
|
+
@hooks.on_session_end
|
|
415
|
+
async def on_session_end():
|
|
416
|
+
await _global_hook.on_session_end()
|
package/lib/codex-hooks.sh
CHANGED
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
# Eagle Mem — Codex hook registration helpers
|
|
4
4
|
# Shared by install.sh, update.sh, and uninstall.sh
|
|
5
5
|
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
7
|
[ -n "${_EAGLE_CODEX_HOOKS_LOADED:-}" ] && return 0
|
|
7
8
|
_EAGLE_CODEX_HOOKS_LOADED=1
|
|
8
9
|
|
|
10
|
+
eagle_codex_backup_file() {
|
|
11
|
+
local file="$1"
|
|
12
|
+
[ -f "$file" ] || return 0
|
|
13
|
+
if [ -z "${_EAGLE_CODEX_BACKUP_DONE:-}" ]; then
|
|
14
|
+
local timestamp
|
|
15
|
+
timestamp=$(date +%Y%m%d%H%M%S)
|
|
16
|
+
cp "$file" "${file}.bak.${timestamp}" 2>/dev/null || true
|
|
17
|
+
_EAGLE_CODEX_BACKUP_DONE=1
|
|
18
|
+
fi
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
eagle_enable_codex_hooks() {
|
|
10
22
|
local config="$EAGLE_CODEX_CONFIG"
|
|
11
23
|
mkdir -p "$(dirname "$config")"
|
|
12
24
|
|
|
25
|
+
eagle_codex_backup_file "$config"
|
|
26
|
+
|
|
13
27
|
if [ ! -f "$config" ]; then
|
|
14
28
|
cat > "$config" << 'TOML'
|
|
15
29
|
[features]
|
|
@@ -71,6 +85,7 @@ eagle_patch_codex_hook() {
|
|
|
71
85
|
script_path="${script_path%\"}"
|
|
72
86
|
|
|
73
87
|
mkdir -p "$(dirname "$hooks_file")"
|
|
88
|
+
eagle_codex_backup_file "$hooks_file"
|
|
74
89
|
if [ ! -f "$hooks_file" ]; then
|
|
75
90
|
printf '{"hooks":{}}\n' > "$hooks_file"
|
|
76
91
|
chmod 600 "$hooks_file" 2>/dev/null || true
|
|
@@ -171,6 +186,8 @@ eagle_remove_codex_hooks() {
|
|
|
171
186
|
[ -f "$hooks_file" ] || return 1
|
|
172
187
|
command -v jq &>/dev/null || return 1
|
|
173
188
|
|
|
189
|
+
eagle_codex_backup_file "$hooks_file"
|
|
190
|
+
|
|
174
191
|
local tmp
|
|
175
192
|
tmp=$(mktemp)
|
|
176
193
|
jq '
|
package/lib/common.sh
CHANGED
|
@@ -771,6 +771,7 @@ eagle_agent_source() {
|
|
|
771
771
|
case "$agent" in
|
|
772
772
|
codex|openai-codex) echo "codex" ;;
|
|
773
773
|
claude|claude-code|cloud-code) echo "claude-code" ;;
|
|
774
|
+
antigravity*|google-antigravity*|google_antigravity*) echo "antigravity" ;;
|
|
774
775
|
*)
|
|
775
776
|
if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ -n "${CODEX_MANAGED_BY_NPM:-}" ]; then
|
|
776
777
|
echo "codex"
|
|
@@ -789,10 +790,15 @@ eagle_agent_source_from_json() {
|
|
|
789
790
|
return
|
|
790
791
|
fi
|
|
791
792
|
|
|
792
|
-
local transcript_path turn_id tool_name
|
|
793
|
+
local transcript_path turn_id tool_name agent_field
|
|
793
794
|
transcript_path=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
794
795
|
turn_id=$(printf '%s' "$input" | jq -r '.turn_id // empty' 2>/dev/null)
|
|
795
796
|
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
797
|
+
agent_field=$(printf '%s' "$input" | jq -r '.agent // empty' 2>/dev/null)
|
|
798
|
+
|
|
799
|
+
case "$agent_field" in
|
|
800
|
+
antigravity*) echo "antigravity"; return ;;
|
|
801
|
+
esac
|
|
796
802
|
|
|
797
803
|
case "$transcript_path" in
|
|
798
804
|
"$HOME/.codex/"*|*/.codex/*) echo "codex"; return ;;
|
|
@@ -807,6 +813,7 @@ eagle_agent_source_from_json() {
|
|
|
807
813
|
eagle_agent_label() {
|
|
808
814
|
case "${1:-$(eagle_agent_source)}" in
|
|
809
815
|
codex) echo "Codex" ;;
|
|
816
|
+
antigravity*) echo "Antigravity" ;;
|
|
810
817
|
*) echo "Claude Code" ;;
|
|
811
818
|
esac
|
|
812
819
|
}
|
package/lib/hooks-posttool.sh
CHANGED
|
@@ -27,6 +27,16 @@ eagle_posttool_mirror_writes() {
|
|
|
27
27
|
eagle_capture_agent_plan "$fp" "$session_id" "$project" "$agent"
|
|
28
28
|
fi
|
|
29
29
|
;;
|
|
30
|
+
*/brain/*/implementation_plan.md)
|
|
31
|
+
if [ -f "$fp" ]; then
|
|
32
|
+
eagle_capture_agent_plan "$fp" "$session_id" "$project" "$agent"
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
*/brain/*/task.md|*/brain/*/walkthrough.md)
|
|
36
|
+
if [ -f "$fp" ]; then
|
|
37
|
+
eagle_capture_agent_memory "$fp" "$session_id" "$project" "$agent"
|
|
38
|
+
fi
|
|
39
|
+
;;
|
|
30
40
|
esac
|
|
31
41
|
fi
|
|
32
42
|
;;
|
package/lib/hooks.sh
CHANGED
|
@@ -4,11 +4,26 @@
|
|
|
4
4
|
# Shared by install.sh and update.sh
|
|
5
5
|
# ═══════════════════════════════════════════════════════════
|
|
6
6
|
|
|
7
|
+
# ═══════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
eagle_backup_file() {
|
|
10
|
+
local file="$1"
|
|
11
|
+
[ -f "$file" ] || return 0
|
|
12
|
+
if [ -z "${_EAGLE_BACKUP_DONE:-}" ]; then
|
|
13
|
+
local timestamp
|
|
14
|
+
timestamp=$(date +%Y%m%d%H%M%S)
|
|
15
|
+
cp "$file" "${file}.bak.${timestamp}" 2>/dev/null || true
|
|
16
|
+
_EAGLE_BACKUP_DONE=1
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
eagle_clean_hook_entries() {
|
|
8
21
|
local settings="$1"
|
|
9
22
|
local event="$2"
|
|
10
23
|
local command="$3"
|
|
11
24
|
|
|
25
|
+
eagle_backup_file "$settings"
|
|
26
|
+
|
|
12
27
|
local tmp
|
|
13
28
|
tmp=$(mktemp)
|
|
14
29
|
jq --arg cmd "$command" \
|
|
@@ -23,6 +38,8 @@ eagle_patch_hook() {
|
|
|
23
38
|
local command="$4"
|
|
24
39
|
local description="${5:-}"
|
|
25
40
|
|
|
41
|
+
eagle_backup_file "$settings"
|
|
42
|
+
|
|
26
43
|
# Check both command AND matcher to avoid skipping entries with different matchers
|
|
27
44
|
# (e.g. PreToolUse with "Bash" vs "Read" matcher using the same script)
|
|
28
45
|
local match_query
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eagle-mem",
|
|
3
|
-
"version": "4.10.
|
|
4
|
-
"description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, and
|
|
3
|
+
"version": "4.10.3",
|
|
4
|
+
"description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, Grok, and Google Antigravity",
|
|
5
5
|
"bin": {
|
|
6
6
|
"eagle-mem": "bin/eagle-mem"
|
|
7
7
|
},
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"db/",
|
|
14
14
|
"skills/",
|
|
15
15
|
"docs/",
|
|
16
|
-
"architecture.html"
|
|
16
|
+
"architecture.html",
|
|
17
|
+
"integrations/",
|
|
18
|
+
"CHANGELOG.md"
|
|
17
19
|
],
|
|
18
20
|
"keywords": [
|
|
19
21
|
"claude-code",
|
|
@@ -26,22 +26,44 @@ eagle_ok "Grok environment detected ($EAGLE_GROK_DIR)"
|
|
|
26
26
|
|
|
27
27
|
# Check skills
|
|
28
28
|
skill_count=0
|
|
29
|
+
broken_links=0
|
|
29
30
|
if [ -d "$EAGLE_GROK_SKILLS_DIR" ]; then
|
|
30
|
-
|
|
31
|
+
for link in "$EAGLE_GROK_SKILLS_DIR"/eagle-mem-*; do
|
|
32
|
+
if [ -L "$link" ] && [ ! -e "$link" ]; then
|
|
33
|
+
broken_links=$((broken_links + 1))
|
|
34
|
+
elif [ -d "$link" ]; then
|
|
35
|
+
skill_count=$((skill_count + 1))
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ "$broken_links" -gt 0 ]; then
|
|
41
|
+
eagle_warn "Detected $broken_links broken or invalid Grok skill symlinks!"
|
|
31
42
|
fi
|
|
32
43
|
|
|
33
|
-
if [ "$skill_count" -gt 0 ]; then
|
|
44
|
+
if [ "$skill_count" -gt 0 ] && [ "$broken_links" -eq 0 ]; then
|
|
34
45
|
eagle_ok "Grok skills installed ($skill_count eagle-mem-* skills)"
|
|
35
46
|
else
|
|
36
|
-
|
|
47
|
+
if [ "$broken_links" -gt 0 ]; then
|
|
48
|
+
echo ""
|
|
49
|
+
eagle_info "Repairing broken skill symlinks now..."
|
|
50
|
+
fi
|
|
37
51
|
if [ -d "$PACKAGE_DIR/skills" ]; then
|
|
38
52
|
echo ""
|
|
39
|
-
if eagle_confirm "Link Eagle Mem skills into ~/.grok/skills/ now?"; then
|
|
53
|
+
if eagle_confirm "Link/repair Eagle Mem skills into ~/.grok/skills/ now?"; then
|
|
40
54
|
mkdir -p "$EAGLE_GROK_SKILLS_DIR"
|
|
41
55
|
for skill_dir in "$PACKAGE_DIR"/skills/*/; do
|
|
42
56
|
[ ! -d "$skill_dir" ] && continue
|
|
43
57
|
skill_name=$(basename "$skill_dir")
|
|
44
58
|
dst="$EAGLE_GROK_SKILLS_DIR/$skill_name"
|
|
59
|
+
|
|
60
|
+
# Protect custom user folders with a timestamped backup
|
|
61
|
+
if [ -d "$dst" ] && [ ! -L "$dst" ]; then
|
|
62
|
+
backup_dst="${dst}.bak.$(date +%Y%m%d%H%M%S)"
|
|
63
|
+
eagle_warn "Custom directory found at $dst. Backing up to $(basename "$backup_dst")..."
|
|
64
|
+
mv "$dst" "$backup_dst"
|
|
65
|
+
fi
|
|
66
|
+
|
|
45
67
|
[ -L "$dst" ] && rm "$dst"
|
|
46
68
|
ln -sf "$skill_dir" "$dst"
|
|
47
69
|
eagle_ok "Linked: $skill_name"
|
package/scripts/install.sh
CHANGED
|
@@ -195,13 +195,14 @@ echo ""
|
|
|
195
195
|
echo -e " ${BOLD}Installing Eagle Mem...${RESET}"
|
|
196
196
|
echo ""
|
|
197
197
|
|
|
198
|
-
mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db,scripts}
|
|
198
|
+
mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db,scripts,integrations}
|
|
199
199
|
|
|
200
200
|
cp "$PACKAGE_DIR"/hooks/*.sh "$EAGLE_MEM_DIR/hooks/"
|
|
201
201
|
cp "$PACKAGE_DIR"/lib/*.sh "$EAGLE_MEM_DIR/lib/"
|
|
202
202
|
cp "$PACKAGE_DIR"/db/*.sh "$EAGLE_MEM_DIR/db/"
|
|
203
203
|
cp "$PACKAGE_DIR"/db/*.sql "$EAGLE_MEM_DIR/db/"
|
|
204
204
|
cp "$PACKAGE_DIR"/scripts/*.sh "$EAGLE_MEM_DIR/scripts/" 2>/dev/null
|
|
205
|
+
cp -r "$PACKAGE_DIR"/integrations/* "$EAGLE_MEM_DIR/integrations/" 2>/dev/null || true
|
|
205
206
|
|
|
206
207
|
chmod +x "$EAGLE_MEM_DIR"/hooks/*.sh
|
|
207
208
|
chmod +x "$EAGLE_MEM_DIR"/db/migrate.sh
|
|
@@ -410,6 +411,7 @@ eagle_kv "Hooks:" "$EAGLE_MEM_DIR/hooks/"
|
|
|
410
411
|
[ "$claude_found" = true ] && eagle_kv "Claude settings:" "$SETTINGS"
|
|
411
412
|
[ "$codex_found" = true ] && eagle_kv "Codex hooks:" "$EAGLE_CODEX_HOOKS"
|
|
412
413
|
[ "$grok_found" = true ] && eagle_kv "Grok skills:" "$EAGLE_GROK_SKILLS_DIR"
|
|
414
|
+
eagle_kv "Antigravity Hook:" "$EAGLE_MEM_DIR/integrations/google_antigravity_hook.py"
|
|
413
415
|
|
|
414
416
|
echo ""
|
|
415
417
|
if [ "$grok_found" = true ]; then
|
|
@@ -419,3 +421,9 @@ if [ "$claude_found" = true ] || [ "$codex_found" = true ]; then
|
|
|
419
421
|
eagle_dim "Start a new Claude Code or Codex session — Eagle Mem will activate automatically."
|
|
420
422
|
fi
|
|
421
423
|
echo ""
|
|
424
|
+
eagle_info "Google Antigravity SDK Integration:"
|
|
425
|
+
eagle_dim " To use Eagle Mem inside your Python Antigravity agents, simply import and register the hook:"
|
|
426
|
+
echo ""
|
|
427
|
+
eagle_dim " from integrations.google_antigravity_hook import EagleMemAntigravityHook"
|
|
428
|
+
eagle_dim " config = LocalAgentConfig(hooks=EagleMemAntigravityHook().get_hooks())"
|
|
429
|
+
echo ""
|