eagle-mem 3.2.0 → 3.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.
package/README.md CHANGED
@@ -159,16 +159,27 @@ The `update` command copies new files, runs any pending database migrations, and
159
159
 
160
160
  ## How it works
161
161
 
162
- Five hooks fire automatically at different points in Claude Code's lifecycle:
162
+ Six hooks fire automatically at different points in Claude Code's lifecycle:
163
163
 
164
164
  | Hook | Fires when | What it does |
165
165
  |------|-----------|--------------|
166
166
  | **SessionStart** | startup, resume, clear, compact | Injects overview, summaries, memories, tasks |
167
+ | **PreToolUse** | before Bash and Read calls | Rewrites noisy commands (learned rules), detects redundant reads |
167
168
  | **UserPromptSubmit** | user sends a message | FTS5 search for relevant past context |
168
- | **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes |
169
+ | **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes, tracks modifications |
169
170
  | **Stop** | Claude's turn ends | Extracts `<eagle-summary>`, strips `<private>` tags |
170
171
  | **SessionEnd** | session closes | Re-syncs tasks, marks session completed |
171
172
 
173
+ ### Token savings
174
+
175
+ Eagle Mem actively reduces token consumption:
176
+
177
+ - **Command rewriting** — PreToolUse rewrites noisy Bash commands (e.g., `find`, `grep`) to pipe through `head -N`, using `updatedInput` to modify the command before execution. Rules are learned by the curator from real usage, not hardcoded.
178
+ - **Read-after-modify detection** — If you just edited or wrote a file, Eagle Mem nudges that the diff is already in context before a redundant Read.
179
+ - **Read dedup tracking** — Files read 3+ times in a session get a soft nudge that contents are likely already in context.
180
+
181
+ ### Data
182
+
172
183
  Data lives in a single SQLite database at `~/.eagle-mem/memory.db` (WAL mode, FTS5 full-text search):
173
184
 
174
185
  | Table | What it stores |
