eagle-mem 4.6.2 → 4.7.0

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.
@@ -24,6 +24,8 @@ session_id=$(echo "$input" | jq -r '.session_id // empty')
24
24
  cwd=$(echo "$input" | jq -r '.cwd // empty')
25
25
  source_type=$(echo "$input" | jq -r '.source // empty')
26
26
  model=$(echo "$input" | jq -r '.model // empty')
27
+ agent=$(eagle_agent_source_from_json "$input")
28
+ agent_label=$(eagle_agent_label "$agent")
27
29
 
28
30
  [ -z "$session_id" ] && exit 0
29
31
 
@@ -32,9 +34,9 @@ project=$(eagle_project_from_cwd "$cwd")
32
34
 
33
35
  p_esc=$(eagle_sql_escape "$project")
34
36
 
35
- eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
37
+ eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type agent=$agent"
36
38
 
37
- eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
39
+ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type" "$agent"
38
40
  eagle_abandon_stale_sessions "$session_id"
39
41
 
40
42
  # ─── Reset turn counter on compact/clear ─────────────────
@@ -90,7 +92,8 @@ fi
90
92
 
91
93
  # ─── Gather stats ────────────────────────────────────────
92
94
 
93
- stat_sessions=0; stat_summaries=0; stat_with_summaries=0; stat_memories=0
95
+ stat_sessions=0; stat_sessions_claude=0; stat_sessions_codex=0
96
+ stat_summaries=0; stat_with_summaries=0; stat_memories=0
94
97
  stat_tasks_pending=0; stat_tasks_progress=0; stat_tasks_done=0
95
98
  stat_chunks=0; stat_observations=0; stat_plans=0
96
99
  stat_last_active="never"; stat_last_summary=""
@@ -98,6 +101,8 @@ stat_last_active="never"; stat_last_summary=""
98
101
  while IFS='|' read -r key val; do
99
102
  case "$key" in
100
103
  sessions) stat_sessions="$val" ;;
104
+ sessions_claude) stat_sessions_claude="$val" ;;
105
+ sessions_codex) stat_sessions_codex="$val" ;;
101
106
  summaries) stat_summaries="$val" ;;
102
107
  with_summaries) stat_with_summaries="$val" ;;
103
108
  memories) stat_memories="$val" ;;
@@ -132,7 +137,12 @@ eagle_banner="======================================
132
137
  Eagle Mem Recall Ready
133
138
  ======================================
134
139
  Project | $project
140
+ Agent | $agent_label
135
141
  Sessions | $stat_sessions ($stat_with_summaries with summaries)"
142
+ if [ "$stat_sessions_codex" -gt 0 ] || [ "$stat_sessions_claude" -gt 0 ]; then
143
+ eagle_banner+="
144
+ Sources | Claude $stat_sessions_claude, Codex $stat_sessions_codex"
145
+ fi
136
146
  [ "$stat_memories" -gt 0 ] && eagle_banner+="
137
147
  Memories | $stat_memories stored"
138
148
  [ "$stat_plans" -gt 0 ] && eagle_banner+="
@@ -188,10 +198,13 @@ if [ -n "$recent" ]; then
188
198
  context+="
189
199
  === Eagle Mem: Recent Recall ===
190
200
  "
191
- while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files; do
201
+ while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files summary_agent; do
192
202
  [ -z "$request" ] && [ -z "$completed" ] && continue
203
+ summary_agent_label=$(eagle_agent_label "$summary_agent")
193
204
  context+="
194
205
  --- $created_at ---"
206
+ [ -n "$summary_agent" ] && context+="
207
+ Source: $summary_agent_label"
195
208
  [ -n "$request" ] && context+="
196
209
  Request: $request"
197
210
  [ -n "$completed" ] && context+="
@@ -213,7 +226,7 @@ fi
213
226
 
214
227
  # ─── Memories (skip if none) ─────────────────────────────
215
228
 
216
- memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at,
229
+ memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at, origin_agent,
217
230
  CAST(julianday('now') - julianday(updated_at) AS INTEGER) as days_ago
218
231
  FROM claude_memories
219
232
  WHERE project = '$p_esc'
@@ -223,8 +236,9 @@ if [ -n "$memories" ]; then
223
236
  context+="
224
237
  === Eagle Mem: Stored Memories ===
225
238
  "
