eagle-mem 4.0.3 → 4.1.1

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
@@ -75,8 +75,8 @@ Six hooks fire automatically at different points in Claude Code's lifecycle:
75
75
 
76
76
  | Hook | Fires when | What it does |
77
77
  |------|-----------|--------------|
78
- | **SessionStart** | startup, resume, clear, compact | Injects overview, summaries, memories, tasks. Auto-provisions new projects (scan, index). |
79
- | **PreToolUse** | before Bash and Read calls | Rewrites noisy commands (learned rules), detects redundant reads |
78
+ | **SessionStart** | startup, resume, clear, compact | Injects overview, summaries, memories, tasks, core files, working set. Auto-provisions new projects (scan, index). |
79
+ | **PreToolUse** | before Bash, Read, Edit, and Write calls | Rewrites noisy commands (learned rules), detects redundant reads, nudges co-edit partners |
80
80
  | **UserPromptSubmit** | user sends a message | FTS5 search for relevant past context |
81
81
  | **PostToolUse** | after tool calls | Records file touches, mirrors memory/plan/task writes, tracks modifications |
82
82
  | **Stop** | Claude's turn ends | Extracts `<eagle-summary>`, strips `<private>` tags |
@@ -89,7 +89,7 @@ These run automatically via SessionStart — no commands needed:
89
89
  - **Auto-scan** — new project with no overview triggers a codebase scan
90
90
  - **Auto-index** — new or stale project triggers FTS5 source indexing
91
91
  - **Auto-prune** — observations over 10K rows trigger cleanup
92
- - **Auto-curate** — the self-learning curator analyzes observation data and generates project-specific command rules (requires LLM provider)
92
+ - **Auto-curate** — the self-learning curator analyzes observation data and generates command rules, co-edit patterns, and hot file detection (partially requires LLM provider)
93
93
 
94
94
  ### Token savings
95
95
 
@@ -99,6 +99,10 @@ Eagle Mem actively reduces token consumption:
99
99
  - **Command rewriting** — PreToolUse rewrites noisy Bash commands to pipe through `head -N` via `updatedInput`. Rules are learned by the curator from real usage, not hardcoded.
100
100
  - **Read-after-modify detection** — detects when you read a file that was just edited or written, nudges that the diff is already in context
101
101
  - **Read dedup tracking** — files read 3+ times in a session get a soft nudge
102
+ - **Co-edit nudges** — learned from observation data: when you edit file X, PreToolUse reminds you that you usually also touch file Y
103
+ - **Hot file awareness** — curator identifies files read in 50%+ of sessions; SessionStart flags them as "likely in context" to reduce re-reads
104
+ - **Working set recovery** — on compact, SessionStart injects the files you were actively editing so you resume without re-reading everything
105
+ - **Stuck loop detection** — if the same file is edited 5+ times in one session, PreToolUse nudges to reconsider the approach
102
106
 
103
107
  ### Data
104
108
 
