eagle-mem 3.0.0 → 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.
- package/db/015_integrity_fixes.sql +156 -0
- package/db/migrate.sh +3 -0
- package/hooks/post-tool-use.sh +18 -147
- package/hooks/pre-tool-use.sh +4 -23
- package/hooks/session-end.sh +4 -1
- package/hooks/session-start.sh +23 -29
- package/hooks/stop.sh +6 -3
- package/lib/common.sh +34 -8
- package/lib/db-backfill.sh +96 -0
- package/lib/db-core.sh +65 -0
- package/lib/db-features.sh +160 -0
- package/lib/db-mirrors.sh +264 -0
- package/lib/db-observations.sh +77 -0
- package/lib/db-sessions.sh +68 -0
- package/lib/db-summaries.sh +142 -0
- package/lib/db.sh +14 -706
- package/lib/hooks-posttool.sh +138 -0
- package/lib/provider.sh +22 -6
- package/package.json +1 -1
- package/scripts/curate.sh +16 -0
- package/scripts/install.sh +5 -2
- package/scripts/memories.sh +3 -3
- package/scripts/uninstall.sh +1 -1
- package/scripts/update.sh +5 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — PostToolUse extracted responsibilities
|
|
4
|
+
# Source from hooks/post-tool-use.sh after common.sh + db.sh
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
[ -n "${_EAGLE_HOOKS_POSTTOOL_LOADED:-}" ] && return 0
|
|
7
|
+
_EAGLE_HOOKS_POSTTOOL_LOADED=1
|
|
8
|
+
|
|
9
|
+
eagle_posttool_mirror_writes() {
|
|
10
|
+
local tool_name="$1" fp="$2" session_id="$3" project="$4"
|
|
11
|
+
|
|
12
|
+
case "$tool_name" in
|
|
13
|
+
Write|Edit)
|
|
14
|
+
if [ -n "$fp" ]; then
|
|
15
|
+
case "$fp" in
|
|
16
|
+
*..*) ;; # path traversal — skip
|
|
17
|
+
"$EAGLE_CLAUDE_PROJECTS_DIR"/*/memory/*.md)
|
|
18
|
+
local mem_base
|
|
19
|
+
mem_base=$(basename "$fp")
|
|
20
|
+
if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
|
|
21
|
+
eagle_capture_claude_memory "$fp" "$session_id" "$project"
|
|
22
|
+
fi
|
|
23
|
+
;;
|
|
24
|
+
"$EAGLE_CLAUDE_PLANS_DIR/"*.md)
|
|
25
|
+
if [ -f "$fp" ]; then
|
|
26
|
+
eagle_capture_claude_plan "$fp" "$session_id" "$project"
|
|
27
|
+
fi
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
fi
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
eagle_posttool_mirror_tasks() {
|
|
36
|
+
local tool_name="$1" session_id="$2" project="$3" input="$4"
|
|
37
|
+
|
|
38
|
+
case "$tool_name" in
|
|
39
|
+
TaskCreate|TaskUpdate)
|
|
40
|
+
if eagle_validate_session_id "$session_id"; then
|
|
41
|
+
local task_dir="$EAGLE_CLAUDE_TASKS_DIR/$session_id"
|
|
42
|
+
if [ -d "$task_dir" ]; then
|
|
43
|
+
local task_id
|
|
44
|
+
task_id=$(echo "$input" | jq -r '.tool_input.id // empty')
|
|
45
|
+
if [ -z "$task_id" ]; then
|
|
46
|
+
local newest
|
|
47
|
+
newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
|
|
48
|
+
[ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
|
|
49
|
+
elif eagle_validate_session_id "$task_id"; then
|
|
50
|
+
local task_json="$task_dir/$task_id.json"
|
|
51
|
+
[ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
eagle_posttool_stale_hint() {
|
|
60
|
+
local tool_name="$1" fp="$2" project="$3"
|
|
61
|
+
|
|
62
|
+
case "$tool_name" in
|
|
63
|
+
Write|Edit)
|
|
64
|
+
if [ -n "$fp" ]; then
|
|
65
|
+
local fname fname_stem
|
|
66
|
+
fname=$(basename "$fp")
|
|
67
|
+
fname_stem="${fname%.*}"
|
|
68
|
+
case "$fp" in
|
|
69
|
+
"$HOME/.claude/"*) ;; # skip Claude config files
|
|
70
|
+
*)
|
|
71
|
+
if [ ${#fname_stem} -ge 3 ]; then
|
|
72
|
+
local fts_query
|
|
73
|
+
fts_query=$(eagle_fts_sanitize "$fname_stem")
|
|
74
|
+
if [ -n "$fts_query" ]; then
|
|
75
|
+
local stale_hit
|
|
76
|
+
stale_hit=$(eagle_search_stale_memories "$project" "$fts_query")
|
|
77
|
+
if [ -n "$stale_hit" ]; then
|
|
78
|
+
local stale_msg="Eagle Mem: Memory '${stale_hit}' may reference '${fname}'. If your edit contradicts it, update the memory."
|
|
79
|
+
jq -nc --arg ctx "$stale_msg" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
;;
|
|
84
|
+
esac
|
|
85
|
+
fi
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
eagle_posttool_decision_surface() {
|
|
91
|
+
local tool_name="$1" fp="$2" project="$3"
|
|
92
|
+
|
|
93
|
+
case "$tool_name" in
|
|
94
|
+
Read)
|
|
95
|
+
if [ -n "$fp" ]; then
|
|
96
|
+
local fname fname_stem read_context=""
|
|
97
|
+
fname=$(basename "$fp")
|
|
98
|
+
fname_stem="${fname%.*}"
|
|
99
|
+
case "$fp" in
|
|
100
|
+
"$HOME/.claude/"*) ;; # skip Claude config files
|
|
101
|
+
*)
|
|
102
|
+
if [ ${#fname_stem} -ge 3 ]; then
|
|
103
|
+
local fts_query
|
|
104
|
+
fts_query=$(eagle_fts_sanitize "$fname_stem")
|
|
105
|
+
if [ -n "$fts_query" ]; then
|
|
106
|
+
local decision_hit
|
|
107
|
+
decision_hit=$(eagle_search_decisions_for_file "$project" "$fts_query")
|
|
108
|
+
if [ -n "$decision_hit" ]; then
|
|
109
|
+
read_context+="Eagle Mem decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request. "
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
local feature_hit
|
|
115
|
+
feature_hit=$(eagle_find_features_for_file "$project" "$fp")
|
|
116
|
+
if [ -n "$feature_hit" ]; then
|
|
117
|
+
while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
|
|
118
|
+
[ -z "$feat_name" ] && continue
|
|
119
|
+
read_context+="Eagle Mem: '${fname}' is part of feature '${feat_name}'"
|
|
120
|
+
[ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
|
|
121
|
+
read_context+="."
|
|
122
|
+
[ -n "$feat_verified" ] && read_context+=" Last verified: ${feat_verified}."
|
|
123
|
+
[ -n "$feat_deps" ] && read_context+=" Dependencies: ${feat_deps}."
|
|
124
|
+
[ -n "$feat_other_files" ] && read_context+=" Other files in pipeline: ${feat_other_files}."
|
|
125
|
+
[ -n "$feat_smoke" ] && read_context+=" Smoke tests: ${feat_smoke}."
|
|
126
|
+
read_context+=" Changes require re-testing after deploy. "
|
|
127
|
+
done <<< "$feature_hit"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [ -n "$read_context" ]; then
|
|
131
|
+
jq -nc --arg ctx "$read_context" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
|
|
132
|
+
fi
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
fi
|
|
136
|
+
;;
|
|
137
|
+
esac
|
|
138
|
+
}
|
package/lib/provider.sh
CHANGED
|
@@ -50,19 +50,30 @@ eagle_config_set() {
|
|
|
50
50
|
local key="$2"
|
|
51
51
|
local value="$3"
|
|
52
52
|
|
|
53
|
+
# Validate section/key are alphanumeric+underscore (safe for grep/sed patterns)
|
|
54
|
+
if [[ ! "$section" =~ ^[A-Za-z0-9_-]+$ ]] || [[ ! "$key" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
|
55
|
+
eagle_log "ERROR" "config_set: invalid section/key: [$section] $key"
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
53
59
|
if [ ! -f "$EAGLE_CONFIG_FILE" ]; then
|
|
54
60
|
eagle_config_init
|
|
55
61
|
fi
|
|
56
62
|
|
|
63
|
+
# Escape sed metacharacters in value to prevent injection via |, &, \, /
|
|
64
|
+
local safe_value
|
|
65
|
+
safe_value=$(printf '%s' "$value" | sed 's/[|&/\]/\\&/g')
|
|
66
|
+
|
|
57
67
|
if grep -q "^\[${section}\]" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
|
|
58
68
|
if grep -q "^[[:space:]]*${key}[[:space:]]*=" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
|
|
59
|
-
sed -i '' "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${
|
|
69
|
+
sed -i '' "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${safe_value}\"|" "$EAGLE_CONFIG_FILE"
|
|
60
70
|
else
|
|
61
71
|
sed -i '' "/^\[${section}\]/a\\
|
|
62
|
-
${key} = \"${
|
|
72
|
+
${key} = \"${safe_value}\"
|
|
63
73
|
" "$EAGLE_CONFIG_FILE"
|
|
64
74
|
fi
|
|
65
75
|
else
|
|
76
|
+
# printf is safe — no sed interpolation needed for append
|
|
66
77
|
printf '\n[%s]\n%s = "%s"\n' "$section" "$key" "$value" >> "$EAGLE_CONFIG_FILE"
|
|
67
78
|
fi
|
|
68
79
|
}
|
|
@@ -115,7 +126,10 @@ eagle_config_init() {
|
|
|
115
126
|
model="gpt-4o-mini"
|
|
116
127
|
fi
|
|
117
128
|
|
|
118
|
-
|
|
129
|
+
# Create config with restrictive permissions from the start (no TOCTOU window)
|
|
130
|
+
(
|
|
131
|
+
umask 077
|
|
132
|
+
cat > "$EAGLE_CONFIG_FILE" << TOML
|
|
119
133
|
# Eagle Mem configuration
|
|
120
134
|
# Docs: https://github.com/eagleisbatman/eagle-mem
|
|
121
135
|
|
|
@@ -144,7 +158,7 @@ schedule = "manual"
|
|
|
144
158
|
# Additional secret patterns (regex) beyond built-in defaults
|
|
145
159
|
# extra_patterns = ["MY_CUSTOM_SECRET_.*"]
|
|
146
160
|
TOML
|
|
147
|
-
|
|
161
|
+
)
|
|
148
162
|
eagle_log "INFO" "Config initialized: provider=$provider model=$model"
|
|
149
163
|
}
|
|
150
164
|
|
|
@@ -236,11 +250,12 @@ _eagle_call_anthropic() {
|
|
|
236
250
|
messages: [{role: "user", content: $prompt}]
|
|
237
251
|
}')
|
|
238
252
|
|
|
253
|
+
# Pass API key via config stdin to avoid exposing it in process list (ps aux)
|
|
239
254
|
local response
|
|
240
255
|
response=$(curl -sf "https://api.anthropic.com/v1/messages" \
|
|
241
256
|
--connect-timeout 5 \
|
|
242
257
|
--max-time 120 \
|
|
243
|
-
-
|
|
258
|
+
-K <(printf 'header = "x-api-key: %s"' "$api_key") \
|
|
244
259
|
-H "anthropic-version: 2023-06-01" \
|
|
245
260
|
-H "content-type: application/json" \
|
|
246
261
|
-d "$body" 2>/dev/null)
|
|
@@ -280,11 +295,12 @@ _eagle_call_openai() {
|
|
|
280
295
|
]
|
|
281
296
|
}')
|
|
282
297
|
|
|
298
|
+
# Pass API key via config stdin to avoid exposing it in process list (ps aux)
|
|
283
299
|
local response
|
|
284
300
|
response=$(curl -sf "https://api.openai.com/v1/chat/completions" \
|
|
285
301
|
--connect-timeout 5 \
|
|
286
302
|
--max-time 120 \
|
|
287
|
-
-
|
|
303
|
+
-K <(printf 'header = "Authorization: Bearer %s"' "$api_key") \
|
|
288
304
|
-H "content-type: application/json" \
|
|
289
305
|
-d "$body" 2>/dev/null)
|
|
290
306
|
|
package/package.json
CHANGED
package/scripts/curate.sh
CHANGED
|
@@ -213,6 +213,16 @@ If no rules needed, output: NONE"
|
|
|
213
213
|
max_lines=$(echo "$max_lines" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
214
214
|
reason=$(echo "$reason" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
215
215
|
|
|
216
|
+
# Guard: skip malformed lines missing required fields
|
|
217
|
+
if [ -z "$pattern" ] || [ -z "$strategy" ]; then
|
|
218
|
+
eagle_log "WARN" "Curator: skipping malformed RULE line: $line"
|
|
219
|
+
continue
|
|
220
|
+
fi
|
|
221
|
+
case "$strategy" in summary|truncate) ;; *)
|
|
222
|
+
eagle_log "WARN" "Curator: skipping RULE with invalid strategy '$strategy'"
|
|
223
|
+
continue
|
|
224
|
+
;; esac
|
|
225
|
+
|
|
216
226
|
[ "$max_lines" = "-" ] && max_lines=""
|
|
217
227
|
|
|
218
228
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
@@ -292,6 +302,12 @@ Rules:
|
|
|
292
302
|
fdesc=$(echo "$fdesc" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
293
303
|
ffiles=$(echo "$ffiles" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
294
304
|
|
|
305
|
+
# Guard: skip malformed lines missing required name
|
|
306
|
+
if [ -z "$fname" ]; then
|
|
307
|
+
eagle_log "WARN" "Curator: skipping malformed FEATURE line: $line"
|
|
308
|
+
continue
|
|
309
|
+
fi
|
|
310
|
+
|
|
295
311
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
296
312
|
eagle_info " Feature: $fname — $fdesc"
|
|
297
313
|
eagle_info " Files: $ffiles"
|
package/scripts/install.sh
CHANGED
|
@@ -152,7 +152,10 @@ eagle_ok "Files copied to $EAGLE_MEM_DIR"
|
|
|
152
152
|
|
|
153
153
|
# ─── Run migrations ────────────────────────────────────────
|
|
154
154
|
|
|
155
|
-
"$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null
|
|
155
|
+
if ! "$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null; then
|
|
156
|
+
eagle_err "Database migration failed"
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
156
159
|
eagle_ok "Database ready"
|
|
157
160
|
|
|
158
161
|
# ─── Patch settings.json ───────────────────────────────────
|
|
@@ -242,7 +245,7 @@ else
|
|
|
242
245
|
echo ""
|
|
243
246
|
eagle_ok "Statusline ${DIM}(manual patch needed — instructions above)${RESET}"
|
|
244
247
|
else
|
|
245
|
-
eagle_ok "Statusline ${DIM}(
|
|
248
|
+
eagle_ok "Statusline ${DIM}(existing — cannot auto-patch; add Eagle Mem manually)${RESET}"
|
|
246
249
|
fi
|
|
247
250
|
fi
|
|
248
251
|
|
package/scripts/memories.sh
CHANGED
|
@@ -438,7 +438,7 @@ memories_sync() {
|
|
|
438
438
|
eagle_info "Scanning for Claude Code auto-memory files..."
|
|
439
439
|
echo ""
|
|
440
440
|
|
|
441
|
-
local claude_mem_root="$
|
|
441
|
+
local claude_mem_root="$EAGLE_CLAUDE_PROJECTS_DIR"
|
|
442
442
|
local mem_synced=0
|
|
443
443
|
local mem_skipped=0
|
|
444
444
|
|
|
@@ -471,7 +471,7 @@ memories_sync() {
|
|
|
471
471
|
eagle_info "Scanning for Claude Code plan files..."
|
|
472
472
|
echo ""
|
|
473
473
|
|
|
474
|
-
local plans_dir="$
|
|
474
|
+
local plans_dir="$EAGLE_CLAUDE_PLANS_DIR"
|
|
475
475
|
local plan_synced=0
|
|
476
476
|
local plan_skipped=0
|
|
477
477
|
|
|
@@ -504,7 +504,7 @@ memories_sync() {
|
|
|
504
504
|
eagle_info "Scanning for Claude Code task files..."
|
|
505
505
|
echo ""
|
|
506
506
|
|
|
507
|
-
local tasks_dir="$
|
|
507
|
+
local tasks_dir="$EAGLE_CLAUDE_TASKS_DIR"
|
|
508
508
|
local task_synced=0
|
|
509
509
|
local task_skipped=0
|
|
510
510
|
|
package/scripts/uninstall.sh
CHANGED
|
@@ -18,7 +18,7 @@ eagle_header "Uninstall"
|
|
|
18
18
|
# ─── Remove hooks from settings.json ──────────────────────
|
|
19
19
|
|
|
20
20
|
if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
|
|
21
|
-
for event in SessionStart Stop PostToolUse SessionEnd UserPromptSubmit; do
|
|
21
|
+
for event in SessionStart Stop PostToolUse PreToolUse SessionEnd UserPromptSubmit; do
|
|
22
22
|
if jq -e ".hooks.${event}" "$SETTINGS" &>/dev/null; then
|
|
23
23
|
tmp=$(mktemp)
|
|
24
24
|
jq ".hooks.${event} = [.hooks.${event}[]? | select(any(.hooks[]?; .command | contains(\"eagle-mem\")) | not)]" "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
|
package/scripts/update.sh
CHANGED
|
@@ -46,7 +46,11 @@ eagle_ok "Files updated"
|
|
|
46
46
|
|
|
47
47
|
# ─── Run pending migrations ────────────────────────────────
|
|
48
48
|
|
|
49
|
-
migration_output=$("$EAGLE_MEM_DIR/db/migrate.sh" 2
|
|
49
|
+
migration_output=$("$EAGLE_MEM_DIR/db/migrate.sh" 2>&1) || {
|
|
50
|
+
eagle_err "Database migration failed"
|
|
51
|
+
eagle_err "$migration_output"
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
50
54
|
if echo "$migration_output" | grep -q "applied:"; then
|
|
51
55
|
echo "$migration_output" | grep "applied:" | while read -r line; do
|
|
52
56
|
eagle_ok "Migration: ${line#*applied: }"
|