@@ -178,6 +189,7 @@ Data lives in a single SQLite database at `~/.eagle-mem/memory.db` (WAL mode, FT
178
189
  | observations | Per-tool-use file touch records |
179
190
  | overviews | One overview per project (scan or manual) |
180
191
  | code_chunks | FTS5-indexed source file chunks |
192
+ | command_rules | Curator-learned command output rules |
181
193
  | claude_memories | Mirror of Claude Code auto-memories |
182
194
  | claude_plans | Mirror of Claude Code plans |
183
195
  | claude_tasks | Mirror of Claude Code tasks |
@@ -0,0 +1,12 @@
1
+ -- Migration 020: Remove seeded global command rules.
2
+ -- Self-learning pipeline (curator) now generates rules from real usage.
3
+ -- Per-project rules created by the curator are preserved.
4
+ --
5
+ -- Safety: back up any existing global rules before deleting so they
6
+ -- can be restored if a user manually created rules they want to keep.
7
+ -- The backup table is left in place intentionally.
8
+
9
+ CREATE TABLE IF NOT EXISTS _backup_020_global_rules AS
10
+ SELECT * FROM command_rules WHERE project = '';
11
+
12
+ DELETE FROM command_rules WHERE project = '';
@@ -68,7 +68,7 @@ case "$tool_name" in
68
68
  cmd=$(echo "$cmd" | eagle_redact)
69
69
  tool_summary="Bash: $cmd"
70
70
 
71
- tool_output=$(echo "$input" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
71
+ tool_output=$(echo "$input" | jq -r '.tool_response.stdout // empty' 2>/dev/null)
72
72
  if [ -n "$tool_output" ]; then
73
73
  output_bytes=${#tool_output}
74
74
  output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
@@ -98,6 +98,25 @@ case "$tool_name" in
98
98
  ;;
99
99
  esac
100
100
 
101
+ # ─── Track recent Edit/Write targets for Read-after-modify detection ──
102
+
103
+ if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
104
+ case "$tool_name" in
105
+ Edit|Write)
106
+ mod_dir="$EAGLE_MEM_DIR/mod-tracker"
107
+ mkdir -p "$mod_dir" 2>/dev/null
108
+ mod_file="$mod_dir/${session_id}"
109
+ echo "$fp" >> "$mod_file"
110
+ # Keep only last 3 entries — use per-process tmp to avoid
111
+ # race when parallel PostToolUse hooks fire on same session
112
+ if [ -f "$mod_file" ]; then
113
+ _mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || _mod_tmp="${mod_file}.$$"
114
+ tail -3 "$mod_file" > "$_mod_tmp" && mv "$_mod_tmp" "$mod_file" || rm -f "$_mod_tmp"
115
+ fi
116
+ ;;
117
+ esac
118
+ fi
119
+
101
120
  # ─── Dispatch to extracted responsibilities ───────────────
102
121
 
103
122
  eagle_posttool_mirror_writes "$tool_name" "$fp" "$session_id" "$project"
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env bash
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — PreToolUse hook
4
- # Fires before every Bash tool use
4
+ # Fires before Bash and Read tool calls
5
5
  # 1. Surfaces feature verification checklists before git push
6
- # 2. Applies learned command filtering rules (RTK-style adaptive)
6
+ # 2. Truncates noisy commands via updatedInput (curator-learned rules)
7
+ # 3. Detects Read-after-Edit/Write (content already in context)
8
+ # 4. Nudges on repeated file reads (dedup tracker)
7
9
  # ═══════════════════════════════════════════════════════════
8
10
  set +e
9
11
 
@@ -17,12 +19,13 @@ input=$(eagle_read_stdin)
17
19
  [ -z "$input" ] && exit 0
18
20
 
19
21
  tool_name=$(echo "$input" | jq -r '.tool_name // empty')
20
- [ "$tool_name" != "Bash" ] && exit 0
21
22
 
22
- [ ! -f "$EAGLE_MEM_DB" ] && exit 0
23
+ case "$tool_name" in
24
+ Bash|Read) ;;
25
+ *) exit 0 ;;
26
+ esac
23
27
 
24
- cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
25
- [ -z "$cmd" ] && exit 0
28
+ [ ! -f "$EAGLE_MEM_DB" ] && exit 0
26
29
 
27
30
  session_id=$(echo "$input" | jq -r '.session_id // empty')
28
31
  cwd=$(echo "$input" | jq -r '.cwd // empty')
@@ -30,77 +33,120 @@ project=$(eagle_project_from_cwd "$cwd")
30
33
  [ -z "$project" ] && exit 0
31
34
 
32
35
  context=""
36
+ updated_input=""
37
+
38
+ case "$tool_name" in
39
+ Bash)
40
+ cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
41
+ [ -z "$cmd" ] && exit 0
42
+
43
+ # ─── Feature verification on git push ─────────────────────
44
+
45
+ case "$cmd" in
46
+ *"git push"*|*"gh pr create"*)
47
+ has_features=$(eagle_count_active_features "$project")
48
+ if [ "${has_features:-0}" -gt 0 ]; then
49
+ changed_files=""
50
+ if [ -n "$cwd" ] && [ -d "$cwd" ]; then
51
+ changed_files=$(git -C "$cwd" diff --name-only HEAD 2>/dev/null)
52
+ [ -z "$changed_files" ] && changed_files=$(git -C "$cwd" diff --cached --name-only 2>/dev/null)
53
+ fi
33
54
 
