eagle-mem 3.0.1 → 3.1.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.
- package/bin/eagle-mem +1 -0
- package/db/016_eagle_meta.sql +8 -0
- package/hooks/post-tool-use.sh +12 -146
- package/hooks/pre-tool-use.sh +6 -25
- package/hooks/session-end.sh +24 -2
- package/hooks/session-start.sh +35 -29
- package/hooks/stop.sh +62 -10
- package/lib/common.sh +15 -0
- package/lib/db-backfill.sh +96 -0
- package/lib/db-core.sh +65 -0
- package/lib/db-features.sh +160 -0
- package/lib/db-mirrors.sh +264 -0
- package/lib/db-observations.sh +77 -0
- package/lib/db-sessions.sh +99 -0
- package/lib/db-summaries.sh +153 -0
- package/lib/db.sh +14 -737
- package/lib/hooks-posttool.sh +138 -0
- package/lib/provider.sh +2 -1
- package/package.json +1 -1
- package/scripts/curate.sh +1 -0
- package/scripts/health.sh +218 -0
- package/scripts/help.sh +1 -0
- package/scripts/memories.sh +3 -3
package/bin/eagle-mem
CHANGED
|
@@ -30,6 +30,7 @@ case "$command" in
|
|
|
30
30
|
config) bash "$SCRIPTS_DIR/config.sh" "$@" ;;
|
|
31
31
|
curate) bash "$SCRIPTS_DIR/curate.sh" "$@" ;;
|
|
32
32
|
feature) bash "$SCRIPTS_DIR/feature.sh" "$@" ;;
|
|
33
|
+
health) bash "$SCRIPTS_DIR/health.sh" "$@" ;;
|
|
33
34
|
help|--help|-h)
|
|
34
35
|
bash "$SCRIPTS_DIR/help.sh" ;;
|
|
35
36
|
version|--version|-v|-V)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Eagle meta key-value store for system state (curator timestamps, etc.)
|
|
2
|
+
CREATE TABLE IF NOT EXISTS eagle_meta (
|
|
3
|
+
key TEXT NOT NULL,
|
|
4
|
+
project TEXT,
|
|
5
|
+
value TEXT NOT NULL,
|
|
6
|
+
updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
7
|
+
UNIQUE(key, project)
|
|
8
|
+
);
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ═══════════════════════════════════════════════════════════
|
|
3
3
|
# Eagle Mem — PostToolUse hook
|
|
4
4
|
# Fires after every tool use
|
|
5
|
-
# Captures
|
|
5
|
+
# Captures observations + dispatches to extracted responsibilities
|
|
6
6
|
# ═══════════════════════════════════════════════════════════
|
|
7
7
|
set +e
|
|
8
8
|
|
|
@@ -11,6 +11,7 @@ LIB_DIR="$SCRIPT_DIR/../lib"
|
|
|
11
11
|
|
|
12
12
|
. "$LIB_DIR/common.sh"
|
|
13
13
|
. "$LIB_DIR/db.sh"
|
|
14
|
+
. "$LIB_DIR/hooks-posttool.sh"
|
|
14
15
|
|
|
15
16
|
input=$(eagle_read_stdin)
|
|
16
17
|
[ -z "$input" ] && exit 0
|
|
@@ -30,11 +31,15 @@ esac
|
|
|
30
31
|
[ ! -f "$EAGLE_MEM_DB" ] && exit 0
|
|
31
32
|
|
|
32
33
|
project=$(eagle_project_from_cwd "$cwd")
|
|
34
|
+
[ -z "$project" ] && exit 0
|
|
33
35
|
|
|
34
36
|
# Ensure session row exists before inserting observations (FK constraint).
|
|
35
37
|
# PostToolUse can race SessionStart — the session row might not exist yet.
|
|
36
38
|
eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
|
|
37
39
|
|
|
40
|
+
# ─── Extract observation data from tool call ──────────────
|
|
41
|
+
|
|
42
|
+
fp=""
|
|
38
43
|
files_read="[]"
|
|
39
44
|
files_modified="[]"
|
|
40
45
|
tool_summary=""
|
|
@@ -63,14 +68,12 @@ case "$tool_name" in
|
|
|
63
68
|
cmd=$(echo "$cmd" | eagle_redact)
|
|
64
69
|
tool_summary="Bash: $cmd"
|
|
65
70
|
|
|
66
|
-
# Output metrics
|
|
67
71
|
tool_output=$(echo "$input" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
|
|
68
72
|
if [ -n "$tool_output" ]; then
|
|
69
73
|
output_bytes=${#tool_output}
|
|
70
74
|
output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
|
|
71
75
|
fi
|
|
72
76
|
|
|
73
|
-
# Command category extraction
|
|
74
77
|
first_word=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
|
|
75
78
|
case "$first_word" in
|
|
76
79
|
git|gh) command_category="git" ;;
|
|
@@ -95,151 +98,14 @@ case "$tool_name" in
|
|
|
95
98
|
;;
|
|
96
99
|
esac
|
|
97
100
|
|
|
98
|
-
# ───
|
|
99
|
-
# Intercept writes to Claude Code's auto-memory and plan files
|
|
100
|
-
case "$tool_name" in
|
|
101
|
-
Write|Edit)
|
|
102
|
-
if [ -n "$fp" ]; then
|
|
103
|
-
# Reject path traversal: bash case `*` matches `/`, so
|
|
104
|
-
# patterns like projects/*/memory/*.md would match paths
|
|
105
|
-
# containing /../ segments. Block any path with `..` first.
|
|
106
|
-
case "$fp" in
|
|
107
|
-
*..*) ;; # path traversal — skip
|
|
108
|
-
"$HOME/.claude/projects"/*/memory/*.md)
|
|
109
|
-
mem_base=$(basename "$fp")
|
|
110
|
-
if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
|
|
111
|
-
eagle_capture_claude_memory "$fp" "$session_id" "$project"
|
|
112
|
-
fi
|
|
113
|
-
;;
|
|
114
|
-
"$HOME/.claude/plans/"*.md)
|
|
115
|
-
if [ -f "$fp" ]; then
|
|
116
|
-
eagle_capture_claude_plan "$fp" "$session_id" "$project"
|
|
117
|
-
fi
|
|
118
|
-
;;
|
|
119
|
-
esac
|
|
120
|
-
fi
|
|
121
|
-
;;
|
|
122
|
-
esac
|
|
123
|
-
|
|
124
|
-
# ─── Claude task mirror ─────────────────────────────────
|
|
125
|
-
# Intercept TaskCreate/TaskUpdate and capture the resulting JSON files
|
|
126
|
-
case "$tool_name" in
|
|
127
|
-
TaskCreate|TaskUpdate)
|
|
128
|
-
if eagle_validate_session_id "$session_id"; then
|
|
129
|
-
task_dir="$HOME/.claude/tasks/$session_id"
|
|
130
|
-
if [ -d "$task_dir" ]; then
|
|
131
|
-
task_id=$(echo "$input" | jq -r '.tool_input.id // empty')
|
|
132
|
-
if [ -z "$task_id" ]; then
|
|
133
|
-
newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
|
|
134
|
-
[ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
|
|
135
|
-
elif eagle_validate_session_id "$task_id"; then
|
|
136
|
-
task_json="$task_dir/$task_id.json"
|
|
137
|
-
[ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
|
|
138
|
-
fi
|
|
139
|
-
fi
|
|
140
|
-
fi
|
|
141
|
-
;;
|
|
142
|
-
esac
|
|
101
|
+
# ─── Dispatch to extracted responsibilities ───────────────
|
|
143
102
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Write|Edit)
|
|
149
|
-
if [ -n "$fp" ]; then
|
|
150
|
-
fname=$(basename "$fp")
|
|
151
|
-
fname_stem="${fname%.*}"
|
|
152
|
-
case "$fp" in
|
|
153
|
-
"$HOME/.claude/"*) ;; # skip Claude config files
|
|
154
|
-
*)
|
|
155
|
-
if [ ${#fname_stem} -ge 3 ]; then
|
|
156
|
-
fts_query=$(eagle_fts_sanitize "$fname_stem")
|
|
157
|
-
if [ -n "$fts_query" ]; then
|
|
158
|
-
fts_esc=$(eagle_sql_escape "$fts_query")
|
|
159
|
-
p_esc=$(eagle_sql_escape "$project")
|
|
160
|
-
stale_hit=$(eagle_db "SELECT m.memory_name
|
|
161
|
-
FROM claude_memories m
|
|
162
|
-
JOIN claude_memories_fts f ON f.rowid = m.id
|
|
163
|
-
WHERE claude_memories_fts MATCH '$fts_esc'
|
|
164
|
-
AND m.project = '$p_esc'
|
|
165
|
-
LIMIT 1;")
|
|
166
|
-
if [ -n "$stale_hit" ]; then
|
|
167
|
-
stale_msg="Eagle Mem: Memory '${stale_hit}' may reference '${fname}'. If your edit contradicts it, update the memory."
|
|
168
|
-
jq -nc --arg ctx "$stale_msg" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
|
|
169
|
-
fi
|
|
170
|
-
fi
|
|
171
|
-
fi
|
|
172
|
-
;;
|
|
173
|
-
esac
|
|
174
|
-
fi
|
|
175
|
-
;;
|
|
176
|
-
esac
|
|
103
|
+
eagle_posttool_mirror_writes "$tool_name" "$fp" "$session_id" "$project"
|
|
104
|
+
eagle_posttool_mirror_tasks "$tool_name" "$session_id" "$project" "$input"
|
|
105
|
+
eagle_posttool_stale_hint "$tool_name" "$fp" "$project"
|
|
106
|
+
eagle_posttool_decision_surface "$tool_name" "$fp" "$project"
|
|
177
107
|
|
|
178
|
-
# ───
|
|
179
|
-
# When Claude reads a file, surface past decisions and feature pipeline context.
|
|
180
|
-
case "$tool_name" in
|
|
181
|
-
Read)
|
|
182
|
-
if [ -n "$fp" ]; then
|
|
183
|
-
fname=$(basename "$fp")
|
|
184
|
-
fname_stem="${fname%.*}"
|
|
185
|
-
read_context=""
|
|
186
|
-
case "$fp" in
|
|
187
|
-
"$HOME/.claude/"*) ;; # skip Claude config files
|
|
188
|
-
*)
|
|
189
|
-
p_esc=$(eagle_sql_escape "$project")
|
|
190
|
-
|
|
191
|
-
# Decision history from summaries
|
|
192
|
-
if [ ${#fname_stem} -ge 3 ]; then
|
|
193
|
-
fts_query=$(eagle_fts_sanitize "$fname_stem")
|
|
194
|
-
if [ -n "$fts_query" ]; then
|
|
195
|
-
fts_esc=$(eagle_sql_escape "$fts_query")
|
|
196
|
-
decision_hit=$(eagle_db "SELECT s.decisions
|
|
197
|
-
FROM summaries s
|
|
198
|
-
JOIN summaries_fts f ON f.rowid = s.id
|
|
199
|
-
WHERE summaries_fts MATCH '$fts_esc'
|
|
200
|
-
AND s.project = '$p_esc'
|
|
201
|
-
AND s.decisions IS NOT NULL
|
|
202
|
-
AND s.decisions != ''
|
|
203
|
-
ORDER BY s.created_at DESC
|
|
204
|
-
LIMIT 1;")
|
|
205
|
-
if [ -n "$decision_hit" ]; then
|
|
206
|
-
read_context+="Eagle Mem decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request. "
|
|
207
|
-
fi
|
|
208
|
-
fi
|
|
209
|
-
fi
|
|
210
|
-
|
|
211
|
-
# Feature pipeline context
|
|
212
|
-
feature_hit=$(eagle_find_features_for_file "$project" "$fp")
|
|
213
|
-
if [ -n "$feature_hit" ]; then
|
|
214
|
-
while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
|
|
215
|
-
[ -z "$feat_name" ] && continue
|
|
216
|
-
read_context+="Eagle Mem: '${fname}' is part of feature '${feat_name}'"
|
|
217
|
-
[ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
|
|
218
|
-
read_context+="."
|
|
219
|
-
if [ -n "$feat_verified" ]; then
|
|
220
|
-
read_context+=" Last verified: ${feat_verified}."
|
|
221
|
-
fi
|
|
222
|
-
if [ -n "$feat_deps" ]; then
|
|
223
|
-
read_context+=" Dependencies: ${feat_deps}."
|
|
224
|
-
fi
|
|
225
|
-
if [ -n "$feat_other_files" ]; then
|
|
226
|
-
read_context+=" Other files in pipeline: ${feat_other_files}."
|
|
227
|
-
fi
|
|
228
|
-
if [ -n "$feat_smoke" ]; then
|
|
229
|
-
read_context+=" Smoke tests: ${feat_smoke}."
|
|
230
|
-
fi
|
|
231
|
-
read_context+=" Changes require re-testing after deploy. "
|
|
232
|
-
done <<< "$feature_hit"
|
|
233
|
-
fi
|
|
234
|
-
|
|
235
|
-
if [ -n "$read_context" ]; then
|
|
236
|
-
jq -nc --arg ctx "$read_context" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
|
|
237
|
-
fi
|
|
238
|
-
;;
|
|
239
|
-
esac
|
|
240
|
-
fi
|
|
241
|
-
;;
|
|
242
|
-
esac
|
|
108
|
+
# ─── Record observation ──────────────────────────────────
|
|
243
109
|
|
|
244
110
|
if ! eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"; then
|
|
245
111
|
eagle_log "ERROR" "PostToolUse: observation insert failed for session=$session_id tool=$tool_name"
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -27,7 +27,7 @@ cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
|
|
|
27
27
|
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
28
28
|
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
29
29
|
project=$(eagle_project_from_cwd "$cwd")
|
|
30
|
-
|
|
30
|
+
[ -z "$project" ] && exit 0
|
|
31
31
|
|
|
32
32
|
context=""
|
|
33
33
|
|
|
@@ -35,7 +35,7 @@ context=""
|
|
|
35
35
|
|
|
36
36
|
case "$cmd" in
|
|
37
37
|
*"git push"*|*"gh pr create"*)
|
|
38
|
-
has_features=$(
|
|
38
|
+
has_features=$(eagle_count_active_features "$project")
|
|
39
39
|
if [ "${has_features:-0}" -gt 0 ]; then
|
|
40
40
|
changed_files=""
|
|
41
41
|
if [ -n "$cwd" ] && [ -d "$cwd" ]; then
|
|
@@ -49,19 +49,8 @@ case "$cmd" in
|
|
|
49
49
|
[ -z "$changed_file" ] && continue
|
|
50
50
|
fname=$(basename "$changed_file")
|
|
51
51
|
fname_esc=$(eagle_sql_escape "$fname")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
feature_hits=$(eagle_db "SELECT DISTINCT f.name,
|
|
55
|
-
(SELECT GROUP_CONCAT(fst.command, '; ')
|
|
56
|
-
FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke,
|
|
57
|
-
(SELECT GROUP_CONCAT(fd.target || ':' || fd.name, ', ')
|
|
58
|
-
FROM feature_dependencies fd WHERE fd.feature_id = f.id) as deps,
|
|
59
|
-
f.last_verified_at
|
|
60
|
-
FROM features f
|
|
61
|
-
JOIN feature_files ff ON ff.feature_id = f.id
|
|
62
|
-
WHERE f.project = '$p_esc'
|
|
63
|
-
AND f.status = 'active'
|
|
64
|
-
AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\');")
|
|
52
|
+
|
|
53
|
+
feature_hits=$(eagle_find_feature_for_push "$project" "$fname_esc")
|
|
65
54
|
|
|
66
55
|
while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
|
|
67
56
|
[ -z "$feat_name" ] && continue
|
|
@@ -93,16 +82,8 @@ esac
|
|
|
93
82
|
|
|
94
83
|
# Extract the base command for rule matching
|
|
95
84
|
base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
rule=$(eagle_db "SELECT strategy, max_lines, reason
|
|
99
|
-
FROM command_rules
|
|
100
|
-
WHERE enabled = 1
|
|
101
|
-
AND (project = '$p_esc' OR project IS NULL)
|
|
102
|
-
AND ('$cmd_esc' LIKE pattern OR '$cmd_esc' = pattern)
|
|
103
|
-
ORDER BY
|
|
104
|
-
CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END
|
|
105
|
-
LIMIT 1;")
|
|
85
|
+
|
|
86
|
+
rule=$(eagle_get_command_rule "$project" "$base_cmd")
|
|
106
87
|
|
|
107
88
|
if [ -n "$rule" ]; then
|
|
108
89
|
IFS='|' read -r strategy max_lines reason <<< "$rule"
|
package/hooks/session-end.sh
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
# ═══════════════════════════════════════════════════════════
|
|
3
3
|
# Eagle Mem — SessionEnd hook
|
|
4
4
|
# Fires when the Claude Code session ends
|
|
5
|
-
# Marks the session as completed
|
|
5
|
+
# Marks the session as completed + triggers auto-curate
|
|
6
6
|
# ═══════════════════════════════════════════════════════════
|
|
7
7
|
set +e
|
|
8
8
|
|
|
9
9
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
10
|
LIB_DIR="$SCRIPT_DIR/../lib"
|
|
11
|
+
SCRIPTS_DIR="$SCRIPT_DIR/../scripts"
|
|
11
12
|
|
|
12
13
|
. "$LIB_DIR/common.sh"
|
|
13
14
|
. "$LIB_DIR/db.sh"
|
|
15
|
+
. "$LIB_DIR/provider.sh"
|
|
14
16
|
|
|
15
17
|
input=$(eagle_read_stdin)
|
|
16
18
|
[ -z "$input" ] && exit 0
|
|
@@ -21,11 +23,12 @@ session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
|
21
23
|
|
|
22
24
|
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
23
25
|
project=$(eagle_project_from_cwd "$cwd")
|
|
26
|
+
[ -z "$project" ] && exit 0
|
|
24
27
|
|
|
25
28
|
# Final sweep: re-capture all task files to catch status changes
|
|
26
29
|
# Claude Code may update task status without triggering PostToolUse
|
|
27
30
|
if eagle_validate_session_id "$session_id"; then
|
|
28
|
-
task_dir="$
|
|
31
|
+
task_dir="$EAGLE_CLAUDE_TASKS_DIR/$session_id"
|
|
29
32
|
if [ -d "$task_dir" ]; then
|
|
30
33
|
for task_file in "$task_dir"/*.json; do
|
|
31
34
|
[ ! -f "$task_file" ] && continue
|
|
@@ -41,4 +44,23 @@ eagle_log "INFO" "SessionEnd: session=$session_id marked completed"
|
|
|
41
44
|
# Prune observations older than 90 days (keeps DB size bounded)
|
|
42
45
|
eagle_prune_observations 90 "$project"
|
|
43
46
|
|
|
47
|
+
# ─── Auto-curate trigger ─────────────────────────────────
|
|
48
|
+
curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
|
|
49
|
+
if [ "$curator_schedule" = "auto" ]; then
|
|
50
|
+
provider=$(eagle_config_get "provider" "type" "none")
|
|
51
|
+
if [ "$provider" != "none" ]; then
|
|
52
|
+
min_sessions=$(eagle_config_get "curator" "min_sessions" "5")
|
|
53
|
+
min_sessions=$(eagle_sql_int "$min_sessions")
|
|
54
|
+
|
|
55
|
+
last_curated=$(eagle_meta_get "last_curated_at" "$project")
|
|
56
|
+
since="${last_curated:-1970-01-01T00:00:00Z}"
|
|
57
|
+
|
|
58
|
+
sessions_since=$(eagle_count_sessions_since "$project" "$since")
|
|
59
|
+
if [ "${sessions_since:-0}" -ge "$min_sessions" ]; then
|
|
60
|
+
eagle_log "INFO" "SessionEnd: auto-curate triggered (${sessions_since} sessions since last curate)"
|
|
61
|
+
nohup bash "$SCRIPTS_DIR/curate.sh" -p "$project" >> "$EAGLE_MEM_LOG" 2>&1 &
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
44
66
|
exit 0
|
package/hooks/session-start.sh
CHANGED
|
@@ -26,6 +26,9 @@ model=$(echo "$input" | jq -r '.model // empty')
|
|
|
26
26
|
|
|
27
27
|
project=$(eagle_project_from_cwd "$cwd")
|
|
28
28
|
|
|
29
|
+
# Skip ephemeral directories (tmp, Downloads, etc.) — no tracking
|
|
30
|
+
[ -z "$project" ] && exit 0
|
|
31
|
+
|
|
29
32
|
eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
|
|
30
33
|
|
|
31
34
|
eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
|
|
@@ -33,10 +36,7 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
|
|
|
33
36
|
# ─── Sweep stuck sessions (no activity for 7 days) ─────────
|
|
34
37
|
# Uses last_activity_at (updated by trigger on every observation insert)
|
|
35
38
|
# so long-lived sessions with regular compactions aren't falsely abandoned
|
|
36
|
-
|
|
37
|
-
WHERE status = 'active'
|
|
38
|
-
AND id != '$(eagle_sql_escape "$session_id")'
|
|
39
|
-
AND COALESCE(last_activity_at, started_at) < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days');"
|
|
39
|
+
eagle_abandon_stale_sessions "$session_id"
|
|
40
40
|
|
|
41
41
|
# ─── Version check (non-blocking) ────────────────────────────
|
|
42
42
|
|
|
@@ -68,30 +68,27 @@ fi
|
|
|
68
68
|
|
|
69
69
|
# ─── Gather stats ───────────────────────────────────────────
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
stat_chunks="${stat_chunks:-0}"
|
|
93
|
-
stat_observations="${stat_observations:-0}"
|
|
94
|
-
stat_plans="${stat_plans:-0}"
|
|
71
|
+
stat_sessions=0; stat_summaries=0; stat_with_summaries=0; stat_memories=0
|
|
72
|
+
stat_tasks_pending=0; stat_tasks_progress=0; stat_tasks_done=0
|
|
73
|
+
stat_chunks=0; stat_observations=0; stat_plans=0
|
|
74
|
+
stat_last_active="never"; stat_last_summary=""
|
|
75
|
+
|
|
76
|
+
while IFS='|' read -r key val; do
|
|
77
|
+
case "$key" in
|
|
78
|
+
sessions) stat_sessions="$val" ;;
|
|
79
|
+
summaries) stat_summaries="$val" ;;
|
|
80
|
+
with_summaries) stat_with_summaries="$val" ;;
|
|
81
|
+
memories) stat_memories="$val" ;;
|
|
82
|
+
plans) stat_plans="$val" ;;
|
|
83
|
+
tasks_pending) stat_tasks_pending="$val" ;;
|
|
84
|
+
tasks_progress) stat_tasks_progress="$val" ;;
|
|
85
|
+
tasks_done) stat_tasks_done="$val" ;;
|
|
86
|
+
chunks) stat_chunks="$val" ;;
|
|
87
|
+
observations) stat_observations="$val" ;;
|
|
88
|
+
last_active) stat_last_active="$val" ;;
|
|
89
|
+
last_summary) stat_last_summary="$val" ;;
|
|
90
|
+
esac
|
|
91
|
+
done <<< "$(eagle_get_project_stats "$project")"
|
|
95
92
|
|
|
96
93
|
# Build task summary line
|
|
97
94
|
task_parts=""
|
|
@@ -117,7 +114,7 @@ eagle_banner="======================================
|
|
|
117
114
|
Eagle Mem Loaded
|
|
118
115
|
======================================
|
|
119
116
|
Project | $project
|
|
120
|
-
Sessions | $stat_sessions total ($
|
|
117
|
+
Sessions | $stat_sessions total ($stat_with_summaries with summaries)
|
|
121
118
|
Memories | $stat_memories stored
|
|
122
119
|
Plans | $stat_plans saved
|
|
123
120
|
Tasks | $task_parts
|
|
@@ -140,6 +137,15 @@ if [ -n "$update_notice" ]; then
|
|
|
140
137
|
"
|
|
141
138
|
fi
|
|
142
139
|
|
|
140
|
+
# Nudge if last session lacked enrichment
|
|
141
|
+
last_enriched=$(eagle_last_session_enriched "$project")
|
|
142
|
+
if [ "${last_enriched:-1}" = "0" ] && [ "$stat_with_summaries" -gt 0 ]; then
|
|
143
|
+
context+="=== EAGLE MEM — Enrichment Reminder ===
|
|
144
|
+
The previous session's summary did NOT include decisions, gotchas, or key_files. These fields power Eagle Mem's self-learning (feature discovery, anti-regression, command intelligence). Please emit an <eagle-summary> block at the end of this session with these fields populated.
|
|
145
|
+
|
|
146
|
+
"
|
|
147
|
+
fi
|
|
148
|
+
|
|
143
149
|
# Project overview
|
|
144
150
|
overview=$(eagle_get_overview "$project")
|
|
145
151
|
if [ -n "$overview" ]; then
|
package/hooks/stop.sh
CHANGED
|
@@ -12,6 +12,7 @@ LIB_DIR="$SCRIPT_DIR/../lib"
|
|
|
12
12
|
|
|
13
13
|
. "$LIB_DIR/common.sh"
|
|
14
14
|
. "$LIB_DIR/db.sh"
|
|
15
|
+
. "$LIB_DIR/provider.sh"
|
|
15
16
|
|
|
16
17
|
eagle_ensure_db
|
|
17
18
|
|
|
@@ -29,6 +30,7 @@ agent_type=$(echo "$input" | jq -r '.agent_type // empty')
|
|
|
29
30
|
[ -n "$agent_type" ] && [ "$agent_type" != "main" ] && exit 0
|
|
30
31
|
|
|
31
32
|
project=$(eagle_project_from_cwd "$cwd")
|
|
33
|
+
[ -z "$project" ] && exit 0
|
|
32
34
|
|
|
33
35
|
eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path"
|
|
34
36
|
|
|
@@ -111,21 +113,25 @@ if [ -n "$summary_block" ]; then
|
|
|
111
113
|
eagle_log "INFO" "Stop: parsed eagle-summary block"
|
|
112
114
|
fi
|
|
113
115
|
|
|
114
|
-
# ───
|
|
116
|
+
# ─── Guard: skip fallback work if summary already exists ──
|
|
117
|
+
# Stop fires every assistant turn. Without this, the heuristic and LLM
|
|
118
|
+
# enrichment blocks fire on turn 2+ — wasting tokens and producing
|
|
119
|
+
# empty inserts that get rejected.
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
existing_count=0
|
|
122
|
+
if [ -z "$summary_block" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
123
|
+
existing_count=$(eagle_count_session_summaries "$session_id")
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ -z "$summary_block" ] && [ "${existing_count:-0}" -eq 0 ]; then
|
|
127
|
+
|
|
128
|
+
# ─── Heuristic fallback: extract from tool calls ───────────
|
|
129
|
+
|
|
130
|
+
if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
123
131
|
eagle_log "INFO" "Stop: no eagle-summary found, using heuristic fallback"
|
|
124
132
|
|
|
125
|
-
# Extract first user prompt as "request"
|
|
126
133
|
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 | head -1 | cut -c1-500)
|
|
127
134
|
|
|
128
|
-
# Extract files from Read/Write/Edit tool calls
|
|
129
135
|
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)
|
|
130
136
|
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)
|
|
131
137
|
|
|
@@ -138,6 +144,52 @@ if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ];
|
|
|
138
144
|
|
|
139
145
|
completed="(auto-captured from tool usage)"
|
|
140
146
|
fi
|
|
147
|
+
|
|
148
|
+
# ─── LLM enrichment: extract decisions/gotchas/key_files ──
|
|
149
|
+
|
|
150
|
+
if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ]; then
|
|
151
|
+
provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
|
|
152
|
+
if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
|
|
153
|
+
excerpt=$(echo "$text_content" | tail -c 2000)
|
|
154
|
+
|
|
155
|
+
enrich_prompt="Extract from this Claude Code session excerpt:
|
|
156
|
+
1. DECISIONS: architectural or design choices made (with WHY). One per line.
|
|
157
|
+
2. GOTCHAS: non-obvious pitfalls, bugs found, things that surprised. One per line.
|
|
158
|
+
3. KEY_FILES: important files that were central to the work. One per line.
|
|
159
|
+
|
|
160
|
+
SESSION EXCERPT:
|
|
161
|
+
$excerpt
|
|
162
|
+
|
|
163
|
+
Output EXACTLY this format (omit sections with nothing to report):
|
|
164
|
+
DECISIONS:
|
|
165
|
+
- <decision> — why: <reason>
|
|
166
|
+
GOTCHAS:
|
|
167
|
+
- <gotcha>
|
|
168
|
+
KEY_FILES:
|
|
169
|
+
- <filepath>"
|
|
170
|
+
|
|
171
|
+
enrich_result=$(eagle_llm_call "$enrich_prompt" "Extract structured facts from development sessions. Be concise. Only include items with clear evidence." 512 2>/dev/null) || true
|
|
172
|
+
|
|
173
|
+
if [ -n "$enrich_result" ]; then
|
|
174
|
+
extract_section() {
|
|
175
|
+
local result="$1" header="$2"
|
|
176
|
+
echo "$result" | awk -v h="$header:" '
|
|
177
|
+
$0 == h || $0 ~ "^"h { found=1; next }
|
|
178
|
+
found && /^[A-Z_]+:/ { exit }
|
|
179
|
+
found && /^- / { sub(/^- /, ""); lines[++n] = $0 }
|
|
180
|
+
END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
|
|
181
|
+
'
|
|
182
|
+
}
|
|
183
|
+
decisions=$(extract_section "$enrich_result" "DECISIONS")
|
|
184
|
+
gotchas=$(extract_section "$enrich_result" "GOTCHAS")
|
|
185
|
+
key_files=$(extract_section "$enrich_result" "KEY_FILES")
|
|
186
|
+
[ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ] && eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id"
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
elif [ -z "$summary_block" ] && [ "${existing_count:-0}" -gt 0 ]; then
|
|
192
|
+
eagle_log "INFO" "Stop: skipping fallback — summary already exists for session=$session_id (count=$existing_count)"
|
|
141
193
|
fi
|
|
142
194
|
|
|
143
195
|
# ─── Redact secrets from all text fields before storage ────
|
package/lib/common.sh
CHANGED
|
@@ -9,6 +9,9 @@ EAGLE_MEM_DB="$EAGLE_MEM_DIR/memory.db"
|
|
|
9
9
|
EAGLE_MEM_LOG="$EAGLE_MEM_DIR/eagle-mem.log"
|
|
10
10
|
EAGLE_SETTINGS="${EAGLE_SETTINGS:-$HOME/.claude/settings.json}"
|
|
11
11
|
EAGLE_SKILLS_DIR="$HOME/.claude/skills"
|
|
12
|
+
EAGLE_CLAUDE_PROJECTS_DIR="$HOME/.claude/projects"
|
|
13
|
+
EAGLE_CLAUDE_PLANS_DIR="$HOME/.claude/plans"
|
|
14
|
+
EAGLE_CLAUDE_TASKS_DIR="$HOME/.claude/tasks"
|
|
12
15
|
|
|
13
16
|
eagle_log() {
|
|
14
17
|
local level="$1"
|
|
@@ -22,6 +25,18 @@ eagle_log() {
|
|
|
22
25
|
|
|
23
26
|
eagle_project_from_cwd() {
|
|
24
27
|
local cwd="${1:-$(pwd)}"
|
|
28
|
+
local resolved="$cwd"
|
|
29
|
+
|
|
30
|
+
# Resolve /private/tmp → /tmp on macOS
|
|
31
|
+
case "$resolved" in /private/tmp*) resolved="/tmp${resolved#/private/tmp}" ;; esac
|
|
32
|
+
|
|
33
|
+
# Skip ephemeral directories — return empty so hooks early-exit
|
|
34
|
+
case "$resolved" in
|
|
35
|
+
/tmp|/tmp/*|/var/tmp|/var/tmp/*) echo ""; return ;;
|
|
36
|
+
"$HOME/Downloads"|"$HOME/Downloads/"*) echo ""; return ;;
|
|
37
|
+
"$HOME/Desktop"|"$HOME/Desktop/"*) echo ""; return ;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
25
40
|
local git_root
|
|
26
41
|
git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
|
|
27
42
|
if [ -n "$git_root" ]; then
|