eagle-mem 3.0.1 → 3.0.2
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/hooks/post-tool-use.sh +11 -146
- package/hooks/pre-tool-use.sh +5 -25
- package/hooks/session-end.sh +1 -1
- package/hooks/session-start.sh +23 -29
- package/hooks/stop.sh +1 -1
- package/lib/common.sh +3 -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 +68 -0
- package/lib/db-summaries.sh +142 -0
- package/lib/db.sh +14 -737
- package/lib/hooks-posttool.sh +138 -0
- package/package.json +1 -1
- package/scripts/memories.sh +3 -3
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
|
|
@@ -35,6 +36,9 @@ project=$(eagle_project_from_cwd "$cwd")
|
|
|
35
36
|
# PostToolUse can race SessionStart — the session row might not exist yet.
|
|
36
37
|
eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
|
|
37
38
|
|
|
39
|
+
# ─── Extract observation data from tool call ──────────────
|
|
40
|
+
|
|
41
|
+
fp=""
|
|
38
42
|
files_read="[]"
|
|
39
43
|
files_modified="[]"
|
|
40
44
|
tool_summary=""
|
|
@@ -63,14 +67,12 @@ case "$tool_name" in
|
|
|
63
67
|
cmd=$(echo "$cmd" | eagle_redact)
|
|
64
68
|
tool_summary="Bash: $cmd"
|
|
65
69
|
|
|
66
|
-
# Output metrics
|
|
67
70
|
tool_output=$(echo "$input" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
|
|
68
71
|
if [ -n "$tool_output" ]; then
|
|
69
72
|
output_bytes=${#tool_output}
|
|
70
73
|
output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
|
|
71
74
|
fi
|
|
72
75
|
|
|
73
|
-
# Command category extraction
|
|
74
76
|
first_word=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
|
|
75
77
|
case "$first_word" in
|
|
76
78
|
git|gh) command_category="git" ;;
|
|
@@ -95,151 +97,14 @@ case "$tool_name" in
|
|
|
95
97
|
;;
|
|
96
98
|
esac
|
|
97
99
|
|
|
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
|
|
100
|
+
# ─── Dispatch to extracted responsibilities ───────────────
|
|
143
101
|
|
|
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
|
|
102
|
+
eagle_posttool_mirror_writes "$tool_name" "$fp" "$session_id" "$project"
|
|
103
|
+
eagle_posttool_mirror_tasks "$tool_name" "$session_id" "$project" "$input"
|
|
104
|
+
eagle_posttool_stale_hint "$tool_name" "$fp" "$project"
|
|
105
|
+
eagle_posttool_decision_surface "$tool_name" "$fp" "$project"
|
|
177
106
|
|
|
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
|
|
107
|
+
# ─── Record observation ──────────────────────────────────
|
|
243
108
|
|
|
244
109
|
if ! eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"; then
|
|
245
110
|
eagle_log "ERROR" "PostToolUse: observation insert failed for session=$session_id tool=$tool_name"
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -27,7 +27,6 @@ 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
|
-
p_esc=$(eagle_sql_escape "$project")
|
|
31
30
|
|
|
32
31
|
context=""
|
|
33
32
|
|
|
@@ -35,7 +34,7 @@ context=""
|
|
|
35
34
|
|
|
36
35
|
case "$cmd" in
|
|
37
36
|
*"git push"*|*"gh pr create"*)
|
|
38
|
-
has_features=$(
|
|
37
|
+
has_features=$(eagle_count_active_features "$project")
|
|
39
38
|
if [ "${has_features:-0}" -gt 0 ]; then
|
|
40
39
|
changed_files=""
|
|
41
40
|
if [ -n "$cwd" ] && [ -d "$cwd" ]; then
|
|
@@ -49,19 +48,8 @@ case "$cmd" in
|
|
|
49
48
|
[ -z "$changed_file" ] && continue
|
|
50
49
|
fname=$(basename "$changed_file")
|
|
51
50
|
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 '\\');")
|
|
51
|
+
|
|
52
|
+
feature_hits=$(eagle_find_feature_for_push "$project" "$fname_esc")
|
|
65
53
|
|
|
66
54
|
while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
|
|
67
55
|
[ -z "$feat_name" ] && continue
|
|
@@ -93,16 +81,8 @@ esac
|
|
|
93
81
|
|
|
94
82
|
# Extract the base command for rule matching
|
|
95
83
|
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;")
|
|
84
|
+
|
|
85
|
+
rule=$(eagle_get_command_rule "$project" "$base_cmd")
|
|
106
86
|
|
|
107
87
|
if [ -n "$rule" ]; then
|
|
108
88
|
IFS='|' read -r strategy max_lines reason <<< "$rule"
|
package/hooks/session-end.sh
CHANGED
|
@@ -25,7 +25,7 @@ project=$(eagle_project_from_cwd "$cwd")
|
|
|
25
25
|
# Final sweep: re-capture all task files to catch status changes
|
|
26
26
|
# Claude Code may update task status without triggering PostToolUse
|
|
27
27
|
if eagle_validate_session_id "$session_id"; then
|
|
28
|
-
task_dir="$
|
|
28
|
+
task_dir="$EAGLE_CLAUDE_TASKS_DIR/$session_id"
|
|
29
29
|
if [ -d "$task_dir" ]; then
|
|
30
30
|
for task_file in "$task_dir"/*.json; do
|
|
31
31
|
[ ! -f "$task_file" ] && continue
|
package/hooks/session-start.sh
CHANGED
|
@@ -33,10 +33,7 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
|
|
|
33
33
|
# ─── Sweep stuck sessions (no activity for 7 days) ─────────
|
|
34
34
|
# Uses last_activity_at (updated by trigger on every observation insert)
|
|
35
35
|
# 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');"
|
|
36
|
+
eagle_abandon_stale_sessions "$session_id"
|
|
40
37
|
|
|
41
38
|
# ─── Version check (non-blocking) ────────────────────────────
|
|
42
39
|
|
|
@@ -68,30 +65,27 @@ fi
|
|
|
68
65
|
|
|
69
66
|
# ─── Gather stats ───────────────────────────────────────────
|
|
70
67
|
|
|
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}"
|
|
68
|
+
stat_sessions=0; stat_summaries=0; stat_with_summaries=0; stat_memories=0
|
|
69
|
+
stat_tasks_pending=0; stat_tasks_progress=0; stat_tasks_done=0
|
|
70
|
+
stat_chunks=0; stat_observations=0; stat_plans=0
|
|
71
|
+
stat_last_active="never"; stat_last_summary=""
|
|
72
|
+
|
|
73
|
+
while IFS='|' read -r key val; do
|
|
74
|
+
case "$key" in
|
|
75
|
+
sessions) stat_sessions="$val" ;;
|
|
76
|
+
summaries) stat_summaries="$val" ;;
|
|
77
|
+
with_summaries) stat_with_summaries="$val" ;;
|
|
78
|
+
memories) stat_memories="$val" ;;
|
|
79
|
+
plans) stat_plans="$val" ;;
|
|
80
|
+
tasks_pending) stat_tasks_pending="$val" ;;
|
|
81
|
+
tasks_progress) stat_tasks_progress="$val" ;;
|
|
82
|
+
tasks_done) stat_tasks_done="$val" ;;
|
|
83
|
+
chunks) stat_chunks="$val" ;;
|
|
84
|
+
observations) stat_observations="$val" ;;
|
|
85
|
+
last_active) stat_last_active="$val" ;;
|
|
86
|
+
last_summary) stat_last_summary="$val" ;;
|
|
87
|
+
esac
|
|
88
|
+
done <<< "$(eagle_get_project_stats "$project")"
|
|
95
89
|
|
|
96
90
|
# Build task summary line
|
|
97
91
|
task_parts=""
|
|
@@ -117,7 +111,7 @@ eagle_banner="======================================
|
|
|
117
111
|
Eagle Mem Loaded
|
|
118
112
|
======================================
|
|
119
113
|
Project | $project
|
|
120
|
-
Sessions | $stat_sessions total ($
|
|
114
|
+
Sessions | $stat_sessions total ($stat_with_summaries with summaries)
|
|
121
115
|
Memories | $stat_memories stored
|
|
122
116
|
Plans | $stat_plans saved
|
|
123
117
|
Tasks | $task_parts
|
package/hooks/stop.sh
CHANGED
|
@@ -116,7 +116,7 @@ fi
|
|
|
116
116
|
if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
117
117
|
# Skip heuristic if we already have a summary for this session.
|
|
118
118
|
# Stop fires every turn -- without this guard, each turn creates a duplicate row.
|
|
119
|
-
existing_count=$(
|
|
119
|
+
existing_count=$(eagle_count_session_summaries "$session_id")
|
|
120
120
|
if [ "${existing_count:-0}" -gt 0 ]; then
|
|
121
121
|
eagle_log "INFO" "Stop: skipping heuristic — summary already exists for session=$session_id (count=$existing_count)"
|
|
122
122
|
else
|
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"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Backfill + orphan cleanup helpers
|
|
4
|
+
# ═══════════════════════════════════════════════════════════
|
|
5
|
+
[ -n "${_EAGLE_DB_BACKFILL_LOADED:-}" ] && return 0
|
|
6
|
+
_EAGLE_DB_BACKFILL_LOADED=1
|
|
7
|
+
|
|
8
|
+
eagle_build_session_project_map() {
|
|
9
|
+
local claude_projects_dir="$EAGLE_CLAUDE_PROJECTS_DIR"
|
|
10
|
+
[ ! -d "$claude_projects_dir" ] && return 0
|
|
11
|
+
|
|
12
|
+
for proj_dir in "$claude_projects_dir"/*/; do
|
|
13
|
+
[ ! -d "$proj_dir" ] && continue
|
|
14
|
+
|
|
15
|
+
local project=""
|
|
16
|
+
local sample_jsonl
|
|
17
|
+
sample_jsonl=$(ls "$proj_dir"*.jsonl 2>/dev/null | head -1)
|
|
18
|
+
if [ -n "$sample_jsonl" ] && [ -f "$sample_jsonl" ]; then
|
|
19
|
+
local cwd
|
|
20
|
+
cwd=$(head -10 "$sample_jsonl" | jq -r 'select(.cwd != null) | .cwd' 2>/dev/null | head -1)
|
|
21
|
+
if [ -n "$cwd" ]; then
|
|
22
|
+
project=$(eagle_project_from_cwd "$cwd")
|
|
23
|
+
fi
|
|
24
|
+
fi
|
|
25
|
+
[ -z "$project" ] && continue
|
|
26
|
+
|
|
27
|
+
for jsonl in "$proj_dir"*.jsonl; do
|
|
28
|
+
[ ! -f "$jsonl" ] && continue
|
|
29
|
+
local sid
|
|
30
|
+
sid=$(basename "$jsonl" .jsonl)
|
|
31
|
+
echo "$sid|$project"
|
|
32
|
+
done
|
|
33
|
+
done
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
eagle_backfill_projects() {
|
|
37
|
+
local updated=0
|
|
38
|
+
local map
|
|
39
|
+
map=$(eagle_build_session_project_map)
|
|
40
|
+
[ -z "$map" ] && echo "0" && return 0
|
|
41
|
+
|
|
42
|
+
while IFS='|' read -r sid project; do
|
|
43
|
+
[ -z "$sid" ] || [ -z "$project" ] && continue
|
|
44
|
+
local sid_sql proj_sql
|
|
45
|
+
sid_sql=$(eagle_sql_escape "$sid")
|
|
46
|
+
proj_sql=$(eagle_sql_escape "$project")
|
|
47
|
+
|
|
48
|
+
# All six tables updated atomically per session to prevent
|
|
49
|
+
# partial backfill if the process is interrupted.
|
|
50
|
+
# Note: total_changes() includes FTS trigger changes, so the
|
|
51
|
+
# reported count may be higher than actual rows updated.
|
|
52
|
+
local ch
|
|
53
|
+
ch=$(eagle_db_pipe <<SQL
|
|
54
|
+
BEGIN;
|
|
55
|
+
UPDATE sessions SET project = '$proj_sql' WHERE id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
|
|
56
|
+
UPDATE claude_tasks SET project = '$proj_sql' WHERE source_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
|
|
57
|
+
UPDATE claude_memories SET project = '$proj_sql' WHERE origin_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
|
|
58
|
+
UPDATE claude_plans SET project = '$proj_sql' WHERE origin_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
|
|
59
|
+
UPDATE summaries SET project = '$proj_sql' WHERE session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
|
|
60
|
+
UPDATE observations SET project = '$proj_sql' WHERE session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
|
|
61
|
+
SELECT total_changes();
|
|
62
|
+
COMMIT;
|
|
63
|
+
SQL
|
|
64
|
+
)
|
|
65
|
+
[ "${ch:-0}" -gt 0 ] && updated=$((updated + ch))
|
|
66
|
+
done <<< "$map"
|
|
67
|
+
|
|
68
|
+
echo "$updated"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
eagle_prune_orphan_chunks() {
|
|
72
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
73
|
+
local target_dir="$2"
|
|
74
|
+
|
|
75
|
+
local paths
|
|
76
|
+
paths=$(eagle_db "SELECT DISTINCT file_path FROM code_chunks WHERE project = '$project';")
|
|
77
|
+
|
|
78
|
+
local removed=0
|
|
79
|
+
local txn_sql="BEGIN;"
|
|
80
|
+
while IFS= read -r fpath; do
|
|
81
|
+
[ -z "$fpath" ] && continue
|
|
82
|
+
if [ ! -f "$target_dir/$fpath" ]; then
|
|
83
|
+
local fpath_sql; fpath_sql=$(eagle_sql_escape "$fpath")
|
|
84
|
+
txn_sql+="
|
|
85
|
+
DELETE FROM code_chunks WHERE project = '$project' AND file_path = '$fpath_sql';"
|
|
86
|
+
removed=$((removed + 1))
|
|
87
|
+
fi
|
|
88
|
+
done <<< "$paths"
|
|
89
|
+
txn_sql+="
|
|
90
|
+
COMMIT;"
|
|
91
|
+
|
|
92
|
+
if [ "$removed" -gt 0 ]; then
|
|
93
|
+
eagle_db_pipe <<< "$txn_sql"
|
|
94
|
+
fi
|
|
95
|
+
echo "$removed"
|
|
96
|
+
}
|
package/lib/db-core.sh
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Database primitives
|
|
4
|
+
# ═══════════════════════════════════════════════════════════
|
|
5
|
+
[ -n "${_EAGLE_DB_CORE_LOADED:-}" ] && return 0
|
|
6
|
+
_EAGLE_DB_CORE_LOADED=1
|
|
7
|
+
|
|
8
|
+
EAGLE_DB_SETUP=".headers off
|
|
9
|
+
.output /dev/null
|
|
10
|
+
PRAGMA journal_mode=WAL;
|
|
11
|
+
PRAGMA synchronous=NORMAL;
|
|
12
|
+
PRAGMA busy_timeout=5000;
|
|
13
|
+
PRAGMA foreign_keys=ON;
|
|
14
|
+
PRAGMA trusted_schema=ON;
|
|
15
|
+
.output stdout"
|
|
16
|
+
|
|
17
|
+
eagle_db() {
|
|
18
|
+
local _eagle_db_err
|
|
19
|
+
_eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_err.$$")
|
|
20
|
+
local _eagle_db_out
|
|
21
|
+
_eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
|
|
22
|
+
local _eagle_db_rc=$?
|
|
23
|
+
if [ -s "$_eagle_db_err" ]; then
|
|
24
|
+
cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
|
|
25
|
+
fi
|
|
26
|
+
rm -f "$_eagle_db_err" 2>/dev/null
|
|
27
|
+
[ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
|
|
28
|
+
return $_eagle_db_rc
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
eagle_db_pipe() {
|
|
32
|
+
local _eagle_db_err
|
|
33
|
+
_eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_pipe_err.$$")
|
|
34
|
+
local _eagle_db_out
|
|
35
|
+
_eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
|
|
36
|
+
local _eagle_db_rc=$?
|
|
37
|
+
if [ -s "$_eagle_db_err" ]; then
|
|
38
|
+
cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
|
|
39
|
+
fi
|
|
40
|
+
rm -f "$_eagle_db_err" 2>/dev/null
|
|
41
|
+
[ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
|
|
42
|
+
return $_eagle_db_rc
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
eagle_db_json() {
|
|
46
|
+
local _eagle_db_err
|
|
47
|
+
_eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_json_err.$$")
|
|
48
|
+
local _eagle_db_out
|
|
49
|
+
_eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
|
|
50
|
+
local _eagle_db_rc=$?
|
|
51
|
+
if [ -s "$_eagle_db_err" ]; then
|
|
52
|
+
cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
|
|
53
|
+
fi
|
|
54
|
+
rm -f "$_eagle_db_err" 2>/dev/null
|
|
55
|
+
[ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
|
|
56
|
+
return $_eagle_db_rc
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
eagle_ensure_db() {
|
|
60
|
+
if [ ! -f "$EAGLE_MEM_DB" ]; then
|
|
61
|
+
local script_dir
|
|
62
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../db" && pwd)"
|
|
63
|
+
"$script_dir/migrate.sh"
|
|
64
|
+
fi
|
|
65
|
+
}
|