34
- # ─── Feature verification on git push ─────────────────────
35
-
36
- case "$cmd" in
37
- *"git push"*|*"gh pr create"*)
38
- has_features=$(eagle_count_active_features "$project")
39
- if [ "${has_features:-0}" -gt 0 ]; then
40
- changed_files=""
41
- if [ -n "$cwd" ] && [ -d "$cwd" ]; then
42
- changed_files=$(git -C "$cwd" diff --name-only HEAD 2>/dev/null)
43
- [ -z "$changed_files" ] && changed_files=$(git -C "$cwd" diff --cached --name-only 2>/dev/null)
44
- fi
45
-
46
- if [ -n "$changed_files" ]; then
47
- seen_features=""
48
- while IFS= read -r changed_file; do
49
- [ -z "$changed_file" ] && continue
50
- fname=$(basename "$changed_file")
51
- fname_esc=$(eagle_sql_escape "$fname")
52
-
53
- feature_hits=$(eagle_find_feature_for_push "$project" "$fname_esc")
54
-
55
- while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
56
- [ -z "$feat_name" ] && continue
57
- case "$seen_features" in *"|$feat_name|"*) continue ;; esac
58
- seen_features+="|$feat_name|"
59
-
60
- context+=" - $feat_name"
61
- [ -n "$feat_smoke" ] && context+=" | smoke: $feat_smoke"
62
- [ -n "$feat_deps" ] && context+=" | deps: $feat_deps"
63
- if [ -n "$feat_verified" ]; then
64
- context+=" | last verified: $feat_verified"
65
- else
66
- context+=" | never verified"
67
- fi
68
- context+=$'\n'
69
- done <<< "$feature_hits"
70
- done <<< "$changed_files"
71
-
72
- if [ -n "$context" ]; then
73
- context="Eagle Mem: This push affects the following features. After deploy, verify each works and run 'eagle-mem feature verify <name>'.
55
+ if [ -n "$changed_files" ]; then
56
+ seen_features=""
57
+ while IFS= read -r changed_file; do
58
+ [ -z "$changed_file" ] && continue
59
+ fname=$(basename "$changed_file")
60
+
61
+ feature_hits=$(eagle_find_feature_for_push "$project" "$fname")
62
+
63
+ while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
64
+ [ -z "$feat_name" ] && continue
65
+ case "$seen_features" in *"|$feat_name|"*) continue ;; esac
66
+ seen_features+="|$feat_name|"
67
+
68
+ context+=" - $feat_name"
69
+ [ -n "$feat_smoke" ] && context+=" | smoke: $feat_smoke"
70
+ [ -n "$feat_deps" ] && context+=" | deps: $feat_deps"
71
+ if [ -n "$feat_verified" ]; then
72
+ context+=" | last verified: $feat_verified"
73
+ else
74
+ context+=" | never verified"
75
+ fi
76
+ context+=$'\n'
77
+ done <<< "$feature_hits"
78
+ done <<< "$changed_files"
79
+
80
+ if [ -n "$context" ]; then
81
+ context="Eagle Mem: This push affects the following features. After deploy, verify each works and run 'eagle-mem feature verify <name>'.
74
82
  ${context}"
83
+ fi
75
84
  fi
76
85
  fi
77
- fi
78
- ;;
79
- esac
86
+ ;;
87
+ esac
80
88
 
81
- # ─── Command output filtering (learned rules) ─────────────
89
+ # ─── Command output filtering (learned rules) ─────────────
90
+
91
+ base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
92
+ rule=$(eagle_get_command_rule "$project" "$base_cmd")
93
+
94
+ if [ -n "$rule" ]; then
95
+ IFS='|' read -r strategy max_lines reason <<< "$rule"
96
+ case "$strategy" in
97
+ truncate)
98
+ if [ -n "$max_lines" ] && [ "$max_lines" -gt 0 ] 2>/dev/null; then
99
+ case "$cmd" in
100
+ *"&&"*|*"||"*|*";"*)
101
+ context+="Eagle Mem: '${base_cmd}' produces long output (${reason}). Consider: | head -${max_lines}"
102
+ ;;
103
+ *"| head"*|*"| tail"*|*"| wc"*|*"| grep"*|*">"*|*">>"*)
104
+ ;;
105
+ *)
106
+ updated_input=$(jq -nc --arg cmd "${cmd} | head -${max_lines}" '{"command":$cmd}')
107
+ context+="Eagle Mem: '${base_cmd}' output is typically long (${reason}). Piped through head -${max_lines}."
108
+ ;;
109
+ esac
110
+ fi
111
+ ;;
112
+ summary)
113
+ context+="Eagle Mem: '${base_cmd}' is typically noisy (${reason}). Consider piping through tail or checking exit code only."
114
+ ;;
115
+ esac
116
+ fi
117
+ ;;
118
+
119
+ Read)
120
+ fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
121
+ if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
122
+
123
+ # ─── Read-after-modify detection ──────────────────────
124
+ mod_file="$EAGLE_MEM_DIR/mod-tracker/${session_id}"
125
+ if [ -f "$mod_file" ] && grep -qFx -- "$fp" "$mod_file" 2>/dev/null; then
126
+ context+="Eagle Mem: '$(basename "$fp")' was just edited/written — the diff is already in context from the tool output. "
127
+ fi
82
128
 