@@ -112,6 +116,7 @@ Single SQLite database at `~/.eagle-mem/memory.db` (WAL mode, FTS5 full-text sea
112
116
  | overviews | One overview per project (auto-scan or manual) |
113
117
  | code_chunks | FTS5-indexed source file chunks |
114
118
  | command_rules | Curator-learned command output rules |
119
+ | file_hints | Curator-learned file access patterns (co-edit pairs) |
115
120
  | claude_memories | Mirror of Claude Code auto-memories |
116
121
  | claude_plans | Mirror of Claude Code plans |
117
122
  | claude_tasks | Mirror of Claude Code tasks |
@@ -0,0 +1,15 @@
1
+ -- Migration 021: File hints — learned file access patterns from curator
2
+ -- Stores co-edit patterns, hot files, and other learned behaviors.
3
+
4
+ CREATE TABLE IF NOT EXISTS file_hints (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ project TEXT NOT NULL,
7
+ hint_type TEXT NOT NULL CHECK (hint_type IN ('co_edit', 'hot_file', 'read_threshold', 'session_profile')),
8
+ file_path TEXT NOT NULL DEFAULT '',
9
+ hint_value TEXT NOT NULL,
10
+ created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
11
+ updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
12
+ UNIQUE(project, hint_type, file_path)
13
+ );
14
+
15
+ CREATE INDEX IF NOT EXISTS idx_file_hints_lookup ON file_hints(project, hint_type, file_path);
@@ -113,6 +113,11 @@ if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_i
113
113
  _mod_tmp=$(mktemp "${mod_file}.XXXXXX" 2>/dev/null) || _mod_tmp="${mod_file}.$$"
114
114
  tail -3 "$mod_file" > "$_mod_tmp" && mv "$_mod_tmp" "$mod_file" || rm -f "$_mod_tmp"
115
115
  fi
116
+
117
+ # Full edit history for stuck loop detection (not truncated)
118
+ edit_dir="$EAGLE_MEM_DIR/edit-tracker"
119
+ mkdir -p "$edit_dir" 2>/dev/null
120
+ echo "$fp" >> "$edit_dir/${session_id}"
116
121
  ;;
117
122
  esac
118
123
  fi
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — PreToolUse hook
4
- # Fires before Bash and Read tool calls
4
+ # Fires before Bash, Read, Edit, Write tool calls
5
5
  # 1. Surfaces feature verification checklists before git push
6
6
  # 2. Truncates noisy commands via updatedInput (curator-learned rules)
7
7
  # 3. Detects Read-after-Edit/Write (content already in context)
8
8
  # 4. Nudges on repeated file reads (dedup tracker)
9
+ # 5. Co-edit nudge on Edit/Write (curator-learned pairs)
10
+ # 6. Stuck loop detection (repeated edits to same file)
9
11
  # ═══════════════════════════════════════════════════════════
10
12
  set +e
11
13
 
@@ -21,7 +23,7 @@ input=$(eagle_read_stdin)
21
23
  tool_name=$(echo "$input" | jq -r '.tool_name // empty')
22
24
 
23
25
  case "$tool_name" in
24
- Bash|Read) ;;
26
+ Bash|Read|Edit|Write) ;;
25
27
  *) exit 0 ;;
26
28
  esac
27
29
 
@@ -116,6 +118,42 @@ ${context}"
116
118
  fi
117
119
  ;;
118
120
 
121
+ Edit|Write)
122
+ fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
123
+ if [ -n "$fp" ]; then
124
+ # ─── Stuck loop detection ─────────────────────────
125
+ if [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
126
+ edit_tracker="$EAGLE_MEM_DIR/edit-tracker/${session_id}"
127
+ if [ -f "$edit_tracker" ]; then
128
+ edit_count=$(grep -cFx -- "$fp" "$edit_tracker" 2>/dev/null)
129
+ edit_count=${edit_count:-0}
130
+ if [ "$edit_count" -ge 8 ]; then
131
+ context+="Eagle Mem: '$(basename "$fp")' has been edited ${edit_count} times this session. You may be stuck — consider stepping back to rethink your approach before making more changes. "
132
+ elif [ "$edit_count" -ge 5 ]; then
133
+ context+="Eagle Mem: '$(basename "$fp")' has been edited ${edit_count} times this session. If the changes aren't converging, consider a different approach. "
134
+ fi
135
+ fi
136
+ fi
137
+
138
+ # ─── Co-edit nudge (skip SQLite call if no co-edit hints exist) ──
139
+ _proj_hash=$(printf '%s' "$project" | shasum | cut -c1-8)
140
+ co_edit_marker="$EAGLE_MEM_DIR/.co-edit-active.${_proj_hash}"
141
+ co_edits=""
142
+ if [ -f "$co_edit_marker" ]; then
143
+ co_edits=$(eagle_get_co_edits "$project" "$fp")
144
+ fi
145
+ if [ -n "$co_edits" ]; then
146
+ partners=""
147
+ IFS=',' read -ra co_arr <<< "$co_edits"
148
+ for co_file in "${co_arr[@]}"; do
149
+ [ -n "$co_file" ] && partners+="$(basename "$co_file"), "
150
+ done
151
+ partners=${partners%, }
152
+ context+="Eagle Mem: When you change '$(basename "$fp")' you usually also touch: $partners"
153
+ fi
154
+ fi
155
+ ;;
156
+
119
157
  Read)
120
158
  fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
