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.
@@ -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()
@@ -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
  }
@@ -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.1",
4
- "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, and Grok",
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
- skill_count=$(find "$EAGLE_GROK_SKILLS_DIR" -maxdepth 1 -name "eagle-mem-*" -type d 2>/dev/null | wc -l | tr -d ' ')
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
- eagle_warn "No eagle-mem skills found in $EAGLE_GROK_SKILLS_DIR"
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"
@@ -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 ""