83
- # Extract the base command for rule matching
84
- base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
129
+ # ─── Read dedup tracker (soft nudge) ──────────────────
130
+ tracker_dir="$EAGLE_MEM_DIR/read-tracker"
131
+ mkdir -p "$tracker_dir" 2>/dev/null
132
+ tracker_file="$tracker_dir/${session_id}"
133
+ echo "$fp" >> "$tracker_file"
134
+ read_count=$(grep -cFx -- "$fp" "$tracker_file" 2>/dev/null)
135
+ read_count=${read_count:-0}
136
+ if [ "$read_count" -ge 3 ]; then
137
+ context+="Eagle Mem: '$(basename "$fp")' has been read ${read_count} times this session. Its contents are likely already in context."
138
+ fi
139
+ fi
140
+ ;;
141
+ esac
85
142
 
86
- rule=$(eagle_get_command_rule "$project" "$base_cmd")
143
+ [ -z "$context" ] && [ -z "$updated_input" ] && exit 0
87
144
 
88
- if [ -n "$rule" ]; then
89
- IFS='|' read -r strategy max_lines reason <<< "$rule"
90
- case "$strategy" in
91
- summary)
92
- context+="Eagle Mem command hint: '${base_cmd}' output is typically noisy (${reason}). Consider piping through 'tail -5' or checking exit code only."
93
- ;;
94
- truncate)
95
- if [ -n "$max_lines" ] && [ "$max_lines" -gt 0 ] 2>/dev/null; then
96
- context+="Eagle Mem command hint: '${base_cmd}' produces long output (${reason}). Consider: ${cmd} | head -${max_lines}"
97
- fi
98
- ;;
99
- esac
145
+ if [ -n "$updated_input" ]; then
146
+ jq -nc --arg ctx "$context" --argjson ui "$updated_input" \
147
+ '{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":$ui,"additionalContext":$ctx}}'
148
+ else
149
+ jq -nc --arg ctx "$context" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":$ctx}}'
100
150
  fi
101
151
 
102
- [ -z "$context" ] && exit 0
103
-
104
- jq -nc --arg ctx "$context" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":$ctx}}'
105
-
106
152
  exit 0
@@ -61,6 +61,10 @@ if [ "$curator_schedule" = "auto" ]; then
61
61
  fi
62
62
  fi
63
63
 
64
+ # ─── Cleanup stale tracker files (non-blocking) ─────────────
65
+ find "$EAGLE_MEM_DIR/read-tracker" -type f -mtime +1 -delete 2>/dev/null &
66
+ find "$EAGLE_MEM_DIR/mod-tracker" -type f -mtime +1 -delete 2>/dev/null &
67
+
64
68
  # ─── Version check (non-blocking) ────────────────────────────
65
69
 
66
70
  update_notice=""