121
159
  if [ -n "$fp" ] && [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
@@ -45,6 +45,7 @@ eagle_sessionstart_auto_curate "$project" "$SCRIPTS_DIR"
45
45
 
46
46
  find "$EAGLE_MEM_DIR/read-tracker" -type f -mtime +1 -delete 2>/dev/null &
47
47
  find "$EAGLE_MEM_DIR/mod-tracker" -type f -mtime +1 -delete 2>/dev/null &
48
+ find "$EAGLE_MEM_DIR/edit-tracker" -type f -mtime +1 -delete 2>/dev/null &
48
49
 
49
50
  # ─── Version check (non-blocking) ────────────────────────
50
51
 
@@ -264,6 +265,36 @@ if [ -n "$synced_tasks" ]; then
264
265
  done <<< "$synced_tasks"
265
266
  fi
266
267
 
268
+ # ─── Core files (hot file hints from curator) ───────────
269
+
270
+ hot_files=$(eagle_get_hot_files "$project")
271
+ if [ -n "$hot_files" ]; then
272
+ context+="
273
+ === Core Files (frequently read — re-read sparingly if unchanged) ===
274
+ "
275
+ IFS=',' read -ra hf_arr <<< "$hot_files"
276
+ for hf in "${hf_arr[@]}"; do
277
+ [ -n "$hf" ] && context+=" - $(basename "$hf")
278
+ "
279
+ done
280
+ fi
281
+
282
+ # ─── Working set (on compact — what you were editing) ────
283
+
284
+ if [ "$source_type" = "compact" ] || [ "$source_type" = "clear" ]; then
285
+ working_set=$(eagle_get_working_set "$session_id")
286
+ if [ -n "$working_set" ]; then
287
+ context+="
288
+ === Working Set (files you were modifying before compact) ===
289
+ "
290
+ while IFS='|' read -r ws_path ws_edits; do
291
+ [ -z "$ws_path" ] && continue
292
+ context+=" - $(basename "$ws_path") (${ws_edits} edits)
293
+ "
294
+ done <<< "$working_set"
295
+ fi
296
+ fi
297
+
267
298
  # ─── Instructions (compressed) ───────────────────────────
268
299
 
269
300
  if [ "$source_type" = "compact" ] || [ "$source_type" = "clear" ]; then
package/hooks/stop.sh CHANGED
@@ -38,15 +38,6 @@ eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcr
38
38
  # Ensure session exists (may not if SessionStart didn't fire)
39
39
  eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
40
40
 
41
- # ─── Guard: skip if summary already exists for this session ──
42
- # Stop fires every assistant turn. Only process once per session.
43
-
44
- existing_count=$(eagle_count_session_summaries "$session_id")
45
- if [ "${existing_count:-0}" -gt 0 ]; then
46
- eagle_log "INFO" "Stop: summary already exists for session=$session_id — skipping"
47
- exit 0
48
- fi
49
-
50
41
  [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ] && exit 0
51
42
 
52
43
  # ─── Primary: heuristic extraction from transcript ───────────
@@ -67,7 +58,7 @@ fi
67
58
 
68
59
  investigated=""
69
60
  learned=""
70
- completed="(auto-captured)"
61
+ completed=""
71
62
  next_steps=""
72
63
  notes=""
73
64
  decisions=""
@@ -134,8 +125,16 @@ if [ -n "$summary_block" ]; then
134
125
  fi
135
126
 
136
127
  # ─── LLM enrichment: fill in decisions/gotchas/key_files ─────
128
+ # Check both local vars (from eagle-summary block) AND existing DB enrichment.
129
+ # Skip LLM call if either source already has enrichment data.
137
130
 
131
+ existing_enrichment=""
138
132
  if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ]; then
133
+ s_esc=$(eagle_sql_escape "$session_id")
134
+ existing_enrichment=$(eagle_db "SELECT decisions||gotchas||key_files FROM summaries WHERE session_id='$s_esc';")
135
+ fi
136
+
137
+ if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ] && [ -z "$existing_enrichment" ]; then
139
138
  provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
140
139
  if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
141
140
  excerpt=$(echo "$text_content" | tail -c 2000)
package/lib/db-core.sh CHANGED
@@ -7,7 +7,6 @@ _EAGLE_DB_CORE_LOADED=1
7
7
 
8
8
  EAGLE_DB_SETUP=".headers off
9
9
  .output /dev/null
10
- PRAGMA journal_mode=WAL;
11
10
  PRAGMA synchronous=NORMAL;
12
11
  PRAGMA busy_timeout=5000;
