eagle-mem 4.2.1 → 4.6.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 +111 -66
- package/bin/eagle-mem +9 -0
- package/db/022_cleanup_fossils.sql +12 -0
- package/db/023_guardrails.sql +18 -0
- package/hooks/post-tool-use.sh +46 -1
- package/hooks/pre-tool-use.sh +41 -4
- package/hooks/session-start.sh +9 -3
- package/hooks/stop.sh +132 -28
- package/hooks/user-prompt-submit.sh +3 -3
- package/lib/common.sh +60 -23
- package/lib/db-backfill.sh +62 -4
- package/lib/db-core.sh +2 -1
- package/lib/db-guardrails.sh +107 -0
- package/lib/db-mirrors.sh +12 -0
- package/lib/db-observations.sh +3 -2
- package/lib/db-summaries.sh +15 -0
- package/lib/db.sh +1 -0
- package/lib/hooks-posttool.sh +12 -4
- package/lib/hooks-sessionstart.sh +8 -2
- package/lib/provider.sh +17 -8
- package/package.json +1 -1
- package/scripts/curate.sh +2 -1
- package/scripts/guard.sh +160 -0
- package/scripts/health.sh +5 -5
- package/scripts/help.sh +7 -0
- package/scripts/install.sh +8 -0
- package/scripts/memories.sh +6 -2
- package/scripts/overview.sh +8 -4
- package/scripts/search.sh +21 -3
- package/scripts/statusline-em.sh +5 -3
- package/scripts/tasks.sh +15 -6
- package/scripts/update.sh +2 -0
|
@@ -81,7 +81,7 @@ fts_query=$(echo "$user_prompt" | tr -cs '[:alnum:]' ' ' | tr '[:upper:]' '[:low
|
|
|
81
81
|
results=$(eagle_search_summaries "$fts_query" "$project" 3)
|
|
82
82
|
|
|
83
83
|
if [ -n "$results" ]; then
|
|
84
|
-
context+="===
|
|
84
|
+
context+="=== Eagle Mem — Relevant Memory ===
|
|
85
85
|
"
|
|
86
86
|
while IFS='|' read -r req completed learned _next_steps created_at _proj decisions gotchas key_files; do
|
|
87
87
|
[ -z "$req" ] && [ -z "$completed" ] && continue
|
|
@@ -106,7 +106,7 @@ if [ "${has_chunks:-0}" -gt 0 ]; then
|
|
|
106
106
|
code_results=$(eagle_search_code_chunks "$fts_query" "$project" 5)
|
|
107
107
|
|
|
108
108
|
if [ -n "$code_results" ]; then
|
|
109
|
-
context+="===
|
|
109
|
+
context+="=== Eagle Mem — Relevant Code ===
|
|
110
110
|
"
|
|
111
111
|
while IFS='|' read -r fpath sline eline lang; do
|
|
112
112
|
[ -z "$fpath" ] && continue
|
|
@@ -123,7 +123,7 @@ fi
|
|
|
123
123
|
context+="
|
|
124
124
|
IMPORTANT: When Eagle Mem finds relevant memories or code for the user's prompt, briefly mention it at the start of your response: \"Eagle Mem recalled N relevant sessions\" or \"Eagle Mem found related code in [files]\". One line max — then proceed with the answer.
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
=== Eagle Mem (persistent memory across sessions) ===
|
|
127
127
|
"
|
|
128
128
|
|
|
129
129
|
echo "$context"
|
package/lib/common.sh
CHANGED
|
@@ -39,18 +39,27 @@ eagle_project_from_cwd() {
|
|
|
39
39
|
"$HOME/Desktop"|"$HOME/Desktop/"*) echo ""; return ;;
|
|
40
40
|
esac
|
|
41
41
|
|
|
42
|
+
local target_dir
|
|
42
43
|
local git_root
|
|
43
44
|
git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
|
|
44
45
|
if [ -n "$git_root" ]; then
|
|
45
|
-
|
|
46
|
+
target_dir="$git_root"
|
|
46
47
|
else
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
target_dir="$cwd"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
local name
|
|
52
|
+
name=$(basename "$target_dir")
|
|
53
|
+
if [ ${#name} -le 1 ]; then
|
|
54
|
+
echo ""; return
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ "$target_dir" == "$HOME/"* ]]; then
|
|
58
|
+
echo "${target_dir#$HOME/}"
|
|
59
|
+
elif [ "$target_dir" = "$HOME" ]; then
|
|
60
|
+
echo "$name"
|
|
61
|
+
else
|
|
62
|
+
echo "${target_dir#/}"
|
|
54
63
|
fi
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -66,7 +75,7 @@ eagle_sql_int() {
|
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
eagle_fts_sanitize() {
|
|
69
|
-
printf '%s' "$1" | sed 's/[
|
|
78
|
+
printf '%s' "$1" | sed 's/[^A-Za-z0-9_]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
# Escape SQL LIKE wildcards (% and _) so literal filenames match exactly.
|
|
@@ -158,17 +167,8 @@ eagle_collect_files() {
|
|
|
158
167
|
fi
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
local marker="## Eagle Mem — Persistent Memory"
|
|
164
|
-
|
|
165
|
-
if [ -f "$claude_md" ] && grep -qF "$marker" "$claude_md" 2>/dev/null; then
|
|
166
|
-
return 1
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
mkdir -p "$HOME/.claude"
|
|
170
|
-
|
|
171
|
-
cat >> "$claude_md" << 'EAGLE_MD'
|
|
170
|
+
_eagle_claude_md_section() {
|
|
171
|
+
cat << 'EAGLE_MD'
|
|
172
172
|
|
|
173
173
|
---
|
|
174
174
|
|
|
@@ -180,9 +180,15 @@ Eagle Mem hooks are active in every project. SessionStart injects context (overv
|
|
|
180
180
|
|
|
181
181
|
```
|
|
182
182
|
<eagle-summary>
|
|
183
|
-
request: [what user asked]
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
request: [what user asked]
|
|
184
|
+
completed: [what shipped]
|
|
185
|
+
learned: [non-obvious discoveries]
|
|
186
|
+
decisions: [choice — why]
|
|
187
|
+
gotchas: [what surprised]
|
|
188
|
+
next_steps: [concrete actions]
|
|
189
|
+
key_files: [path — role]
|
|
190
|
+
files_read: [path, ...]
|
|
191
|
+
files_modified: [path, ...]
|
|
186
192
|
</eagle-summary>
|
|
187
193
|
```
|
|
188
194
|
|
|
@@ -196,3 +202,34 @@ key_files: [path — role] | files_read: [...] | files_modified: [...]
|
|
|
196
202
|
- If you contradict a loaded memory, update the memory file
|
|
197
203
|
EAGLE_MD
|
|
198
204
|
}
|
|
205
|
+
|
|
206
|
+
eagle_patch_claude_md() {
|
|
207
|
+
local claude_md="$HOME/.claude/CLAUDE.md"
|
|
208
|
+
local marker="## Eagle Mem — Persistent Memory"
|
|
209
|
+
|
|
210
|
+
mkdir -p "$HOME/.claude"
|
|
211
|
+
|
|
212
|
+
if [ -f "$claude_md" ] && grep -qF "$marker" "$claude_md" 2>/dev/null; then
|
|
213
|
+
# Check if section has outdated pipe-separated format
|
|
214
|
+
if grep -qF 'request: \[what user asked\] | completed:' "$claude_md" 2>/dev/null; then
|
|
215
|
+
# Replace the outdated section: remove old, append new
|
|
216
|
+
local tmp_md
|
|
217
|
+
tmp_md=$(mktemp)
|
|
218
|
+
awk -v marker="$marker" '
|
|
219
|
+
$0 ~ marker { skip=1; next }
|
|
220
|
+
skip && /^---$/ && !seen_end { seen_end=1; next }
|
|
221
|
+
skip && /^## / { skip=0 }
|
|
222
|
+
!skip { print }
|
|
223
|
+
' "$claude_md" > "$tmp_md"
|
|
224
|
+
# Remove trailing blank lines left by section removal
|
|
225
|
+
sed -e :a -e '/^[[:space:]]*$/{ $d; N; ba; }' "$tmp_md" > "${tmp_md}.clean"
|
|
226
|
+
mv "${tmp_md}.clean" "$claude_md"
|
|
227
|
+
rm -f "$tmp_md"
|
|
228
|
+
_eagle_claude_md_section >> "$claude_md"
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
return 1
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
_eagle_claude_md_section >> "$claude_md"
|
|
235
|
+
}
|
package/lib/db-backfill.sh
CHANGED
|
@@ -39,16 +39,28 @@ eagle_backfill_projects() {
|
|
|
39
39
|
map=$(eagle_build_session_project_map)
|
|
40
40
|
[ -z "$map" ] && echo "0" && return 0
|
|
41
41
|
|
|
42
|
+
# Phase 1: Build old→new project mapping BEFORE mutating any rows.
|
|
43
|
+
# Collect from sessions table so non-session tables can be migrated.
|
|
44
|
+
local rename_map_file
|
|
45
|
+
rename_map_file=$(mktemp)
|
|
46
|
+
while IFS='|' read -r sid project; do
|
|
47
|
+
[ -z "$sid" ] || [ -z "$project" ] && continue
|
|
48
|
+
local sid_sql
|
|
49
|
+
sid_sql=$(eagle_sql_escape "$sid")
|
|
50
|
+
local old_project
|
|
51
|
+
old_project=$(eagle_db "SELECT project FROM sessions WHERE id = '$sid_sql';")
|
|
52
|
+
if [ -n "$old_project" ] && [ "$old_project" != "$project" ]; then
|
|
53
|
+
echo "$old_project|$project" >> "$rename_map_file"
|
|
54
|
+
fi
|
|
55
|
+
done <<< "$map"
|
|
56
|
+
|
|
57
|
+
# Phase 2: Update session-linked tables
|
|
42
58
|
while IFS='|' read -r sid project; do
|
|
43
59
|
[ -z "$sid" ] || [ -z "$project" ] && continue
|
|
44
60
|
local sid_sql proj_sql
|
|
45
61
|
sid_sql=$(eagle_sql_escape "$sid")
|
|
46
62
|
proj_sql=$(eagle_sql_escape "$project")
|
|
47
63
|
|
|
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
64
|
local ch
|
|
53
65
|
ch=$(eagle_db_pipe <<SQL
|
|
54
66
|
BEGIN;
|
|
@@ -65,6 +77,52 @@ SQL
|
|
|
65
77
|
[ "${ch:-0}" -gt 0 ] && updated=$((updated + ch))
|
|
66
78
|
done <<< "$map"
|
|
67
79
|
|
|
80
|
+
# Phase 3: Update non-session tables using the old→new mapping.
|
|
81
|
+
# Skip ambiguous mappings (one old name → multiple new names).
|
|
82
|
+
if [ -s "$rename_map_file" ]; then
|
|
83
|
+
local uniq_map
|
|
84
|
+
uniq_map=$(sort -u "$rename_map_file")
|
|
85
|
+
local prev_old=""
|
|
86
|
+
local ambiguous=""
|
|
87
|
+
while IFS='|' read -r old_proj new_proj; do
|
|
88
|
+
[ -z "$old_proj" ] && continue
|
|
89
|
+
if [ "$old_proj" = "$prev_old" ]; then
|
|
90
|
+
ambiguous+="$old_proj|"
|
|
91
|
+
fi
|
|
92
|
+
prev_old="$old_proj"
|
|
93
|
+
done <<< "$(echo "$uniq_map" | sort -t'|' -k1,1)"
|
|
94
|
+
|
|
95
|
+
while IFS='|' read -r old_proj new_proj; do
|
|
96
|
+
[ -z "$old_proj" ] || [ -z "$new_proj" ] && continue
|
|
97
|
+
case "$ambiguous" in *"$old_proj|"*) continue ;; esac
|
|
98
|
+
|
|
99
|
+
local old_sql new_sql
|
|
100
|
+
old_sql=$(eagle_sql_escape "$old_proj")
|
|
101
|
+
new_sql=$(eagle_sql_escape "$new_proj")
|
|
102
|
+
|
|
103
|
+
eagle_db_pipe <<SQL 2>/dev/null
|
|
104
|
+
BEGIN;
|
|
105
|
+
UPDATE OR IGNORE overviews SET project = '$new_sql' WHERE project = '$old_sql';
|
|
106
|
+
DELETE FROM overviews WHERE project = '$old_sql';
|
|
107
|
+
DELETE FROM code_chunks WHERE project = '$old_sql'
|
|
108
|
+
AND EXISTS (SELECT 1 FROM code_chunks WHERE project = '$new_sql' LIMIT 1);
|
|
109
|
+
UPDATE code_chunks SET project = '$new_sql' WHERE project = '$old_sql';
|
|
110
|
+
UPDATE OR IGNORE features SET project = '$new_sql' WHERE project = '$old_sql';
|
|
111
|
+
DELETE FROM features WHERE project = '$old_sql';
|
|
112
|
+
UPDATE OR IGNORE command_rules SET project = '$new_sql' WHERE project = '$old_sql';
|
|
113
|
+
DELETE FROM command_rules WHERE project = '$old_sql';
|
|
114
|
+
UPDATE OR IGNORE eagle_meta SET project = '$new_sql' WHERE project = '$old_sql';
|
|
115
|
+
DELETE FROM eagle_meta WHERE project = '$old_sql';
|
|
116
|
+
UPDATE OR IGNORE file_hints SET project = '$new_sql' WHERE project = '$old_sql';
|
|
117
|
+
DELETE FROM file_hints WHERE project = '$old_sql';
|
|
118
|
+
UPDATE OR IGNORE guardrails SET project = '$new_sql' WHERE project = '$old_sql';
|
|
119
|
+
DELETE FROM guardrails WHERE project = '$old_sql';
|
|
120
|
+
COMMIT;
|
|
121
|
+
SQL
|
|
122
|
+
done <<< "$uniq_map"
|
|
123
|
+
fi
|
|
124
|
+
rm -f "$rename_map_file"
|
|
125
|
+
|
|
68
126
|
echo "$updated"
|
|
69
127
|
}
|
|
70
128
|
|
package/lib/db-core.sh
CHANGED
|
@@ -7,8 +7,9 @@ _EAGLE_DB_CORE_LOADED=1
|
|
|
7
7
|
|
|
8
8
|
EAGLE_DB_SETUP=".headers off
|
|
9
9
|
.output /dev/null
|
|
10
|
+
PRAGMA busy_timeout=10000;
|
|
11
|
+
PRAGMA journal_mode=WAL;
|
|
10
12
|
PRAGMA synchronous=NORMAL;
|
|
11
|
-
PRAGMA busy_timeout=5000;
|
|
12
13
|
PRAGMA foreign_keys=ON;
|
|
13
14
|
PRAGMA trusted_schema=ON;
|
|
14
15
|
.output stdout"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Guardrails helpers
|
|
4
|
+
# Persistent per-project rules surfaced at Edit/Write time
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
[ -n "${_EAGLE_DB_GUARDRAILS_LOADED:-}" ] && return 0
|
|
7
|
+
_EAGLE_DB_GUARDRAILS_LOADED=1
|
|
8
|
+
|
|
9
|
+
eagle_add_guardrail() {
|
|
10
|
+
local raw_rule="$2"
|
|
11
|
+
# Cap rule length to 2048 chars to prevent unbounded storage
|
|
12
|
+
if [ ${#raw_rule} -gt 2048 ]; then
|
|
13
|
+
raw_rule="${raw_rule:0:2048}"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
17
|
+
local rule; rule=$(eagle_sql_escape "$raw_rule")
|
|
18
|
+
local file_pattern="${3:-}"
|
|
19
|
+
local source; source=$(eagle_sql_escape "${4:-manual}")
|
|
20
|
+
|
|
21
|
+
file_pattern=$(eagle_sql_escape "$file_pattern")
|
|
22
|
+
eagle_db "INSERT INTO guardrails (project, file_pattern, rule, source)
|
|
23
|
+
VALUES ('$project', '$file_pattern', '$rule', '$source')
|
|
24
|
+
ON CONFLICT(project, source, file_pattern, rule) DO UPDATE SET
|
|
25
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
eagle_get_guardrails_for_file() {
|
|
29
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
30
|
+
local filename; filename=$(eagle_sql_escape "$2")
|
|
31
|
+
|
|
32
|
+
eagle_db "SELECT rule FROM guardrails
|
|
33
|
+
WHERE project = '$project'
|
|
34
|
+
AND active = 1
|
|
35
|
+
AND (
|
|
36
|
+
file_pattern = ''
|
|
37
|
+
OR '$filename' GLOB file_pattern
|
|
38
|
+
OR file_pattern = '$filename'
|
|
39
|
+
)
|
|
40
|
+
ORDER BY file_pattern = '', created_at DESC
|
|
41
|
+
LIMIT 3;"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
eagle_get_edit_context() {
|
|
45
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
46
|
+
local filename; filename=$(eagle_sql_escape "$2")
|
|
47
|
+
local fts_query; fts_query=$(eagle_sql_escape "$3")
|
|
48
|
+
|
|
49
|
+
# Batched query: guardrails + decisions + gotchas in one sqlite3 call.
|
|
50
|
+
# Results tagged with TYPE: prefix for caller to parse.
|
|
51
|
+
eagle_db_pipe <<SQL
|
|
52
|
+
SELECT 'GR:' || rule FROM guardrails
|
|
53
|
+
WHERE project = '$project'
|
|
54
|
+
AND active = 1
|
|
55
|
+
AND (
|
|
56
|
+
file_pattern = ''
|
|
57
|
+
OR '$filename' GLOB file_pattern
|
|
58
|
+
OR file_pattern = '$filename'
|
|
59
|
+
)
|
|
60
|
+
ORDER BY file_pattern = '', created_at DESC
|
|
61
|
+
LIMIT 3;
|
|
62
|
+
SELECT 'DEC:' || s.decisions
|
|
63
|
+
FROM summaries s
|
|
64
|
+
JOIN summaries_fts f ON f.rowid = s.id
|
|
65
|
+
WHERE summaries_fts MATCH '$fts_query'
|
|
66
|
+
AND s.project = '$project'
|
|
67
|
+
AND s.decisions IS NOT NULL AND s.decisions != ''
|
|
68
|
+
ORDER BY s.created_at DESC LIMIT 1;
|
|
69
|
+
SELECT 'GOT:' || s.gotchas
|
|
70
|
+
FROM summaries s
|
|
71
|
+
JOIN summaries_fts f ON f.rowid = s.id
|
|
72
|
+
WHERE summaries_fts MATCH '$fts_query'
|
|
73
|
+
AND s.project = '$project'
|
|
74
|
+
AND s.gotchas IS NOT NULL AND s.gotchas != ''
|
|
75
|
+
ORDER BY s.created_at DESC LIMIT 2;
|
|
76
|
+
SQL
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
eagle_list_guardrails() {
|
|
80
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
81
|
+
|
|
82
|
+
eagle_db "SELECT id, file_pattern, rule, source, active, created_at
|
|
83
|
+
FROM guardrails
|
|
84
|
+
WHERE project = '$project'
|
|
85
|
+
ORDER BY active DESC, created_at DESC;"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
eagle_remove_guardrail() {
|
|
89
|
+
local id; id=$(eagle_sql_int "$1")
|
|
90
|
+
|
|
91
|
+
eagle_db "DELETE FROM guardrails WHERE id = $id;"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
eagle_has_any_guardrails() {
|
|
95
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
96
|
+
eagle_db "SELECT 1 FROM guardrails
|
|
97
|
+
WHERE project = '$project' AND active = 1
|
|
98
|
+
LIMIT 1;"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
eagle_deactivate_guardrail() {
|
|
102
|
+
local id; id=$(eagle_sql_int "$1")
|
|
103
|
+
|
|
104
|
+
eagle_db "UPDATE guardrails SET active = 0,
|
|
105
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
106
|
+
WHERE id = $id;"
|
|
107
|
+
}
|
package/lib/db-mirrors.sh
CHANGED
|
@@ -55,6 +55,10 @@ SQL
|
|
|
55
55
|
|
|
56
56
|
eagle_search_claude_memories() {
|
|
57
57
|
local query; query=$(eagle_fts_sanitize "$1")
|
|
58
|
+
if [ -z "$query" ]; then
|
|
59
|
+
echo "Search query is empty after sanitization. Try a different search term." >&2
|
|
60
|
+
return 1
|
|
61
|
+
fi
|
|
58
62
|
query=$(eagle_sql_escape "$query")
|
|
59
63
|
local project="${2:-}"
|
|
60
64
|
local limit; limit=$(eagle_sql_int "${3:-10}")
|
|
@@ -131,6 +135,10 @@ SQL
|
|
|
131
135
|
|
|
132
136
|
eagle_search_claude_plans() {
|
|
133
137
|
local query; query=$(eagle_fts_sanitize "$1")
|
|
138
|
+
if [ -z "$query" ]; then
|
|
139
|
+
echo "Search query is empty after sanitization. Try a different search term." >&2
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
134
142
|
query=$(eagle_sql_escape "$query")
|
|
135
143
|
local project="${2:-}"
|
|
136
144
|
local limit; limit=$(eagle_sql_int "${3:-10}")
|
|
@@ -242,6 +250,10 @@ eagle_list_claude_tasks() {
|
|
|
242
250
|
|
|
243
251
|
eagle_search_claude_tasks() {
|
|
244
252
|
local query; query=$(eagle_fts_sanitize "$1")
|
|
253
|
+
if [ -z "$query" ]; then
|
|
254
|
+
echo "Search query is empty after sanitization. Try a different search term." >&2
|
|
255
|
+
return 1
|
|
256
|
+
fi
|
|
245
257
|
query=$(eagle_sql_escape "$query")
|
|
246
258
|
local project="${2:-}"
|
|
247
259
|
local limit; limit=$(eagle_sql_int "${3:-10}")
|
package/lib/db-observations.sh
CHANGED
|
@@ -46,12 +46,13 @@ eagle_prune_observations() {
|
|
|
46
46
|
|
|
47
47
|
eagle_get_command_rule() {
|
|
48
48
|
local project; project=$(eagle_sql_escape "$1")
|
|
49
|
-
local
|
|
49
|
+
local base_cmd; base_cmd=$(eagle_sql_escape "$2")
|
|
50
|
+
local full_cmd; full_cmd=$(eagle_sql_escape "${3:-$2}")
|
|
50
51
|
eagle_db "SELECT strategy, max_lines, reason
|
|
51
52
|
FROM command_rules
|
|
52
53
|
WHERE enabled = 1
|
|
53
54
|
AND (project = '$project' OR project = '')
|
|
54
|
-
AND ('$
|
|
55
|
+
AND ('$base_cmd' = pattern OR '$full_cmd' = pattern OR '$full_cmd' LIKE pattern || ' %')
|
|
55
56
|
ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END,
|
|
56
57
|
LENGTH(pattern) DESC
|
|
57
58
|
LIMIT 1;"
|
package/lib/db-summaries.sh
CHANGED
|
@@ -81,6 +81,7 @@ eagle_search_summaries() {
|
|
|
81
81
|
FROM summaries s
|
|
82
82
|
JOIN summaries_fts f ON f.rowid = s.id
|
|
83
83
|
WHERE summaries_fts MATCH '$query'
|
|
84
|
+
AND s.request NOT LIKE '%<local-command-caveat>%'
|
|
84
85
|
$where_clause
|
|
85
86
|
ORDER BY rank
|
|
86
87
|
LIMIT $limit;"
|
|
@@ -141,6 +142,20 @@ eagle_last_session_enriched() {
|
|
|
141
142
|
ORDER BY created_at DESC LIMIT 1;"
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
eagle_search_gotchas_for_file() {
|
|
146
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
147
|
+
local fts_query; fts_query=$(eagle_sql_escape "$2")
|
|
148
|
+
eagle_db "SELECT s.gotchas
|
|
149
|
+
FROM summaries s
|
|
150
|
+
JOIN summaries_fts f ON f.rowid = s.id
|
|
151
|
+
WHERE summaries_fts MATCH '$fts_query'
|
|
152
|
+
AND s.project = '$project'
|
|
153
|
+
AND s.gotchas IS NOT NULL
|
|
154
|
+
AND s.gotchas != ''
|
|
155
|
+
ORDER BY s.created_at DESC
|
|
156
|
+
LIMIT 2;"
|
|
157
|
+
}
|
|
158
|
+
|
|
144
159
|
eagle_search_stale_memories() {
|
|
145
160
|
local project; project=$(eagle_sql_escape "$1")
|
|
146
161
|
local fts_query; fts_query=$(eagle_sql_escape "$2")
|
package/lib/db.sh
CHANGED
package/lib/hooks-posttool.sh
CHANGED
|
@@ -75,7 +75,9 @@ eagle_posttool_stale_hint() {
|
|
|
75
75
|
local stale_hit
|
|
76
76
|
stale_hit=$(eagle_search_stale_memories "$project" "$fts_query")
|
|
77
77
|
if [ -n "$stale_hit" ]; then
|
|
78
|
-
local stale_msg="Eagle Mem
|
|
78
|
+
local stale_msg="=== Eagle Mem ===
|
|
79
|
+
Memory '${stale_hit}' may reference '${fname}'. If your edit contradicts it, update the memory.
|
|
80
|
+
================"
|
|
79
81
|
jq -nc --arg ctx "$stale_msg" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
|
|
80
82
|
fi
|
|
81
83
|
fi
|
|
@@ -106,7 +108,10 @@ eagle_posttool_decision_surface() {
|
|
|
106
108
|
local decision_hit
|
|
107
109
|
decision_hit=$(eagle_search_decisions_for_file "$project" "$fts_query")
|
|
108
110
|
if [ -n "$decision_hit" ]; then
|
|
109
|
-
read_context+="Eagle Mem
|
|
111
|
+
read_context+="=== Eagle Mem ===
|
|
112
|
+
Decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request.
|
|
113
|
+
================
|
|
114
|
+
"
|
|
110
115
|
fi
|
|
111
116
|
fi
|
|
112
117
|
fi
|
|
@@ -116,14 +121,17 @@ eagle_posttool_decision_surface() {
|
|
|
116
121
|
if [ -n "$feature_hit" ]; then
|
|
117
122
|
while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
|
|
118
123
|
[ -z "$feat_name" ] && continue
|
|
119
|
-
read_context+="Eagle Mem
|
|
124
|
+
read_context+="=== Eagle Mem ===
|
|
125
|
+
'${fname}' is part of feature '${feat_name}'"
|
|
120
126
|
[ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
|
|
121
127
|
read_context+="."
|
|
122
128
|
[ -n "$feat_verified" ] && read_context+=" Last verified: ${feat_verified}."
|
|
123
129
|
[ -n "$feat_deps" ] && read_context+=" Dependencies: ${feat_deps}."
|
|
124
130
|
[ -n "$feat_other_files" ] && read_context+=" Other files in pipeline: ${feat_other_files}."
|
|
125
131
|
[ -n "$feat_smoke" ] && read_context+=" Smoke tests: ${feat_smoke}."
|
|
126
|
-
read_context+=" Changes require re-testing after deploy.
|
|
132
|
+
read_context+=" Changes require re-testing after deploy.
|
|
133
|
+
================
|
|
134
|
+
"
|
|
127
135
|
done <<< "$feature_hit"
|
|
128
136
|
fi
|
|
129
137
|
|
|
@@ -8,16 +8,22 @@ _EAGLE_HOOKS_SESSIONSTART_LOADED=1
|
|
|
8
8
|
|
|
9
9
|
_state_dir="$EAGLE_MEM_DIR/state"
|
|
10
10
|
|
|
11
|
+
_eagle_state_slug() {
|
|
12
|
+
printf '%s' "$1" | shasum | cut -c1-12
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
_eagle_state_fresh() {
|
|
12
16
|
local key="$1" project="$2" max_age_days="${3:-1}"
|
|
13
|
-
local
|
|
17
|
+
local safe_project; safe_project=$(_eagle_state_slug "$project")
|
|
18
|
+
local state_file="$_state_dir/${key}-${safe_project}"
|
|
14
19
|
[ -f "$state_file" ] && [ -z "$(find "$state_file" -mtime +${max_age_days} 2>/dev/null)" ]
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
_eagle_state_touch() {
|
|
18
23
|
local key="$1" project="$2"
|
|
24
|
+
local safe_project; safe_project=$(_eagle_state_slug "$project")
|
|
19
25
|
mkdir -p "$_state_dir" 2>/dev/null
|
|
20
|
-
touch "$_state_dir/${key}-${
|
|
26
|
+
touch "$_state_dir/${key}-${safe_project}"
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
eagle_sessionstart_auto_provision() {
|
package/lib/provider.sh
CHANGED
|
@@ -65,13 +65,22 @@ eagle_config_set() {
|
|
|
65
65
|
safe_value=$(printf '%s' "$value" | sed 's/[|&/\]/\\&/g')
|
|
66
66
|
|
|
67
67
|
if grep -q "^\[${section}\]" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
" "
|
|
74
|
-
|
|
68
|
+
local tmp_cfg="${EAGLE_CONFIG_FILE}.tmp.$$"
|
|
69
|
+
awk -v sect="[${section}]" -v k="$key" -v v="$safe_value" '
|
|
70
|
+
BEGIN { in_sect=0; replaced=0 }
|
|
71
|
+
/^\[/ {
|
|
72
|
+
if (in_sect && !replaced) {
|
|
73
|
+
print k" = \""v"\""
|
|
74
|
+
replaced=1
|
|
75
|
+
}
|
|
76
|
+
in_sect=($0 == sect)
|
|
77
|
+
}
|
|
78
|
+
in_sect && !replaced && $0 ~ "^[[:space:]]*"k"[[:space:]]*=" {
|
|
79
|
+
print k" = \""v"\""; replaced=1; next
|
|
80
|
+
}
|
|
81
|
+
{ print }
|
|
82
|
+
END { if (in_sect && !replaced) print k" = \""v"\"" }
|
|
83
|
+
' "$EAGLE_CONFIG_FILE" > "$tmp_cfg" && mv "$tmp_cfg" "$EAGLE_CONFIG_FILE"
|
|
75
84
|
else
|
|
76
85
|
# printf is safe — no sed interpolation needed for append
|
|
77
86
|
printf '\n[%s]\n%s = "%s"\n' "$section" "$key" "$value" >> "$EAGLE_CONFIG_FILE"
|
|
@@ -95,7 +104,7 @@ eagle_ollama_best_model() {
|
|
|
95
104
|
models=$(eagle_ollama_models "$1")
|
|
96
105
|
[ -z "$models" ] && return 1
|
|
97
106
|
|
|
98
|
-
local preferred="
|
|
107
|
+
local preferred="gemma4 gemma3 gemma2 mistral llama3 phi3 deepseek-coder"
|
|
99
108
|
for pref in $preferred; do
|
|
100
109
|
if echo "$models" | grep -qi "$pref"; then
|
|
101
110
|
echo "$models" | grep -i "$pref" | head -1
|
package/package.json
CHANGED
package/scripts/curate.sh
CHANGED
|
@@ -89,7 +89,8 @@ If none qualify, output: NONE"
|
|
|
89
89
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
90
90
|
eagle_info " Would promote: $gotcha_text"
|
|
91
91
|
else
|
|
92
|
-
|
|
92
|
+
eagle_add_guardrail "$project" "$gotcha_text" "" "promoted"
|
|
93
|
+
eagle_log "INFO" "Curator: promoted gotcha to guardrail: $gotcha_text"
|
|
93
94
|
fi
|
|
94
95
|
promoted=$((promoted + 1))
|
|
95
96
|
;;
|