@@ -52,7 +52,8 @@ eagle_get_command_rule() {
52
52
  WHERE enabled = 1
53
53
  AND (project = '$project' OR project = '')
54
54
  AND ('$cmd' LIKE pattern OR '$cmd' = pattern)
55
- ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END
55
+ ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END,
56
+ LENGTH(pattern) DESC
56
57
  LIMIT 1;"
57
58
  }
58
59
 
@@ -15,9 +15,9 @@ eagle_upsert_session() {
15
15
  eagle_db "INSERT INTO sessions (id, project, cwd, model, source, last_activity_at)
16
16
  VALUES ('$session_id', '$project', '$cwd', '$model', '$source', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
17
17
  ON CONFLICT(id) DO UPDATE SET
18
- cwd = COALESCE(excluded.cwd, sessions.cwd),
19
- model = COALESCE(excluded.model, sessions.model),
20
- source = COALESCE(excluded.source, sessions.source),
18
+ cwd = COALESCE(NULLIF(excluded.cwd, ''), sessions.cwd),
19
+ model = COALESCE(NULLIF(excluded.model, ''), sessions.model),
20
+ source = COALESCE(NULLIF(excluded.source, ''), sessions.source),
21
21
  status = 'active',
22
22
  last_activity_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
23
23
  }
package/lib/hooks.sh CHANGED
@@ -11,7 +11,15 @@ eagle_patch_hook() {
11
11
  local command="$4"
12
12
  local description="${5:-}"
13
13
 
14
- if jq -e ".hooks.${event}[]? | select(.hooks[]?.command == \"$command\")" "$settings" &>/dev/null; then
14
+ # Check both command AND matcher to avoid skipping entries with different matchers
15
+ # (e.g. PreToolUse with "Bash" vs "Read" matcher using the same script)
16
+ local match_query
17
+ if [ -n "$matcher" ]; then
18
+ match_query=".hooks.${event}[]? | select(.matcher == \"$matcher\" and (.hooks[]?.command == \"$command\"))"
19
+ else
20
+ match_query=".hooks.${event}[]? | select(.matcher == null and (.hooks[]?.command == \"$command\"))"
21
+ fi
22
+ if jq -e "$match_query" "$settings" &>/dev/null; then
15
23
  [ -n "$description" ] && eagle_ok "$description ${DIM}(already registered)${RESET}"
16
24
  return 0
17
25
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Persistent memory for Claude Code — SQLite + FTS5, no daemon, no bloat",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/curate.sh CHANGED
@@ -222,6 +222,13 @@ If no rules needed, output: NONE"
222
222
  eagle_log "WARN" "Curator: skipping RULE with invalid strategy '$strategy'"
223
223
  continue
224
224
  ;; esac
225
+ # Guard: reject dangerous LLM-generated patterns that match everything
226
+ # Require at least 2 literal characters (not just wildcards)
227
+ _literal_chars=$(printf '%s' "$pattern" | sed 's/[%_]//g')
228
+ if [ ${#_literal_chars} -lt 2 ]; then
229
+ eagle_log "WARN" "Curator: skipping overly broad pattern '$pattern' (needs >=2 literal chars)"
230
+ continue
231
+ fi
225
232
 
226
233
  [ "$max_lines" = "-" ] && max_lines=""
227
234
 
@@ -186,7 +186,11 @@ eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" \
186
186
 
187
187
  eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash" \
188
188
  "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
189
- "PreToolUse hook"
189
+ "PreToolUse hook (Bash)"
190
+
191
+ eagle_patch_hook "$SETTINGS" "PreToolUse" "Read" \
192
+ "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
193
+ "PreToolUse hook (Read)"
190
194
 
191
195
  # ─── Install skills ────────────────────────────────────────
192
196
 
package/scripts/update.sh CHANGED
@@ -76,6 +76,7 @@ if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
76
76
  eagle_patch_hook "$SETTINGS" "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh"
77
77
  eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
78
78
  eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
79
+ eagle_patch_hook "$SETTINGS" "PreToolUse" "Read" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
79
80
 
80
81
  eagle_ok "Hooks registered"
81
82
  fi