13
12
  PRAGMA foreign_keys=ON;
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — File hint helpers (learned patterns from curator)
4
+ # ═══════════════════════════════════════════════════════════
5
+ [ -n "${_EAGLE_DB_HINTS_LOADED:-}" ] && return 0
6
+ _EAGLE_DB_HINTS_LOADED=1
7
+
8
+ eagle_upsert_file_hint() {
9
+ local project; project=$(eagle_sql_escape "$1")
10
+ local hint_type; hint_type=$(eagle_sql_escape "$2")
11
+ local file_path; file_path=$(eagle_sql_escape "$3")
12
+ local hint_value; hint_value=$(eagle_sql_escape "$4")
13
+
14
+ eagle_db "INSERT INTO file_hints (project, hint_type, file_path, hint_value)
15
+ VALUES ('$project', '$hint_type', '$file_path', '$hint_value')
16
+ ON CONFLICT(project, hint_type, file_path) DO UPDATE SET
17
+ hint_value = excluded.hint_value,
18
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
19
+ }
20
+
21
+ eagle_get_co_edits() {
22
+ local project; project=$(eagle_sql_escape "$1")
23
+ local file_path; file_path=$(eagle_sql_escape "$2")
24
+
25
+ eagle_db "SELECT hint_value FROM file_hints
26
+ WHERE project = '$project'
27
+ AND hint_type = 'co_edit'
28
+ AND file_path = '$file_path'
29
+ LIMIT 1;"
30
+ }
31
+
32
+ eagle_get_hot_files() {
33
+ local project; project=$(eagle_sql_escape "$1")
34
+
35
+ eagle_db "SELECT hint_value FROM file_hints
36
+ WHERE project = '$project'
37
+ AND hint_type = 'hot_file'
38
+ AND file_path = ''
39
+ LIMIT 1;"
40
+ }
41
+
42
+ eagle_get_working_set() {
43
+ local session_id; session_id=$(eagle_sql_escape "$1")
44
+
45
+ eagle_db "SELECT
46
+ CASE tool_name
47
+ WHEN 'Edit' THEN SUBSTR(tool_input_summary, 6)
48
+ WHEN 'Write' THEN SUBSTR(tool_input_summary, 7)
49
+ END as file_path,
50
+ COUNT(*) as edits
51
+ FROM observations
52
+ WHERE session_id = '$session_id'
53
+ AND tool_name IN ('Edit', 'Write')
54
+ AND tool_input_summary IS NOT NULL
55
+ GROUP BY file_path
56
+ ORDER BY MAX(created_at) DESC
57
+ LIMIT 10;"
58
+ }
59
+
60
+ eagle_delete_file_hints() {
61
+ local project; project=$(eagle_sql_escape "$1")
62
+ local hint_type; hint_type=$(eagle_sql_escape "$2")
63
+
64
+ eagle_db "DELETE FROM file_hints
65
+ WHERE project = '$project'
66
+ AND hint_type = '$hint_type';"
67
+ }
package/lib/db.sh CHANGED
@@ -14,4 +14,5 @@ _eagle_db_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  . "$_eagle_db_dir/db-summaries.sh"
15
15
  . "$_eagle_db_dir/db-mirrors.sh"
16
16
  . "$_eagle_db_dir/db-features.sh"
17
+ . "$_eagle_db_dir/db-hints.sh"
17
18
  . "$_eagle_db_dir/db-backfill.sh"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.0.3",
3
+ "version": "4.1.1",
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
@@ -6,6 +6,8 @@
6
6
  # 2. Superseded decision detection
7
7
  # 3. Command compression rules
8
8
  # 4. Feature auto-discovery
9
+ # 5. Co-edit pattern detection
10
+ # 6. Hot file detection
9
11
  # ═══════════════════════════════════════════════════════════
10
12
  set -euo pipefail
11
13
 
@@ -348,7 +350,167 @@ else
348
350
  eagle_dim " Not enough session data for feature discovery"
349
351
  fi
350
352
 
