eagle-mem 3.0.1 → 3.1.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,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
@@ -151,8 +151,9 @@ model = "claude-haiku-4-5-20251001"
151
151
  model = "gpt-4o-mini"
152
152
 
153
153
  [curator]
154
- # How often the curator runs: "manual", "daily", "weekly"
154
+ # "auto" = triggers at session end after min_sessions; "manual" = CLI only
155
155
  schedule = "manual"
156
+ min_sessions = 5
156
157
 
157
158
  [redaction]
158
159
  # Additional secret patterns (regex) beyond built-in defaults
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "3.0.1",
3
+ "version": "3.1.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
@@ -361,5 +361,6 @@ echo ""
361
361
  if [ "$DRY_RUN" -eq 1 ]; then
362
362
  eagle_footer "Dry run complete. Run without --dry-run to apply changes."
363
363
  else
364
+ eagle_meta_set "last_curated_at" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$project"
364
365
  eagle_footer "Curation complete for '$project'."
365
366
  fi
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Health Check
4
+ # Diagnoses how well the self-learning pipeline is working
5
+ # ═══════════════════════════════════════════════════════════
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ LIB_DIR="$SCRIPT_DIR/../lib"
10
+
11
+ . "$LIB_DIR/common.sh"
12
+ . "$SCRIPT_DIR/style.sh"
13
+ . "$LIB_DIR/db.sh"
14
+ . "$LIB_DIR/provider.sh"
15
+
16
+ eagle_header "Health Check"
17
+
18
+ project=""
19
+ JSON_OUT=0
20
+
21
+ while [ $# -gt 0 ]; do
22
+ case "$1" in
23
+ -p|--project) project="$2"; shift 2 ;;
24
+ -j|--json) JSON_OUT=1; shift ;;
25
+ *) shift ;;
26
+ esac
27
+ done
28
+
29
+ if [ -z "$project" ]; then
30
+ project=$(eagle_project_from_cwd "$(pwd)")
31
+ fi
32
+
33
+ if [ -z "$project" ]; then
34
+ eagle_err "Cannot determine project (ephemeral directory). Use -p <project>."
35
+ exit 1
36
+ fi
37
+
38
+ p_esc=$(eagle_sql_escape "$project")
39
+
40
+ eagle_info "Project: ${BOLD}$project${RESET}"
41
+ echo ""
42
+
43
+ score=0
44
+ max_score=0
45
+ issues=()
46
+
47
+ # ─── 1. Summary enrichment rate ──────────────────────────
48
+
49
+ max_score=$((max_score + 30))
50
+
51
+ total_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc';")
52
+ enriched_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc' AND (decisions IS NOT NULL AND decisions != '' OR gotchas IS NOT NULL AND gotchas != '' OR key_files IS NOT NULL AND key_files != '');")
53
+
54
+ if [ "${total_summaries:-0}" -eq 0 ]; then
55
+ enrich_pct=0
56
+ else
57
+ enrich_pct=$((enriched_summaries * 100 / total_summaries))
58
+ fi
59
+
60
+ if [ "$enrich_pct" -ge 50 ]; then
61
+ eagle_ok "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%)"
62
+ score=$((score + 30))
63
+ elif [ "$enrich_pct" -ge 20 ]; then
64
+ eagle_warn "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — aim for 50%+"
65
+ score=$((score + 15))
66
+ issues+=("Low enrichment rate (${enrich_pct}%). Eagle-summary blocks aren't being emitted reliably.")
67
+ elif [ "${total_summaries:-0}" -gt 0 ]; then
68
+ eagle_fail "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — self-learning not working"
69
+ issues+=("Critical: ${enrich_pct}% enrichment. Decisions/gotchas/key_files are not being captured.")
70
+ else
71
+ eagle_dim " No summaries yet"
72
+ fi
73
+
74
+ # ─── 2. Feature discovery ────────────────────────────────
75
+
76
+ max_score=$((max_score + 20))
77
+
78
+ feature_count=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
79
+ feature_file_count=$(eagle_db "SELECT COUNT(*) FROM feature_files ff JOIN features f ON ff.feature_id = f.id WHERE f.project = '$p_esc' AND f.status = 'active';")
80
+
81
+ if [ "${feature_count:-0}" -ge 3 ]; then
82
+ eagle_ok "Features tracked: ${feature_count} (${feature_file_count} files mapped)"
83
+ score=$((score + 20))
84
+ elif [ "${feature_count:-0}" -ge 1 ]; then
85
+ eagle_warn "Features tracked: ${feature_count} — curator needs more sessions"
86
+ score=$((score + 10))
87
+ issues+=("Only ${feature_count} features discovered. Run curator more often.")
88
+ else
89
+ eagle_fail "Features tracked: 0 — feature graph is empty"
90
+ issues+=("No features discovered. Run: eagle-mem curate")
91
+ fi
92
+
93
+ # ─── 3. Command intelligence ─────────────────────────────
94
+
95
+ max_score=$((max_score + 15))
96
+
97
+ rule_count=$(eagle_db "SELECT COUNT(*) FROM command_rules WHERE (project = '$p_esc' OR project IS NULL) AND enabled = 1;")
98
+ obs_with_metrics=$(eagle_db "SELECT COUNT(*) FROM observations WHERE project = '$p_esc' AND tool_name = 'Bash' AND output_bytes IS NOT NULL AND output_bytes > 0;")
99
+
100
+ if [ "${rule_count:-0}" -ge 2 ]; then
101
+ eagle_ok "Command rules: ${rule_count} active (${obs_with_metrics} observations with metrics)"
102
+ score=$((score + 15))
103
+ elif [ "${rule_count:-0}" -ge 1 ]; then
104
+ eagle_warn "Command rules: ${rule_count} — learning in progress"
105
+ score=$((score + 8))
106
+ elif [ "${obs_with_metrics:-0}" -gt 20 ]; then
107
+ eagle_fail "Command rules: 0 (but ${obs_with_metrics} observations available — run curator)"
108
+ issues+=("Command metrics collected but no rules generated yet.")
109
+ else
110
+ eagle_dim " Command metrics: ${obs_with_metrics} observations (need more data)"
111
+ score=$((score + 5))
112
+ fi
113
+
114
+ # ─── 4. Provider configured ──────────────────────────────
115
+
116
+ max_score=$((max_score + 15))
117
+
118
+ provider=$(eagle_config_get "provider" "type" "none")
119
+ if [ "$provider" != "none" ]; then
120
+ model=$(eagle_config_get "$provider" "model" "default")
121
+ eagle_ok "LLM provider: ${provider} (${model})"
122
+ score=$((score + 15))
123
+ else
124
+ eagle_fail "No LLM provider — curator and enrichment extraction disabled"
125
+ issues+=("Configure a provider: eagle-mem config init")
126
+ fi
127
+
128
+ # ─── 5. Project data quality ─────────────────────────────
129
+
130
+ max_score=$((max_score + 10))
131
+
132
+ tmp_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project IN ('tmp', 'private', '');")
133
+ total_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions;")
134
+
135
+ if [ "${total_sessions:-0}" -eq 0 ]; then
136
+ noise_pct=0
137
+ else
138
+ noise_pct=$((tmp_sessions * 100 / total_sessions))
139
+ fi
140
+
141
+ if [ "$noise_pct" -le 5 ]; then
142
+ eagle_ok "Data quality: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
143
+ score=$((score + 10))
144
+ elif [ "$noise_pct" -le 20 ]; then
145
+ eagle_warn "Data quality: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
146
+ score=$((score + 5))
147
+ issues+=("${noise_pct}% of sessions from ephemeral dirs. Skiplist should prevent new ones.")
148
+ else
149
+ eagle_fail "Data quality: ${noise_pct}% noise — ${tmp_sessions}/${total_sessions} sessions from ephemeral dirs"
150
+ issues+=("Heavy ephemeral pollution. Update Eagle Mem to get skiplist protection.")
151
+ fi
152
+
153
+ # ─── 6. Curator activity ─────────────────────────────────
154
+
155
+ max_score=$((max_score + 10))
156
+
157
+ curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
158
+ last_curated=$(eagle_db "SELECT value FROM eagle_meta WHERE key = 'last_curated_at' AND (project = '$p_esc' OR project IS NULL) ORDER BY CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END LIMIT 1;" 2>/dev/null || echo "")
159
+
160
+ if [ -n "$last_curated" ]; then
161
+ eagle_ok "Curator: last run ${last_curated} (schedule: ${curator_schedule})"
162
+ score=$((score + 10))
163
+ elif [ "$curator_schedule" = "auto" ]; then
164
+ eagle_warn "Curator: auto-scheduled but hasn't run yet"
165
+ score=$((score + 5))
166
+ issues+=("Auto-curate is configured but hasn't run. It triggers at session end.")
167
+ else
168
+ eagle_fail "Curator: never run (schedule: ${curator_schedule})"
169
+ issues+=("Curator has never run. Try: eagle-mem curate --dry-run")
170
+ fi
171
+
172
+ # ─── Score ────────────────────────────────────────────────
173
+
174
+ echo ""
175
+ echo -e " ${DIM}─────────────────────────────────────${RESET}"
176
+
177
+ pct=$((score * 100 / max_score))
178
+ if [ "$pct" -ge 80 ]; then
179
+ color="$GREEN"
180
+ grade="Healthy"
181
+ elif [ "$pct" -ge 50 ]; then
182
+ color="$YELLOW"
183
+ grade="Needs attention"
184
+ else
185
+ color="$RED"
186
+ grade="Unhealthy"
187
+ fi
188
+
189
+ echo -e " ${BOLD}Score: ${color}${score}/${max_score} (${pct}%)${RESET} ${color}${grade}${RESET}"
190
+
191
+ if [ ${#issues[@]} -gt 0 ]; then
192
+ echo ""
193
+ echo -e " ${BOLD}Issues:${RESET}"
194
+ for issue in "${issues[@]}"; do
195
+ echo -e " ${YELLOW}!${RESET} $issue"
196
+ done
197
+ fi
198
+
199
+ eagle_footer "Health check complete."
200
+
201
+ if [ "$JSON_OUT" -eq 1 ]; then
202
+ jq -nc \
203
+ --arg project "$project" \
204
+ --argjson score "$score" \
205
+ --argjson max_score "$max_score" \
206
+ --argjson pct "$pct" \
207
+ --arg grade "$grade" \
208
+ --argjson total_summaries "${total_summaries:-0}" \
209
+ --argjson enriched_summaries "${enriched_summaries:-0}" \
210
+ --argjson features "${feature_count:-0}" \
211
+ --argjson command_rules "${rule_count:-0}" \
212
+ --arg provider "$provider" \
213
+ --argjson noise_pct "$noise_pct" \
214
+ '{project:$project, score:$score, max:$max_score, pct:$pct, grade:$grade,
215
+ enrichment:{total:$total_summaries, enriched:$enriched_summaries},
216
+ features:$features, command_rules:$command_rules,
217
+ provider:$provider, noise_pct:$noise_pct}'
218
+ fi
package/scripts/help.sh CHANGED
@@ -27,6 +27,7 @@ echo -e " ${CYAN}prune${RESET} Remove old observations and orphaned chu
27
27
  echo -e " ${CYAN}config${RESET} View or change LLM provider settings"
28
28
  echo -e " ${CYAN}curate${RESET} Run the self-learning curator (LLM-powered analysis)"
29
29
  echo -e " ${CYAN}feature${RESET} Manage feature graph (list/show/verify/add)"
30
+ echo -e " ${CYAN}health${RESET} Diagnose self-learning pipeline health"
30
31
  echo -e " ${CYAN}install${RESET} First-time setup: hooks, database, skills"
31
32
  echo -e " ${CYAN}update${RESET} Re-deploy hooks and run migrations"
32
33
  echo -e " ${CYAN}uninstall${RESET} Remove hooks and optionally delete data"
@@ -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="$HOME/.claude/projects"
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="$HOME/.claude/plans"
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="$HOME/.claude/tasks"
507
+ local tasks_dir="$EAGLE_CLAUDE_TASKS_DIR"
508
508
  local task_synced=0
509
509
  local task_skipped=0
510
510