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.
@@ -2,7 +2,7 @@
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — PostToolUse hook
4
4
  # Fires after every tool use
5
- # Captures file read/write operations as lightweight observations
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
- # ─── Claude memory + plan mirror ─────────────────────────
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
- # ─── Stale memory hint ──────────────────────────────────
145
- # After editing a project file, FTS5-search memories for the filename.
146
- # If a memory mentions this file, remind Claude to check for staleness.
147
- case "$tool_name" in
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
- # ─── Decision + feature surfacing on Read ──────────────────
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"
@@ -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=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
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
- fname_like=$(eagle_like_escape "$fname_esc")
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
- cmd_esc=$(eagle_sql_escape "$base_cmd")
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"
@@ -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="$HOME/.claude/tasks/$session_id"
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
@@ -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
- eagle_db "UPDATE sessions SET status = 'abandoned'
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
- p_esc=$(eagle_sql_escape "$project")
72
-
73
- stat_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$p_esc';")
74
- stat_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc';")
75
- stat_memories=$(eagle_db "SELECT COUNT(*) FROM claude_memories WHERE project = '$p_esc';")
76
- stat_tasks_pending=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'pending';")
77
- stat_tasks_progress=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'in_progress';")
78
- stat_tasks_done=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'completed';")
79
- stat_chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$p_esc';")
80
- stat_observations=$(eagle_db "SELECT COUNT(*) FROM observations WHERE session_id IN (SELECT id FROM sessions WHERE project = '$p_esc');")
81
- stat_plans=$(eagle_db "SELECT COUNT(*) FROM claude_plans WHERE project = '$p_esc';")
82
- stat_last_active=$(eagle_db "SELECT COALESCE(MAX(date(COALESCE(last_activity_at, started_at))), 'never') FROM sessions WHERE project = '$p_esc';")
83
- stat_last_summary=$(eagle_db "SELECT request FROM summaries WHERE project = '$p_esc' ORDER BY created_at DESC LIMIT 1;")
84
-
85
- # Trim to defaults
86
- stat_sessions="${stat_sessions:-0}"
87
- stat_summaries="${stat_summaries:-0}"
88
- stat_memories="${stat_memories:-0}"
89
- stat_tasks_pending="${stat_tasks_pending:-0}"
90
- stat_tasks_progress="${stat_tasks_progress:-0}"
91
- stat_tasks_done="${stat_tasks_done:-0}"
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 ($stat_summaries with summaries)
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=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE session_id = '$(eagle_sql_escape "$session_id")';")
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
+ }