226
- while IFS='|' read -r mname mtype mdesc _fpath _updated days_ago; do
239
+ while IFS='|' read -r mname mtype mdesc _fpath _updated morigin days_ago; do
227
240
  [ -z "$mname" ] && continue
241
+ origin_label=$(eagle_agent_label "$morigin")
228
242
  age_label=""
229
243
  if [ -n "$days_ago" ] && [ "$days_ago" -gt 0 ] 2>/dev/null; then
230
244
  if [ "$days_ago" -eq 1 ]; then
@@ -235,7 +249,7 @@ if [ -n "$memories" ]; then
235
249
  else
236
250
  age_label=" (today)"
237
251
  fi
238
- context+=" - [$mtype] $mname: $mdesc$age_label
252
+ context+=" - [$mtype][$origin_label] $mname: $mdesc$age_label
239
253
  "
240
254
  done <<< "$memories"
241
255
  fi
@@ -247,16 +261,17 @@ if [ -n "$plans" ]; then
247
261
  context+="
248
262
  === Eagle Mem: Plans ===
249
263
  "
250
- while IFS='|' read -r ptitle _pproj _fpath _updated; do
264
+ while IFS='|' read -r ptitle _pproj _fpath _updated porigin; do
251
265
  [ -z "$ptitle" ] && continue
252
- context+=" - $ptitle
266
+ origin_label=$(eagle_agent_label "$porigin")
267
+ context+=" - [$origin_label] $ptitle
253
268
  "
254
269
  done <<< "$plans"
255
270
  fi
256
271
 
257
272
  # ─── Tasks (skip if none) ────────────────────────────────
258
273
 
