eagle-mem 1.1.0 → 1.3.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 ""
package/scripts/search.sh CHANGED
@@ -62,11 +62,13 @@ while [ $# -gt 0 ]; do
62
62
  done
63
63
 
64
64
  [ -z "$project" ] && project=$(eagle_project_from_cwd "$(pwd)")
65
+ limit=$(eagle_sql_int "$limit")
66
+ [ "$limit" -eq 0 ] && limit=10
65
67
 
66
68
  # ─── Keyword search ──────────────────────────────────────
67
69
 
68
70
  search_keyword() {
69
- local q; q=$(eagle_sql_escape "$query")
71
+ local q; q=$(eagle_sql_escape "$(eagle_fts_sanitize "$query")")
70
72
  local p; p=$(eagle_sql_escape "$project")
71
73
 
72
74
  local where_project=""
@@ -158,10 +160,12 @@ search_timeline() {
158
160
  # ─── Session details ──────────────────────────────────────
159
161
 
160
162
  search_session() {
163
+ local sid_sql; sid_sql=$(eagle_sql_escape "$session_id")
164
+
161
165
  if [ "$json_output" = true ]; then
162
166
  eagle_db_json "SELECT o.tool_name, o.tool_input_summary, o.files_read, o.files_modified, o.created_at
163
167
  FROM observations o
164
- WHERE o.session_id = '$session_id'
168
+ WHERE o.session_id = '$sid_sql'
165
169
  ORDER BY o.created_at ASC;"
166
170
  return
167
171
  fi
@@ -169,7 +173,7 @@ search_session() {
169
173
  local results
170
174
  results=$(eagle_db "SELECT o.tool_name, o.tool_input_summary, o.files_read, o.files_modified, o.created_at
171
175
  FROM observations o
172
- WHERE o.session_id = '$session_id'
176
+ WHERE o.session_id = '$sid_sql'
173
177
  ORDER BY o.created_at ASC;")
174
178
 
175
179
  if [ -z "$results" ]; then
@@ -250,8 +254,13 @@ search_stats() {
250
254
  chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$p';")
251
255
 
252
256
  if [ "$json_output" = true ]; then
253
- printf '{"project":"%s","sessions":%s,"summaries":%s,"observations":%s,"tasks":%s,"code_chunks":%s}\n' \
254
- "$project" "${sessions:-0}" "${summaries:-0}" "${observations:-0}" "${tasks:-0}" "${chunks:-0}"
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}'
255
264
  return
256
265
  fi
257
266
 
package/scripts/tasks.sh CHANGED
@@ -16,7 +16,7 @@ eagle_ensure_db
16
16
 
17
17
  # ─── Parse arguments ──────────────────────────────────────
18
18
 
19
- action="${1:-list}"
19
+ action="${1:-pending}"
20
20
  shift 2>/dev/null || true
21
21
 
22
22
  project=""
@@ -123,14 +123,14 @@ tasks_add() {
123
123
  max_ord=$(eagle_db "SELECT COALESCE(MAX(ordinal), 0) FROM tasks WHERE project = '$project_sql';")
124
124
  local next_ord=$((max_ord + 1))
125
125
 
126
- eagle_db "INSERT INTO tasks (project, title, instructions, ordinal)
127
- VALUES ('$project_sql', '$title_sql', '$instr_sql', $next_ord);"
128
-
129
126
  local new_id
130
- new_id=$(eagle_db "SELECT last_insert_rowid();")
127
+ new_id=$(eagle_db "INSERT INTO tasks (project, title, instructions, ordinal)
128
+ VALUES ('$project_sql', '$title_sql', '$instr_sql', $next_ord);
129
+ SELECT last_insert_rowid();")
131
130
 
132
131
  if [ "$json_output" = true ]; then
133
- printf '{"id":%s,"title":"%s","ordinal":%s}\n' "$new_id" "$title" "$next_ord"
132
+ jq -nc --arg id "$new_id" --arg title "$title" --argjson ord "$next_ord" \
133
+ '{id: ($id | tonumber), title: $title, ordinal: $ord}'
134
134
  return
135
135
  fi
136
136
 
@@ -147,14 +147,26 @@ tasks_done() {
147
147
  exit 1
148
148
  fi
149
149
 
150
- eagle_db "UPDATE tasks SET status = 'done', completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
151
- WHERE id = $task_id AND project = '$project_sql';"
150
+ if ! [[ "$task_id" =~ ^[0-9]+$ ]]; then
151
+ eagle_err "Task ID must be a number, got: $task_id"
152
+ exit 1
153
+ fi
154
+
155
+ local changed
156
+ changed=$(eagle_db "UPDATE tasks SET status = 'done', completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
157
+ WHERE id = $task_id AND project = '$project_sql';
158
+ SELECT changes();")
159
+
160
+ if [ "${changed:-0}" -eq 0 ]; then
161
+ eagle_err "Task #$task_id not found in project '$project'"
162
+ exit 1
163
+ fi
152
164
 
153
165
  local title
154
- title=$(eagle_db "SELECT title FROM tasks WHERE id = $task_id;")
166
+ title=$(eagle_db "SELECT title FROM tasks WHERE id = $task_id AND project = '$project_sql';")
155
167
 
156
168
  if [ "$json_output" = true ]; then
157
- printf '{"id":%s,"status":"done"}\n' "$task_id"
169
+ jq -nc --arg id "$task_id" '{id: ($id | tonumber), status: "done"}'
158
170
  return
159
171
  fi
160
172
 
@@ -171,10 +183,22 @@ tasks_block() {
171
183
  exit 1
172
184
  fi
173
185
 
174
- eagle_db "UPDATE tasks SET status = 'blocked' WHERE id = $task_id AND project = '$project_sql';"
186
+ if ! [[ "$task_id" =~ ^[0-9]+$ ]]; then
187
+ eagle_err "Task ID must be a number, got: $task_id"
188
+ exit 1
189
+ fi
190
+
191
+ local changed
192
+ changed=$(eagle_db "UPDATE tasks SET status = 'blocked' WHERE id = $task_id AND project = '$project_sql';
193
+ SELECT changes();")
194
+
195
+ if [ "${changed:-0}" -eq 0 ]; then
196
+ eagle_err "Task #$task_id not found in project '$project'"
197
+ exit 1
198
+ fi
175
199
 
176
200
  if [ "$json_output" = true ]; then
177
- printf '{"id":%s,"status":"blocked"}\n' "$task_id"
201
+ jq -nc --arg id "$task_id" '{id: ($id | tonumber), status: "blocked"}'
178
202
  return
179
203
  fi
180
204
 
@@ -192,11 +216,23 @@ tasks_context() {
192
216
  exit 1
193
217
  fi
194
218
 
219
+ if ! [[ "$task_id" =~ ^[0-9]+$ ]]; then
220
+ eagle_err "Task ID must be a number, got: $task_id"
221
+ exit 1
222
+ fi
223
+
195
224
  local snap_sql; snap_sql=$(eagle_sql_escape "$snapshot")
196
- eagle_db "UPDATE tasks SET context_snapshot = '$snap_sql' WHERE id = $task_id AND project = '$project_sql';"
225
+ local changed
226
+ changed=$(eagle_db "UPDATE tasks SET context_snapshot = '$snap_sql' WHERE id = $task_id AND project = '$project_sql';
227
+ SELECT changes();")
228
+
229
+ if [ "${changed:-0}" -eq 0 ]; then
230
+ eagle_err "Task #$task_id not found in project '$project'"
231
+ exit 1
232
+ fi
197
233
 
198
234
  if [ "$json_output" = true ]; then
199
- printf '{"id":%s,"context_snapshot":true}\n' "$task_id"
235
+ jq -nc --arg id "$task_id" '{id: ($id | tonumber), context_snapshot: true}'
200
236
  return
201
237
  fi
202
238
 
@@ -212,7 +248,7 @@ tasks_clear() {
212
248
  eagle_db "DELETE FROM tasks WHERE project = '$project_sql' AND status = 'done';"
213
249
 
214
250
  if [ "$json_output" = true ]; then
215
- printf '{"cleared":%s}\n' "${count:-0}"
251
+ jq -nc --argjson cleared "${count:-0}" '{cleared: $cleared}'
216
252
  return
217
253
  fi
218
254
 
@@ -230,7 +266,22 @@ case "$action" in
230
266
  context) tasks_context ;;
231
267
  clear) tasks_clear ;;
232
268
  --help|-h)
233
- echo -e " Run ${CYAN}eagle-mem tasks --help${RESET} for usage"
269
+ echo -e " ${BOLD}eagle-mem tasks${RESET} Manage tracked tasks"
270
+ echo ""
271
+ echo -e " ${BOLD}Usage:${RESET}"
272
+ echo -e " eagle-mem tasks ${DIM}# list pending tasks${RESET}"
273
+ echo -e " eagle-mem tasks ${CYAN}list${RESET} ${DIM}# list all tasks${RESET}"
274
+ echo -e " eagle-mem tasks ${CYAN}add${RESET} <title> [instructions] ${DIM}# add a task${RESET}"
275
+ echo -e " eagle-mem tasks ${CYAN}done${RESET} <id> ${DIM}# mark task complete${RESET}"
276
+ echo -e " eagle-mem tasks ${CYAN}block${RESET} <id> ${DIM}# mark task blocked${RESET}"
277
+ echo -e " eagle-mem tasks ${CYAN}context${RESET} <id> <snapshot> ${DIM}# set task context${RESET}"
278
+ echo -e " eagle-mem tasks ${CYAN}clear${RESET} ${DIM}# remove all done tasks${RESET}"
279
+ echo ""
280
+ echo -e " ${BOLD}Options:${RESET}"
281
+ echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current dir)"
282
+ echo -e " ${CYAN}-j, --json${RESET} Output as JSON"
283
+ echo ""
284
+ exit 0
234
285
  ;;
235
286
  *)
236
287
  eagle_err "Unknown action: $action"
@@ -6,12 +6,12 @@
6
6
  set -euo pipefail
7
7
 
8
8
  SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ LIB_DIR="$SCRIPTS_DIR/../lib"
9
10
 
10
11
  . "$SCRIPTS_DIR/style.sh"
12
+ . "$LIB_DIR/common.sh"
11
13
 
12
- EAGLE_MEM_DIR="${EAGLE_MEM_DIR:-$HOME/.eagle-mem}"
13
- SETTINGS="$HOME/.claude/settings.json"
14
- SKILLS_DIR="$HOME/.claude/skills"
14
+ SETTINGS="$EAGLE_SETTINGS"
15
15
 
16
16
  eagle_header "Uninstall"
17
17
 
@@ -35,9 +35,9 @@ fi
35
35
 
36
36
  # ─── Remove skill symlinks ────────────────────────────────
37
37
 
38
- if [ -d "$SKILLS_DIR" ]; then
38
+ if [ -d "$EAGLE_SKILLS_DIR" ]; then
39
39
  for skill in eagle-mem-search eagle-mem-tasks eagle-mem-overview; do
40
- target="$SKILLS_DIR/$skill"
40
+ target="$EAGLE_SKILLS_DIR/$skill"
41
41
  if [ -L "$target" ]; then
42
42
  rm "$target"
43
43
  eagle_ok "Skill removed: $skill"
package/scripts/update.sh CHANGED
@@ -7,11 +7,12 @@ set -euo pipefail
7
7
 
8
8
  PACKAGE_DIR="${1:-.}"
9
9
  SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ LIB_DIR="$SCRIPTS_DIR/../lib"
10
11
 
11
12
  . "$SCRIPTS_DIR/style.sh"
13
+ . "$LIB_DIR/common.sh"
12
14
 
13
- EAGLE_MEM_DIR="${EAGLE_MEM_DIR:-$HOME/.eagle-mem}"
14
- SETTINGS="$HOME/.claude/settings.json"
15
+ SETTINGS="$EAGLE_SETTINGS"
15
16
 
16
17
  eagle_header "Update"
17
18
 
@@ -78,7 +79,12 @@ if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
78
79
 
79
80
  patch_hook "SessionStart" "" "$EAGLE_MEM_DIR/hooks/session-start.sh"
80
81
  patch_hook "Stop" "" "$EAGLE_MEM_DIR/hooks/stop.sh"
81
- patch_hook "PostToolUse" "Read|Write|Edit|Bash" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
82
+ # Update PostToolUse matcher if it has the old value (pre-v1.3.0)
83
+ if jq -e '.hooks.PostToolUse[]? | select(.matcher == "Read|Write|Edit|Bash")' "$SETTINGS" &>/dev/null; then
84
+ _tmp=$(mktemp)
85
+ jq '(.hooks.PostToolUse[] | select(.matcher == "Read|Write|Edit|Bash")).matcher = "Read|Write|Edit|Bash|TaskCreate|TaskUpdate"' "$SETTINGS" > "$_tmp" && mv "$_tmp" "$SETTINGS"
86
+ fi
87
+ patch_hook "PostToolUse" "Read|Write|Edit|Bash|TaskCreate|TaskUpdate" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
82
88
  patch_hook "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh"
83
89
  patch_hook "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
84
90
 
@@ -87,13 +93,12 @@ fi
87
93
 
88
94
  # ─── Update skill symlinks ────────────────────────────────
89
95
 
90
- SKILLS_DIR="$HOME/.claude/skills"
91
96
  if [ -d "$PACKAGE_DIR/skills" ]; then
92
- mkdir -p "$SKILLS_DIR"
97
+ mkdir -p "$EAGLE_SKILLS_DIR"
93
98
  for skill_dir in "$PACKAGE_DIR"/skills/*/; do
94
99
  [ ! -d "$skill_dir" ] && continue
95
100
  skill_name=$(basename "$skill_dir")
96
- dst="$SKILLS_DIR/$skill_name"
101
+ dst="$EAGLE_SKILLS_DIR/$skill_name"
97
102
  [ -L "$dst" ] && rm "$dst"
98
103
  ln -sf "$skill_dir" "$dst"
99
104
  done