eagle-mem 1.0.3 → 1.2.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.
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Prune
4
+ # Removes old observations and orphaned code chunks
5
+ # to keep the database lean
6
+ # ═══════════════════════════════════════════════════════════
7
+ set -euo pipefail
8
+
9
+ SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ LIB_DIR="$SCRIPTS_DIR/../lib"
11
+
12
+ . "$SCRIPTS_DIR/style.sh"
13
+ . "$LIB_DIR/common.sh"
14
+ . "$LIB_DIR/db.sh"
15
+
16
+ eagle_ensure_db
17
+
18
+ # ─── Parse arguments ──────────────────────────────────────
19
+
20
+ days=90
21
+ project=""
22
+ dry_run=false
23
+
24
+ while [ $# -gt 0 ]; do
25
+ case "$1" in
26
+ --days|-d) days="$2"; shift 2 ;;
27
+ --project|-p) project="$2"; shift 2 ;;
28
+ --dry-run|-n) dry_run=true; shift ;;
29
+ --help|-h)
30
+ echo -e " ${BOLD}eagle-mem prune${RESET} — Clean up old data"
31
+ echo ""
32
+ echo -e " ${BOLD}Usage:${RESET}"
33
+ echo -e " eagle-mem prune ${DIM}# prune observations > 90 days${RESET}"
34
+ echo -e " eagle-mem prune ${CYAN}--days 30${RESET} ${DIM}# prune observations > 30 days${RESET}"
35
+ echo -e " eagle-mem prune ${CYAN}--dry-run${RESET} ${DIM}# show what would be pruned${RESET}"
36
+ echo ""
37
+ echo -e " ${BOLD}What gets pruned:${RESET}"
38
+ echo -e " ${DOT} Observations older than --days (default: 90)"
39
+ echo -e " ${DOT} Code chunks for files that no longer exist"
40
+ echo ""
41
+ echo -e " ${BOLD}What is preserved:${RESET}"
42
+ echo -e " ${DOT} All sessions and summaries (your session history)"
43
+ echo -e " ${DOT} All tasks"
44
+ echo -e " ${DOT} Project overviews"
45
+ echo ""
46
+ echo -e " ${BOLD}Options:${RESET}"
47
+ echo -e " ${CYAN}-d, --days${RESET} <N> Age threshold (default: 90)"
48
+ echo -e " ${CYAN}-p, --project${RESET} <name> Only prune for this project"
49
+ echo -e " ${CYAN}-n, --dry-run${RESET} Show counts without deleting"
50
+ echo ""
51
+ exit 0
52
+ ;;
53
+ *)
54
+ eagle_err "Unknown option: $1"
55
+ exit 1
56
+ ;;
57
+ esac
58
+ done
59
+
60
+ if ! [[ "$days" =~ ^[0-9]+$ ]]; then
61
+ eagle_err "Days must be a number, got: $days"
62
+ exit 1
63
+ fi
64
+
65
+ eagle_header "Prune"
66
+
67
+ # ─── Count before ────────────────────────────────────────
68
+
69
+ total_obs=$(eagle_db "SELECT COUNT(*) FROM observations;")
70
+ total_chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks;")
71
+ eagle_info "Database: ${total_obs:-0} observations, ${total_chunks:-0} chunks"
72
+ echo ""
73
+
74
+ # ─── Old observations ───────────────────────────────────
75
+
76
+ obs_project_filter=""
77
+ if [ -n "$project" ]; then
78
+ obs_project_filter="AND session_id IN (SELECT id FROM sessions WHERE project = '$(eagle_sql_escape "$project")')"
79
+ fi
80
+
81
+ old_obs_count=$(eagle_db "SELECT COUNT(*) FROM observations WHERE created_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-$days days') $obs_project_filter;")
82
+
83
+ if [ "${old_obs_count:-0}" -gt 0 ]; then
84
+ if [ "$dry_run" = true ]; then
85
+ eagle_dim "Would prune $old_obs_count observations older than $days days"
86
+ else
87
+ eagle_prune_observations "$days" "$project"
88
+ eagle_ok "Pruned $old_obs_count observations older than $days days"
89
+ fi
90
+ else
91
+ eagle_ok "No observations older than $days days"
92
+ fi
93
+
94
+ # ─── Orphaned code chunks ───────────────────────────────
95
+
96
+ if [ -n "$project" ]; then
97
+ projects="$project"
98
+ else
99
+ projects=$(eagle_db "SELECT DISTINCT project FROM code_chunks;")
100
+ fi
101
+
102
+ orphan_total=0
103
+ if [ -n "$projects" ]; then
104
+ while IFS= read -r proj; do
105
+ [ -z "$proj" ] && continue
106
+
107
+ # Try to find the project directory
108
+ proj_cwd=$(eagle_db "SELECT cwd FROM sessions WHERE project = '$(eagle_sql_escape "$proj")' ORDER BY started_at DESC LIMIT 1;")
109
+
110
+ if [ -n "$proj_cwd" ] && [ -d "$proj_cwd" ]; then
111
+ if [ "$dry_run" = true ]; then
112
+ orphan_count=$(eagle_db "SELECT COUNT(DISTINCT file_path) FROM code_chunks WHERE project = '$(eagle_sql_escape "$proj")';")
113
+ # Count files that no longer exist
114
+ orphans=0
115
+ paths=$(eagle_db "SELECT DISTINCT file_path FROM code_chunks WHERE project = '$(eagle_sql_escape "$proj")';")
116
+ while IFS= read -r fpath; do
117
+ [ -z "$fpath" ] && continue
118
+ [ ! -f "$proj_cwd/$fpath" ] && orphans=$((orphans + 1))
119
+ done <<< "$paths"
120
+ if [ "$orphans" -gt 0 ]; then
121
+ eagle_dim "Would prune $orphans orphaned files from '$proj'"
122
+ orphan_total=$((orphan_total + orphans))
123
+ fi
124
+ else
125
+ removed=$(eagle_prune_orphan_chunks "$proj" "$proj_cwd")
126
+ if [ "${removed:-0}" -gt 0 ]; then
127
+ eagle_ok "Pruned $removed orphaned files from '$proj'"
128
+ orphan_total=$((orphan_total + removed))
129
+ fi
130
+ fi
131
+ fi
132
+ done <<< "$projects"
133
+ fi
134
+
135
+ if [ "$orphan_total" -eq 0 ]; then
136
+ eagle_ok "No orphaned code chunks found"
137
+ fi
138
+
139
+ # ─── Summary ────────────────────────────────────────────
140
+
141
+ new_obs=$(eagle_db "SELECT COUNT(*) FROM observations;")
142
+ new_chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks;")
143
+
144
+ echo ""
145
+ if [ "$dry_run" = true ]; then
146
+ eagle_footer "Dry run complete (no changes made)."
147
+ else
148
+ eagle_footer "Prune complete."
149
+ fi
150
+
151
+ eagle_kv "Observations:" "${new_obs:-0} (was ${total_obs:-0})"
152
+ eagle_kv "Code chunks:" "${new_chunks:-0} (was ${total_chunks:-0})"
153
+ eagle_kv "Database:" "$EAGLE_MEM_DB"
154
+ echo ""
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Search
4
+ # CLI wrapper for memory search (replaces raw SQL in skills)
5
+ # ═══════════════════════════════════════════════════════════
6
+ set -euo pipefail
7
+
8
+ SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ LIB_DIR="$SCRIPTS_DIR/../lib"
10
+
11
+ . "$SCRIPTS_DIR/style.sh"
12
+ . "$LIB_DIR/common.sh"
13
+ . "$LIB_DIR/db.sh"
14
+
15
+ eagle_ensure_db
16
+
17
+ # ─── Parse arguments ──────────────────────────────────────
18
+
19
+ mode="keyword"
20
+ project=""
21
+ limit=10
22
+ session_id=""
23
+ json_output=false
24
+ query=""
25
+ cross_project=false
26
+
27
+ while [ $# -gt 0 ]; do
28
+ case "$1" in
29
+ --timeline|-t) mode="timeline"; shift ;;
30
+ --session|-s) mode="session"; session_id="$2"; shift 2 ;;
31
+ --files|-f) mode="files"; shift ;;
32
+ --stats) mode="stats"; shift ;;
33
+ --project|-p) project="$2"; shift 2 ;;
34
+ --limit|-n) limit="$2"; shift 2 ;;
35
+ --all|-a) cross_project=true; shift ;;
36
+ --json|-j) json_output=true; shift ;;
37
+ --help|-h)
38
+ echo -e " ${BOLD}eagle-mem search${RESET} — Search persistent memory"
39
+ echo ""
40
+ echo -e " ${BOLD}Usage:${RESET}"
41
+ echo -e " eagle-mem search ${CYAN}<query>${RESET} ${DIM}# keyword search${RESET}"
42
+ echo -e " eagle-mem search ${CYAN}--timeline${RESET} ${DIM}# recent sessions${RESET}"
43
+ echo -e " eagle-mem search ${CYAN}--session <id>${RESET} ${DIM}# session details${RESET}"
44
+ echo -e " eagle-mem search ${CYAN}--files${RESET} ${DIM}# frequently modified files${RESET}"
45
+ echo -e " eagle-mem search ${CYAN}--stats${RESET} ${DIM}# project statistics${RESET}"
46
+ echo ""
47
+ echo -e " ${BOLD}Options:${RESET}"
48
+ echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current dir)"
49
+ echo -e " ${CYAN}-n, --limit${RESET} <N> Max results (default: 10)"
50
+ echo -e " ${CYAN}-a, --all${RESET} Search across all projects"
51
+ echo -e " ${CYAN}-j, --json${RESET} Output as JSON"
52
+ echo ""
53
+ exit 0
54
+ ;;
55
+ -*)
56
+ eagle_err "Unknown option: $1"
57
+ exit 1
58
+ ;;
59
+ *)
60
+ query="$1"; shift ;;
61
+ esac
62
+ done
63
+
64
+ [ -z "$project" ] && project=$(eagle_project_from_cwd "$(pwd)")
65
+ limit=$(eagle_sql_int "$limit")
66
+ [ "$limit" -eq 0 ] && limit=10
67
+
68
+ # ─── Keyword search ──────────────────────────────────────
69
+
70
+ search_keyword() {
71
+ local q; q=$(eagle_sql_escape "$(eagle_fts_sanitize "$query")")
72
+ local p; p=$(eagle_sql_escape "$project")
73
+
74
+ local where_project=""
75
+ if [ "$cross_project" = false ]; then
76
+ where_project="AND s.project = '$p'"
77
+ fi
78
+
79
+ if [ "$json_output" = true ]; then
80
+ eagle_db_json "SELECT s.id, s.project, s.request, s.completed, s.learned, s.created_at
81
+ FROM summaries s
82
+ JOIN summaries_fts f ON f.rowid = s.id
83
+ WHERE summaries_fts MATCH '$q'
84
+ $where_project
85
+ ORDER BY rank
86
+ LIMIT $limit;"
87
+ return
88
+ fi
89
+
90
+ local results
91
+ results=$(eagle_db "SELECT s.id, s.project, s.request, s.completed, s.learned, s.created_at
92
+ FROM summaries s
93
+ JOIN summaries_fts f ON f.rowid = s.id
94
+ WHERE summaries_fts MATCH '$q'
95
+ $where_project
96
+ ORDER BY rank
97
+ LIMIT $limit;")
98
+
99
+ if [ -z "$results" ]; then
100
+ eagle_dim "No results for '$query'"
101
+ return
102
+ fi
103
+
104
+ echo ""
105
+ while IFS='|' read -r sid sproj req completed learned created_at; do
106
+ [ -z "$sid" ] && continue
107
+ echo -e " ${BOLD}#$sid${RESET} ${DIM}$created_at${RESET}"
108
+ if [ "$cross_project" = true ]; then
109
+ echo -e " ${DIM}project:${RESET} $sproj"
110
+ fi
111
+ [ -n "$req" ] && echo -e " ${CYAN}Request:${RESET} $req"
112
+ [ -n "$completed" ] && echo -e " ${GREEN}Done:${RESET} $completed"
113
+ [ -n "$learned" ] && echo -e " ${YELLOW}Learned:${RESET} $learned"
114
+ echo ""
115
+ done <<< "$results"
116
+ }
117
+
118
+ # ─── Timeline ────────────────────────────────────────────
119
+
120
+ search_timeline() {
121
+ local p; p=$(eagle_sql_escape "$project")
122
+
123
+ if [ "$json_output" = true ]; then
124
+ eagle_db_json "SELECT s.id, s.request, s.completed, s.learned, s.next_steps, s.created_at
125
+ FROM summaries s
126
+ WHERE s.project = '$p'
127
+ ORDER BY s.created_at DESC
128
+ LIMIT $limit;"
129
+ return
130
+ fi
131
+
132
+ local results
133
+ results=$(eagle_db "SELECT s.id, s.request, s.completed, s.learned, s.next_steps, s.created_at
134
+ FROM summaries s
135
+ WHERE s.project = '$p'
136
+ ORDER BY s.created_at DESC
137
+ LIMIT $limit;")
138
+
139
+ if [ -z "$results" ]; then
140
+ eagle_dim "No sessions found for project '$project'"
141
+ return
142
+ fi
143
+
144
+ echo ""
145
+ echo -e " ${BOLD}Recent sessions${RESET} ${DIM}($project)${RESET}"
146
+ echo -e " ${DIM}─────────────────────────────────────${RESET}"
147
+ echo ""
148
+
149
+ while IFS='|' read -r sid req completed learned next_steps created_at; do
150
+ [ -z "$sid" ] && continue
151
+ echo -e " ${BOLD}#$sid${RESET} ${DIM}$created_at${RESET}"
152
+ [ -n "$req" ] && echo -e " ${CYAN}Request:${RESET} $req"
153
+ [ -n "$completed" ] && echo -e " ${GREEN}Done:${RESET} $completed"
154
+ [ -n "$learned" ] && echo -e " ${YELLOW}Learned:${RESET} $learned"
155
+ [ -n "$next_steps" ] && echo -e " ${DIM}Next:${RESET} $next_steps"
156
+ echo ""
157
+ done <<< "$results"
158
+ }
159
+
160
+ # ─── Session details ──────────────────────────────────────
161
+
162
+ search_session() {
163
+ local sid_sql; sid_sql=$(eagle_sql_escape "$session_id")
164
+
165
+ if [ "$json_output" = true ]; then
166
+ eagle_db_json "SELECT o.tool_name, o.tool_input_summary, o.files_read, o.files_modified, o.created_at
167
+ FROM observations o
168
+ WHERE o.session_id = '$sid_sql'
169
+ ORDER BY o.created_at ASC;"
170
+ return
171
+ fi
172
+
173
+ local results
174
+ results=$(eagle_db "SELECT o.tool_name, o.tool_input_summary, o.files_read, o.files_modified, o.created_at
175
+ FROM observations o
176
+ WHERE o.session_id = '$sid_sql'
177
+ ORDER BY o.created_at ASC;")
178
+
179
+ if [ -z "$results" ]; then
180
+ eagle_dim "No observations found for session '$session_id'"
181
+ return
182
+ fi
183
+
184
+ echo ""
185
+ echo -e " ${BOLD}Session${RESET} ${DIM}$session_id${RESET}"
186
+ echo -e " ${DIM}─────────────────────────────────────${RESET}"
187
+ echo ""
188
+
189
+ while IFS='|' read -r tool_name summary files_r files_m created_at; do
190
+ [ -z "$tool_name" ] && continue
191
+ local icon="$DOT"
192
+ case "$tool_name" in
193
+ Read) icon="${CYAN}R${RESET}" ;;
194
+ Write) icon="${GREEN}W${RESET}" ;;
195
+ Edit) icon="${YELLOW}E${RESET}" ;;
196
+ Bash) icon="${BLUE}\$${RESET}" ;;
197
+ esac
198
+ echo -e " ${icon} ${DIM}$created_at${RESET} $summary"
199
+ done <<< "$results"
200
+ echo ""
201
+ }
202
+
203
+ # ─── Frequently modified files ────────────────────────────
204
+
205
+ search_files() {
206
+ local p; p=$(eagle_sql_escape "$project")
207
+
208
+ if [ "$json_output" = true ]; then
209
+ eagle_db_json "SELECT json_each.value as file, COUNT(*) as times
210
+ FROM observations, json_each(observations.files_modified)
211
+ WHERE observations.project = '$p'
212
+ GROUP BY json_each.value
213
+ ORDER BY times DESC
214
+ LIMIT $limit;"
215
+ return
216
+ fi
217
+
218
+ local results
219
+ results=$(eagle_db "SELECT json_each.value as file, COUNT(*) as times
220
+ FROM observations, json_each(observations.files_modified)
221
+ WHERE observations.project = '$p'
222
+ GROUP BY json_each.value
223
+ ORDER BY times DESC
224
+ LIMIT $limit;")
225
+
226
+ if [ -z "$results" ]; then
227
+ eagle_dim "No file history for project '$project'"
228
+ return
229
+ fi
230
+
231
+ echo ""
232
+ echo -e " ${BOLD}Frequently modified files${RESET} ${DIM}($project)${RESET}"
233
+ echo -e " ${DIM}─────────────────────────────────────${RESET}"
234
+ echo ""
235
+
236
+ while IFS='|' read -r filepath count; do
237
+ [ -z "$filepath" ] && continue
238
+ printf " ${GREEN}%3s×${RESET} %s\n" "$count" "$filepath"
239
+ done <<< "$results"
240
+ echo ""
241
+ }
242
+
243
+ # ─── Stats ────────────────────────────────────────────────
244
+
245
+ search_stats() {
246
+ local p; p=$(eagle_sql_escape "$project")
247
+
248
+ local sessions summaries observations tasks
249
+ sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$p';")
250
+ summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p';")
251
+ observations=$(eagle_db "SELECT COUNT(*) FROM observations o JOIN sessions s ON s.id = o.session_id WHERE s.project = '$p';")
252
+ tasks=$(eagle_db "SELECT COUNT(*) FROM tasks WHERE project = '$p';")
253
+ local chunks
254
+ chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$p';")
255
+
256
+ if [ "$json_output" = true ]; then
257
+ jq -nc --arg project "$project" \
258
+ --argjson sessions "${sessions:-0}" \
259
+ --argjson summaries "${summaries:-0}" \
260
+ --argjson observations "${observations:-0}" \
261
+ --argjson tasks "${tasks:-0}" \
262
+ --argjson code_chunks "${chunks:-0}" \
263
+ '{project: $project, sessions: $sessions, summaries: $summaries, observations: $observations, tasks: $tasks, code_chunks: $code_chunks}'
264
+ return
265
+ fi
266
+
267
+ echo ""
268
+ echo -e " ${BOLD}Stats${RESET} ${DIM}($project)${RESET}"
269
+ echo -e " ${DIM}─────────────────────────────────────${RESET}"
270
+ echo ""
271
+ eagle_kv "Sessions:" "${sessions:-0}"
272
+ eagle_kv "Summaries:" "${summaries:-0}"
273
+ eagle_kv "Observations:" "${observations:-0}"
274
+ eagle_kv "Tasks:" "${tasks:-0}"
275
+ eagle_kv "Code chunks:" "${chunks:-0}"
276
+ echo ""
277
+ }
278
+
279
+ # ─── Dispatch ─────────────────────────────────────────────
280
+
281
+ case "$mode" in
282
+ keyword)
283
+ if [ -z "$query" ]; then
284
+ eagle_err "Usage: eagle-mem search <query>"
285
+ eagle_dim " Run 'eagle-mem search --help' for options"
286
+ exit 1
287
+ fi
288
+ search_keyword
289
+ ;;
290
+ timeline) search_timeline ;;
291
+ session) search_session ;;
292
+ files) search_files ;;
293
+ stats) search_stats ;;
294
+ esac