259
- synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
274
+ synced_tasks=$(eagle_db "SELECT subject, status, blocked_by, origin_agent FROM claude_tasks
260
275
  WHERE project = '$p_esc'
261
276
  AND status IN ('in_progress', 'pending')
262
277
  AND updated_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days')
@@ -268,17 +283,40 @@ if [ -n "$synced_tasks" ]; then
268
283
  context+="
269
284
  === Eagle Mem: Tasks ===
270
285
  "
271
- while IFS='|' read -r tsubject tstatus tblocked; do
286
+ while IFS='|' read -r tsubject tstatus tblocked torigin; do
272
287
  [ -z "$tsubject" ] && continue
288
+ origin_label=$(eagle_agent_label "$torigin")
273
289
  block_marker=""
274
290
  if [ "$tblocked" != "[]" ] && [ -n "$tblocked" ]; then
275
291
  block_marker=" (blocked)"
276
292
  fi
277
- context+=" - [$tstatus] $tsubject$block_marker
293
+ context+=" - [$tstatus][$origin_label] $tsubject$block_marker
278
294
  "
279
295
  done <<< "$synced_tasks"
280
296
  fi
281
297
 
298
+ # ─── Pending feature verifications ───────────────────────
299
+
300
+ pending_features=$(eagle_list_pending_feature_verifications "$project" 10 2>/dev/null)
301
+ if [ -n "$pending_features" ]; then
302
+ context+="
303
+ === Eagle Mem: Pending Feature Verification ===
304
+ Release-boundary commands are blocked until these are verified or waived.
305
+ "
306
+ while IFS='|' read -r pf_id pf_name pf_file pf_reason _pf_trigger _pf_created pf_smoke pfingerprint; do
307
+ [ -z "$pf_id" ] && continue
308
+ context+=" - #${pf_id} ${pf_name}"
309
+ [ -n "$pf_file" ] && context+=" (${pf_file})"
310
+ [ -n "$pf_reason" ] && context+=" — ${pf_reason}"
311
+ [ -n "$pf_smoke" ] && context+=" | smoke: ${pf_smoke}"
312
+ [ -n "$pfingerprint" ] && context+=" | diff: ${pfingerprint}"
313
+ context+="
314
+ "
315
+ done <<< "$pending_features"
316
+ context+="Resolve with: eagle-mem feature verify <name> --notes \"what passed\"; or eagle-mem feature waive <id> --reason \"why safe\".
317
+ "
318
+ fi
319
+
282
320
  # ─── Core files (hot file hints from curator) ───────────
283
321
 
284
322
  hot_files=$(eagle_get_hot_files "$project")
@@ -334,12 +372,15 @@ next_steps: [concrete actions]
334
372
  key_files: [path — role]
335
373
  files_read: [path, ...]
336
374
  files_modified: [path, ...]
375
+ affected_features: [feature, ...]
376
+ verified_features: [feature, ...]
377
+ regression_risks: [risk, ...]
337
378
  </eagle-summary>
338
379
  "
339
380
  fi
340
381
 
341
382
  if [ -n "$context" ]; then
342
- echo "$context"
383
+ eagle_emit_context_for_agent "$agent" "SessionStart" "$context"
343
384
  fi
344
385
 
345
386
  exit 0
package/hooks/stop.sh CHANGED
@@ -23,6 +23,8 @@ input=$(eagle_read_stdin)
23
23
  session_id=$(echo "$input" | jq -r '.session_id // empty')
24
24
  cwd=$(echo "$input" | jq -r '.cwd // empty')
25
25
  transcript_path=$(echo "$input" | jq -r '.transcript_path // empty')
26
+ last_assistant_message=$(echo "$input" | jq -r '.last_assistant_message // empty')
27
+ agent=$(eagle_agent_source_from_json "$input")
26
28
 
27
29
  [ -z "$session_id" ] && exit 0
28
30
 
@@ -33,25 +35,62 @@ agent_type=$(echo "$input" | jq -r '.agent_type // empty')
33
35
  project=$(eagle_project_from_cwd "$cwd")
34
36
  [ -z "$project" ] && exit 0
35
37
 
36
- eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path"
38
+ eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path agent=$agent"
37
39
 
38
40
  # Ensure session exists (may not if SessionStart didn't fire)
39
- eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
40
-
41
- [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ] && exit 0
41
+ eagle_upsert_session "$session_id" "$project" "$cwd" "" "" "$agent"
42
+
43
+ # Reconcile from git diff, not only from edit-tool hooks. This keeps
44
+ # anti-regression agent-agnostic: Claude, Codex, manual edits, and script edits
45
+ # all become visible before a release boundary.
46
+ if [ -n "$cwd" ] && [ -d "$cwd" ]; then
47
+ changed_files=$(eagle_changed_files_for_release "$cwd")
48
+ if [ -n "$changed_files" ]; then
49
+ eagle_reconcile_current_feature_verifications "$project" "$cwd" "$session_id" "Stop" "Repository diff detected at turn end" "$changed_files" >/dev/null
50
+ fi
51
+ fi
42
52
 
43
53
  # ─── Primary: heuristic extraction from transcript ───────────
44
54
 
45
- request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
46
- | grep -v '<local-command-caveat>' \
47
- | grep -v '<system-reminder>' \
48
- | grep -v '<command-name>' \
49
- | grep -v '<command-message>' \
50
- | grep -v '^\[{' \
51
- | head -1 | cut -c1-500)
55
+ request=""
56
+ heuristic_reads=""
57
+ heuristic_writes=""
58
+
59
+ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
60
+ request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
61
+ | grep -v '<local-command-caveat>' \
62
+ | grep -v '<system-reminder>' \
63
+ | grep -v '<command-name>' \
64
+ | grep -v '<command-message>' \
65
+ | grep -v '^\[{' \
66
+ | head -1 | cut -c1-500)
67
+
68
+ if [ -z "$request" ]; then
69
+ request=$(jq -r '
70
+ select(.type == "response_item" and .payload.role == "user")
71
+ | .payload.content
72
+ | if type == "string" then .
73
+ elif type == "array" then [.[]? | select(.type == "input_text" or .type == "text") | .text] | join(" ")
74
+ else "" end
75
+ ' "$transcript_path" 2>/dev/null \
76
+ | grep -v '<local-command-caveat>' \
77
+ | grep -v '<system-reminder>' \
78
+ | grep -v '<command-name>' \
79
+ | grep -v '<command-message>' \
80
+ | grep -v '^\[{' \
81
+ | head -1 | cut -c1-500)
82
+ fi
83
+
84
+ if [ -z "$request" ]; then
85
+ request=$(jq -r 'select(.type == "event_msg" and .payload.type == "user_message") | .payload.message // empty' "$transcript_path" 2>/dev/null \
86
+ | grep -v '<local-command-caveat>' \
87
+ | grep -v '<system-reminder>' \
88
+ | head -1 | cut -c1-500)
89
+ fi
52
90
 
53
- heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
54
- heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
91
+ heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
92
+ heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
93
+ fi
55
94
 
56
95
  files_read="[]"
57
96
  files_modified="[]"
@@ -70,17 +109,46 @@ notes=""
70
109
  decisions=""