351
- # ─── 5. Session compression (--full only) ─────────────────
353
+ # ─── 5. Co-edit pattern detection (pure SQL) ─────────────
354
+
355
+ eagle_info "Analyzing co-edit patterns..."
356
+
357
+ # Need 3+ sessions with edits — with fewer, every pair trivially co-occurs
358
+ edit_sessions=$(eagle_db "SELECT COUNT(DISTINCT session_id) FROM observations
359
+ WHERE project = '$p_esc' AND tool_name IN ('Edit', 'Write');")
360
+
361
+ co_edit_data=""
362
+ if [ "${edit_sessions:-0}" -ge 3 ]; then
363
+ co_edit_data=$(eagle_db "WITH edits AS (
364
+ SELECT session_id,
365
+ CASE tool_name
366
+ WHEN 'Edit' THEN SUBSTR(tool_input_summary, 6)
367
+ WHEN 'Write' THEN SUBSTR(tool_input_summary, 7)
368
+ END as file_path
369
+ FROM observations
370
+ WHERE project = '$p_esc'
371
+ AND tool_name IN ('Edit', 'Write')
372
+ AND tool_input_summary IS NOT NULL
373
+ ),
374
+ file_stats AS (
375
+ SELECT file_path, COUNT(DISTINCT session_id) as edit_sessions
376
+ FROM edits GROUP BY file_path
377
+ ),
378
+ -- Filter files edited in >85% of all edit sessions (like .env — changes with everything)
379
+ noisy_files AS (
380
+ SELECT file_path FROM file_stats
381
+ WHERE CAST(edit_sessions AS REAL) / $edit_sessions > 0.85
382
+ )
383
+ SELECT e1.file_path as f1, e2.file_path as f2,
384
+ COUNT(DISTINCT e1.session_id) as co_sessions
385
+ FROM edits e1
386
+ JOIN edits e2 ON e1.session_id = e2.session_id AND e1.file_path < e2.file_path
387
+ WHERE e1.file_path NOT IN (SELECT file_path FROM noisy_files)
388
+ AND e2.file_path NOT IN (SELECT file_path FROM noisy_files)
389
+ GROUP BY f1, f2
390
+ HAVING co_sessions >= 2
391
+ ORDER BY co_sessions DESC
392
+ LIMIT 30;")
393
+ fi
394
+
395
+ if [ -n "$co_edit_data" ]; then
396
+ if [ "$DRY_RUN" -eq 1 ]; then
397
+ eagle_info " Co-edit pairs found:"
398
+ fi
399
+
400
+ co_edit_count=0
401
+ declare -A co_map
402
+
403
+ while IFS='|' read -r f1 f2 co_sessions; do
404
+ [ -z "$f1" ] || [ -z "$f2" ] && continue
405
+
406
+ # Build comma-separated partner list per file (both directions)
407
+ if [ -n "${co_map[$f1]+x}" ]; then
408
+ co_map[$f1]="${co_map[$f1]},$f2"
409
+ else
410
+ co_map[$f1]="$f2"
411
+ fi
412
+ if [ -n "${co_map[$f2]+x}" ]; then
413
+ co_map[$f2]="${co_map[$f2]},$f1"
414
+ else
415
+ co_map[$f2]="$f1"
416
+ fi
417
+ co_edit_count=$((co_edit_count + 1))
418
+ done <<< "$co_edit_data"
419
+
420
+ if [ "$DRY_RUN" -eq 1 ]; then
421
+ for f in "${!co_map[@]}"; do
422
+ eagle_info " $(basename "$f") → ${co_map[$f]}"
423
+ done
424
+ else
425
+ {
426
+ echo "BEGIN;"
427
+ echo "DELETE FROM file_hints WHERE project = '$(eagle_sql_escape "$project")' AND hint_type = 'co_edit';"
428
+ for f in "${!co_map[@]}"; do
429
+ local_f=$(eagle_sql_escape "$f")
430
+ local_v=$(eagle_sql_escape "${co_map[$f]}")
431
+ echo "INSERT INTO file_hints (project, hint_type, file_path, hint_value) VALUES ('$(eagle_sql_escape "$project")', 'co_edit', '$local_f', '$local_v') ON CONFLICT(project, hint_type, file_path) DO UPDATE SET hint_value = excluded.hint_value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
432
+ done
433
+ echo "COMMIT;"
434
+ } | eagle_db_pipe
435
+ _proj_hash=$(printf '%s' "$project" | shasum | cut -c1-8)
436
+ touch "$EAGLE_MEM_DIR/.co-edit-active.${_proj_hash}"
437
+ eagle_log "INFO" "Curator: stored ${#co_map[@]} co-edit hints from $co_edit_count pairs"
438
+ fi
439
+ eagle_ok "$co_edit_count co-edit pairs found (${#co_map[@]} files)"
440
+ else
441
+ if [ "$DRY_RUN" -eq 0 ]; then
442
+ eagle_delete_file_hints "$project" "co_edit"
443
+ _proj_hash=$(printf '%s' "$project" | shasum | cut -c1-8)
444
+ rm -f "$EAGLE_MEM_DIR/.co-edit-active.${_proj_hash}"
445
+ fi
446
+ eagle_dim " Not enough edit data for co-edit detection (need 3+ sessions, have ${edit_sessions:-0})"
447
+ fi
448
+
449
+ # ─── 6. Hot file detection (pure SQL) ────────────────────
450
+
451
+ eagle_info "Detecting hot files..."
452
+
453
+ total_sessions=$(eagle_db "SELECT COUNT(DISTINCT session_id) FROM observations
454
+ WHERE project = '$p_esc' AND tool_name = 'Read';")
455
+
456
+ hot_file_data=""
457
+ if [ "${total_sessions:-0}" -ge 3 ]; then
458
+ hot_file_data=$(eagle_db "WITH read_stats AS (
459
+ SELECT
460
+ SUBSTR(tool_input_summary, 6) as file_path,
461
+ COUNT(*) as total_reads,
462
+ COUNT(DISTINCT session_id) as sessions_read
463
+ FROM observations
464
+ WHERE project = '$p_esc'
465
+ AND tool_name = 'Read'
466
+ AND tool_input_summary IS NOT NULL
467
+ GROUP BY file_path
468
+ )
469
+ SELECT file_path, total_reads, sessions_read,
470
+ CAST(total_reads * 1.0 / sessions_read AS INTEGER) as reads_per_session
471
+ FROM read_stats
472
+ WHERE (CAST(sessions_read AS REAL) / $total_sessions > 0.5
473
+ OR total_reads * 1.0 / sessions_read >= 10)
474
+ AND total_reads >= 5
475
+ ORDER BY reads_per_session DESC
476
+ LIMIT 15;")
477
+ fi
478
+
479
+ if [ -n "$hot_file_data" ]; then
480
+ hot_files=""
481
+ hot_count=0
482
+
483
+ while IFS='|' read -r hf_path hf_reads hf_sessions hf_rps; do
484
+ [ -z "$hf_path" ] && continue
485
+ if [ -n "$hot_files" ]; then
486
+ hot_files+=","
487
+ fi
488
+ hot_files+="$hf_path"
489
+ hot_count=$((hot_count + 1))
490
+
491
+ if [ "$DRY_RUN" -eq 1 ]; then
492
+ eagle_info " $(basename "$hf_path") — ${hf_rps} reads/session, ${hf_sessions}/${total_sessions} sessions"
493
+ fi
494
+ done <<< "$hot_file_data"
495
+
496
+ if [ "$DRY_RUN" -eq 0 ] && [ -n "$hot_files" ]; then
497
+ {
498
+ echo "BEGIN;"
499
+ echo "DELETE FROM file_hints WHERE project = '$(eagle_sql_escape "$project")' AND hint_type = 'hot_file';"
500
+ echo "INSERT INTO file_hints (project, hint_type, file_path, hint_value) VALUES ('$(eagle_sql_escape "$project")', 'hot_file', '', '$(eagle_sql_escape "$hot_files")') ON CONFLICT(project, hint_type, file_path) DO UPDATE SET hint_value = excluded.hint_value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
501
+ echo "COMMIT;"
502
+ } | eagle_db_pipe
503
+ eagle_log "INFO" "Curator: stored $hot_count hot files"
504
+ fi
505
+ eagle_ok "$hot_count hot files detected"
506
+ else
507
+ if [ "$DRY_RUN" -eq 0 ]; then
508
+ eagle_delete_file_hints "$project" "hot_file"
509
+ fi
510
+ eagle_dim " Not enough session data for hot file detection (need 3+ sessions, have ${total_sessions:-0})"
511
+ fi
512
+
513
+ # ─── 7. Session compression (--full only) ─────────────────
352
514
 
353
515
  if [ "$FULL" -eq 1 ]; then
354
516
  eagle_info "Compressing old sessions..."
@@ -192,6 +192,14 @@ eagle_patch_hook "$SETTINGS" "PreToolUse" "Read" \
192
192
  "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
193
193
  "PreToolUse hook (Read)"
194
194
 
195
+ eagle_patch_hook "$SETTINGS" "PreToolUse" "Edit" \
196
+ "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
197
+ "PreToolUse hook (Edit)"
198
+
199
+ eagle_patch_hook "$SETTINGS" "PreToolUse" "Write" \
200
+ "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh" \
201
+ "PreToolUse hook (Write)"
202
+
195
203
  # ─── Install skills ────────────────────────────────────────
196
204
 
197
205
  if [ -d "$PACKAGE_DIR/skills" ]; then
package/scripts/update.sh CHANGED
@@ -77,6 +77,8 @@ if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
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
79
  eagle_patch_hook "$SETTINGS" "PreToolUse" "Read" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
80
+ eagle_patch_hook "$SETTINGS" "PreToolUse" "Edit" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
81
+ eagle_patch_hook "$SETTINGS" "PreToolUse" "Write" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"
80
82
 
81
83
  eagle_ok "Hooks registered"
82
84
  fi