71
110
  gotchas=""
72
111
  key_files=""
112
+ affected_features=""
113
+ verified_features=""
114
+ regression_risks=""
73
115
 
74
116
  eagle_log "INFO" "Stop: heuristic extraction complete"
75
117
 
76
118
  # ─── Bonus: eagle-summary block overrides where present ──────
77
119
 
78
- text_content=$(jq -rs '
79
- [.[] | select(.type == "assistant")] | last |
80
- if . then
81
- [.message.content[]? | select(.type == "text") | .text] | join("\n")
82
- else "" end
83
- ' "$transcript_path" 2>/dev/null)
120
+ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
121
+ text_content=$(jq -r -s '
122
+ [.[] | select(.type == "assistant")] | last |
123
+ if . then
124
+ [.message.content[]? | select(.type == "text") | .text] | join("\n")
125
+ else "" end
126
+ ' "$transcript_path" 2>/dev/null)
127
+
128
+ if [ -z "$text_content" ]; then
129
+ text_content=$(jq -r -s '
130
+ def content_text:
131
+ if type == "string" then .
132
+ elif type == "array" then
133
+ [.[]? | select(.type == "output_text" or .type == "text") | (.text // empty)] | join("\n")
134
+ else "" end;
135
+
136
+ (
137
+ [.[] | select(.type == "response_item" and .payload.role == "assistant") | (.payload.content | content_text)]
138
+ | map(select(. != ""))
139
+ | last
140
+ ) // (
141
+ [.[] | select(.type == "event_msg" and .payload.type == "agent_message") | (.payload.message // "")]
142
+ | map(select(. != ""))
143
+ | last
144
+ ) // ""
145
+ ' "$transcript_path" 2>/dev/null)
146
+ fi
147
+ fi
148
+
149
+ if [ -z "$text_content" ]; then
150
+ text_content="$last_assistant_message"
151
+ fi
84
152
 
85
153
  # Strip <private>...</private> blocks
86
154
  text_content=$(echo "$text_content" | sed -E '/<[Pp][Rr][Ii][Vv][Aa][Tt][Ee][^>]*>/,/<\/[Pp][Rr][Ii][Vv][Aa][Tt][Ee][[:space:]]*>/d')
@@ -99,7 +167,7 @@ if [ -n "$summary_block" ]; then
99
167
  $0 ~ "^"f":" {
100
168
  sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
101
169
  }
102
- found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files):/ { exit }
170
+ found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files|affected_features|verified_features|regression_risks):/ { exit }
103
171
  found { val = val " " $0 }
104
172
  END { if (found) print val }
105
173
  '
@@ -114,6 +182,9 @@ if [ -n "$summary_block" ]; then
114
182
  _val=$(parse_field "$summary_block" "decisions"); [ -n "$_val" ] && decisions="$_val"
115
183
  _val=$(parse_field "$summary_block" "gotchas"); [ -n "$_val" ] && gotchas="$_val"
116
184
  _val=$(parse_field "$summary_block" "key_files"); [ -n "$_val" ] && key_files="$_val"
185
+ _val=$(parse_field "$summary_block" "affected_features"); [ -n "$_val" ] && affected_features="$_val"
186
+ _val=$(parse_field "$summary_block" "verified_features"); [ -n "$_val" ] && verified_features="$_val"
187
+ _val=$(parse_field "$summary_block" "regression_risks"); [ -n "$_val" ] && regression_risks="$_val"
117
188
 
118
189
  # Convert bracket-list to JSON array
119
190
  to_json_array() {
@@ -295,11 +366,33 @@ next_steps=$(echo "$next_steps" | eagle_redact)
295
366
  decisions=$(echo "$decisions" | eagle_redact)
296
367
  gotchas=$(echo "$gotchas" | eagle_redact)
297
368
  key_files=$(echo "$key_files" | eagle_redact)
369
+ notes=$(echo "$notes" | eagle_redact)
370
+ affected_features=$(echo "$affected_features" | eagle_redact)
371
+ verified_features=$(echo "$verified_features" | eagle_redact)
372
+ regression_risks=$(echo "$regression_risks" | eagle_redact)
373
+
374
+ regression_notes=""
375
+ [ -n "$affected_features" ] && regression_notes+="affected_features: $affected_features"
376
+ if [ -n "$verified_features" ]; then
377
+ [ -n "$regression_notes" ] && regression_notes+="; "
378
+ regression_notes+="verified_features: $verified_features"
379
+ fi
380
+ if [ -n "$regression_risks" ]; then
381
+ [ -n "$regression_notes" ] && regression_notes+="; "
382
+ regression_notes+="regression_risks: $regression_risks"
383
+ fi
384
+ if [ -n "$regression_notes" ]; then
385
+ if [ -n "$notes" ]; then
386
+ notes="${notes}; ${regression_notes}"
387
+ else
388
+ notes="$regression_notes"
389
+ fi
390
+ fi
298
391
 
299
392
  # ─── Write to database ─────────────────────────────────────
300
393
 
301
394
  if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
302
- if eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"; then
395
+ if eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files" "$agent"; then
303
396
  eagle_log "INFO" "Stop: summary saved for session=$session_id"
304
397
  else
305
398
  eagle_log "ERROR" "Stop: summary insert FAILED for session=$session_id — check DB constraints"
@@ -20,6 +20,7 @@ input=$(eagle_read_stdin)
20
20
  session_id=$(echo "$input" | jq -r '.session_id // empty')
21
21
  cwd=$(echo "$input" | jq -r '.cwd // empty')
22
22
  user_prompt=$(echo "$input" | jq -r '.prompt // empty')
23
+ agent=$(eagle_agent_source_from_json "$input")
23
24
 
24
25
  [ -z "$user_prompt" ] && exit 0
25
26
 
@@ -60,7 +61,10 @@ fi
60
61
 
61
62
  # Skip short prompts — not enough signal for meaningful search
62
63
  word_count=$(echo "$user_prompt" | wc -w | tr -d ' ')
63
- [ "$word_count" -lt 3 ] && { [ -n "$context" ] && echo "$context"; exit 0; }
64
+ if [ "$word_count" -lt 3 ]; then
65
+ eagle_emit_context_for_agent "$agent" "UserPromptSubmit" "$context"
66
+ exit 0
67
+ fi
64
68
 
65
69
  # Build FTS5 query from significant words (drop stop words, take first 6)
66
70
  fts_query=$(echo "$user_prompt" | tr -cs '[:alnum:]' ' ' | tr '[:upper:]' '[:lower:]' | \
@@ -75,7 +79,10 @@ fts_query=$(echo "$user_prompt" | tr -cs '[:alnum:]' ' ' | tr '[:upper:]' '[:low
75
79
  }
76
80
  }')
77
81
 
78
- [ -z "$fts_query" ] && { [ -n "$context" ] && echo "$context"; exit 0; }
82
+ if [ -z "$fts_query" ]; then
83
+ eagle_emit_context_for_agent "$agent" "UserPromptSubmit" "$context"
84
+ exit 0
85
+ fi
79
86
 
80
87
  # Search for relevant past summaries (cross-session)
81
88
  results=$(eagle_search_summaries "$fts_query" "$project" 3)
@@ -83,9 +90,10 @@ results=$(eagle_search_summaries "$fts_query" "$project" 3)
83
90
  if [ -n "$results" ]; then
84
91
  context+="=== Eagle Mem: Relevant Recall ===
85
92
  "
86
- while IFS='|' read -r req completed learned _next_steps created_at _proj decisions gotchas key_files; do
93
+ while IFS='|' read -r req completed learned _next_steps created_at _proj decisions gotchas key_files summary_agent; do
87
94
  [ -z "$req" ] && [ -z "$completed" ] && continue
88
- context+="[$created_at] "
95
+ origin_label=$(eagle_agent_label "$summary_agent")
96
+ context+="[$created_at][$origin_label] "
89
97
  [ -n "$req" ] && context+="$req"
90
98
  [ -n "$completed" ] && context+=" → $completed"
91
99
  [ -n "$learned" ] && context+=" (Learned: $learned)"
@@ -126,5 +134,5 @@ IMPORTANT: When Eagle Mem finds relevant memories or code for the user's prompt,
126
134
  === Eagle Mem: Persistent Memory ===
127
135
  "
128
136
 
129
- echo "$context"
137
+ eagle_emit_context_for_agent "$agent" "UserPromptSubmit" "$context"
130
138
  exit 0
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Codex hook registration helpers
4
+ # Shared by install.sh, update.sh, and uninstall.sh
5
+ # ═══════════════════════════════════════════════════════════
6
+ [ -n "${_EAGLE_CODEX_HOOKS_LOADED:-}" ] && return 0
7
+ _EAGLE_CODEX_HOOKS_LOADED=1
8
+
9
+ eagle_enable_codex_hooks() {
10
+ local config="$EAGLE_CODEX_CONFIG"
11
+ mkdir -p "$(dirname "$config")"
12
+
13
+ if [ ! -f "$config" ]; then
14
+ cat > "$config" << 'TOML'
15
+ [features]
16
+ codex_hooks = true
17
+ TOML
18
+ chmod 600 "$config" 2>/dev/null || true
19
+ return 0
20
+ fi
21
+
22
+ local tmp
23
+ tmp=$(mktemp)
24
+ awk '
25
+ BEGIN { in_features=0; saw_features=0; saw_flag=0; inserted=0 }
26
+ /^[[:space:]]*\[features\][[:space:]]*$/ {
27
+ saw_features=1
28
+ in_features=1
29
+ print
30
+ next
31
+ }
32
+ /^[[:space:]]*\[/ && in_features {
33
+ if (!saw_flag && !inserted) {
34
+ print "codex_hooks = true"
35
+ inserted=1
36
+ }
37
+ in_features=0
38
+ }
39
+ in_features && /^[[:space:]]*codex_hooks[[:space:]]*=/ {
40
+ print "codex_hooks = true"
41
+ saw_flag=1
42
+ next
43
+ }
44
+ { print }
45
+ END {
46
+ if (in_features && !saw_flag && !inserted) {
47
+ print "codex_hooks = true"
48
+ inserted=1
49
+ }
50
+ if (!saw_features) {
51
+ print ""
52
+ print "[features]"
53
+ print "codex_hooks = true"
54
+ }
55
+ }
56
+ ' "$config" > "$tmp" && mv "$tmp" "$config"
57
+ chmod 600 "$config" 2>/dev/null || true
58
+ }
59
+
60
+ eagle_patch_codex_hook() {
61
+ local hooks_file="$1"
62
+ local event="$2"
63
+ local matcher="$3"
64
+ local command="$4"
65
+ local description="${5:-}"
66
+ local status_message="${6:-}"
67
+ local timeout="${7:-}"
68
+ local script_path="$command"
69
+ script_path="${script_path#EAGLE_AGENT_SOURCE=codex bash \"}"
70
+ script_path="${script_path#bash \"}"
71
+ script_path="${script_path%\"}"
72
+
73
+ mkdir -p "$(dirname "$hooks_file")"
74
+ if [ ! -f "$hooks_file" ]; then
75
+ printf '{"hooks":{}}\n' > "$hooks_file"
76
+ chmod 600 "$hooks_file" 2>/dev/null || true
77
+ fi
78
+
79
+ local match_query
80
+ if [ -n "$matcher" ]; then
81
+ match_query='.hooks[$event][]? | select(.matcher == $matcher and (.hooks[]?.command == $command))'
82
+ else
83
+ match_query='.hooks[$event][]? | select((.matcher == null or .matcher == "") and (.hooks[]?.command == $command))'
84
+ fi
85
+ if jq -e --arg event "$event" --arg matcher "$matcher" --arg command "$command" "$match_query" "$hooks_file" &>/dev/null; then
86
+ [ -n "$description" ] && eagle_ok "$description ${DIM}(already registered)${RESET}"
87
+ return 0
88
+ fi
89
+
90
+ if jq -e --arg event "$event" --arg matcher "$matcher" --arg script "$script_path" '
91
+ .hooks[$event][]?
92
+ | select((($matcher == "" and (.matcher == null or .matcher == "")) or .matcher == $matcher)
93
+ and any(.hooks[]?; (.command // "") | contains($script)))
94
+ ' "$hooks_file" &>/dev/null; then
95
+ local tmp_existing
96
+ tmp_existing=$(mktemp)
97
+ jq --arg event "$event" --arg matcher "$matcher" --arg script "$script_path" --arg command "$command" '
98
+ .hooks[$event] |= map(
99
+ if ((($matcher == "" and (.matcher == null or .matcher == "")) or .matcher == $matcher)
100
+ and any(.hooks[]?; (.command // "") | contains($script)))
101
+ then .hooks |= map(if ((.command // "") | contains($script)) then .command = $command else . end)
102
+ else .
103
+ end
104
+ )
105
+ ' "$hooks_file" > "$tmp_existing" && mv "$tmp_existing" "$hooks_file"
106
+ [ -n "$description" ] && eagle_ok "$description ${DIM}(updated)${RESET}"
107
+ return 0
108
+ fi
109
+
110
+ local entry
111
+ entry=$(jq -nc \
112
+ --arg m "$matcher" \
113
+ --arg c "$command" \
114
+ --arg s "$status_message" \
115
+ --arg timeout "$timeout" '
116
+ {
117
+ hooks: [
118
+ {
119
+ type: "command",
120
+ command: $c
121
+ }
122
+ + (if $s == "" then {} else {statusMessage: $s} end)
123
+ + (if $timeout == "" then {} else {timeout: ($timeout | tonumber)} end)
124
+ ]
125
+ }
126
+ + (if $m == "" then {} else {matcher: $m} end)')
127
+
128
+ local tmp
129
+ tmp=$(mktemp)
130
+ jq --argjson entry "$entry" ".hooks.${event} = ((.hooks.${event} // []) + [\$entry])" "$hooks_file" > "$tmp" && mv "$tmp" "$hooks_file"
131
+ chmod 600 "$hooks_file" 2>/dev/null || true
132
+ [ -n "$description" ] && eagle_ok "$description"
133
+ }
134
+
135
+ eagle_register_codex_hooks() {
136
+ eagle_enable_codex_hooks
137
+
138
+ eagle_patch_codex_hook "$EAGLE_CODEX_HOOKS" "SessionStart" "startup|resume|clear" \
139
+ "EAGLE_AGENT_SOURCE=codex bash \"$EAGLE_MEM_DIR/hooks/session-start.sh\"" \
140
+ "Codex SessionStart hook" \
141
+ "Loading Eagle Mem recall" \
142
+ "30"
143
+
144
+ eagle_patch_codex_hook "$EAGLE_CODEX_HOOKS" "UserPromptSubmit" "" \
145
+ "EAGLE_AGENT_SOURCE=codex bash \"$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh\"" \
146
+ "Codex UserPromptSubmit hook" \
147
+ "Searching Eagle Mem" \
148
+ "30"
149
+
150
+ eagle_patch_codex_hook "$EAGLE_CODEX_HOOKS" "PreToolUse" "^(Bash|exec_command|shell_command|unified_exec|apply_patch|Edit|Write)$" \
151
+ "EAGLE_AGENT_SOURCE=codex bash \"$EAGLE_MEM_DIR/hooks/pre-tool-use.sh\"" \
152
+ "Codex PreToolUse hook" \
153
+ "Checking Eagle Mem guardrails" \
154
+ "30"
155
+
156
+ eagle_patch_codex_hook "$EAGLE_CODEX_HOOKS" "PostToolUse" "^(Bash|exec_command|shell_command|unified_exec|apply_patch|Edit|Write)$" \
157
+ "EAGLE_AGENT_SOURCE=codex bash \"$EAGLE_MEM_DIR/hooks/post-tool-use.sh\"" \
158
+ "Codex PostToolUse hook" \
159
+ "Recording Eagle Mem observation" \
160
+ "30"
161
+
162
+ eagle_patch_codex_hook "$EAGLE_CODEX_HOOKS" "Stop" "" \
163
+ "EAGLE_AGENT_SOURCE=codex bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" \
164
+ "Codex Stop hook" \
165
+ "Saving Eagle Mem summary" \
166
+ "30"
167
+ }
168
+
169
+ eagle_remove_codex_hooks() {
170
+ local hooks_file="$EAGLE_CODEX_HOOKS"
171
+ [ -f "$hooks_file" ] || return 1
172
+ command -v jq &>/dev/null || return 1
173
+
174
+ local tmp
175
+ tmp=$(mktemp)
176
+ jq '
177
+ def without_eagle_mem_handlers:
178
+ .hooks = ((.hooks // [])
179
+ | map(select(((.command // "") | contains(".eagle-mem/hooks/")) | not)));
180
+
181
+ if .hooks then
182
+ .hooks |= with_entries(
183
+ .value = [
184
+ .value[]?
185
+ | without_eagle_mem_handlers
186
+ | select((.hooks // []) | length > 0)
187
+ ]
188
+ | select(.value != [])
189
+ )
190
+ else . end
191
+ | if .hooks == {} then del(.hooks) else . end
192
+ ' "$hooks_file" > "$tmp" && mv "$tmp" "$hooks_file"
193
+ chmod 600 "$hooks_file" 2>/dev/null || true
194
+ }