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.
@@ -0,0 +1,156 @@
1
+ -- Migration 015: Data integrity fixes
2
+ -- 1. Add CHECK constraints for status fields (claude_tasks, features, command_rules)
3
+ -- 2. Add UNIQUE constraint to feature_smoke_tests to prevent duplicates
4
+ -- 3. Deduplicate any existing smoke test rows before adding constraint
5
+
6
+ -- ─── 1. CHECK constraints via table rebuild ──────────────────
7
+
8
+ -- claude_tasks: enforce status values
9
+ CREATE TABLE claude_tasks_new (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ project TEXT NOT NULL DEFAULT '',
12
+ source_session_id TEXT NOT NULL,
13
+ source_task_id TEXT NOT NULL,
14
+ file_path TEXT UNIQUE,
15
+ subject TEXT,
16
+ description TEXT,
17
+ active_form TEXT,
18
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled')),
19
+ blocks TEXT DEFAULT '[]',
20
+ blocked_by TEXT DEFAULT '[]',
21
+ content_hash TEXT,
22
+ captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
23
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
24
+ );
25
+
26
+ INSERT INTO claude_tasks_new SELECT * FROM claude_tasks;
27
+ DROP TABLE claude_tasks;
28
+ ALTER TABLE claude_tasks_new RENAME TO claude_tasks;
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_project ON claude_tasks(project);
31
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_session ON claude_tasks(source_session_id);
32
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_status ON claude_tasks(status);
33
+
34
+ -- Recreate FTS triggers (dropped with table)
35
+ CREATE TRIGGER claude_tasks_ai AFTER INSERT ON claude_tasks BEGIN
36
+ INSERT INTO claude_tasks_fts(rowid, subject, description)
37
+ VALUES (new.id, new.subject, new.description);
38
+ END;
39
+
40
+ CREATE TRIGGER claude_tasks_ad AFTER DELETE ON claude_tasks BEGIN
41
+ INSERT INTO claude_tasks_fts(claude_tasks_fts, rowid, subject, description)
42
+ VALUES ('delete', old.id, old.subject, old.description);
43
+ END;
44
+
45
+ CREATE TRIGGER claude_tasks_au AFTER UPDATE ON claude_tasks BEGIN
46
+ INSERT INTO claude_tasks_fts(claude_tasks_fts, rowid, subject, description)
47
+ VALUES ('delete', old.id, old.subject, old.description);
48
+ INSERT INTO claude_tasks_fts(rowid, subject, description)
49
+ VALUES (new.id, new.subject, new.description);
50
+ END;
51
+
52
+ -- features: enforce status values
53
+ CREATE TABLE features_new (
54
+ id INTEGER PRIMARY KEY,
55
+ project TEXT NOT NULL,
56
+ name TEXT NOT NULL,
57
+ description TEXT,
58
+ status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deprecated')),
59
+ last_verified_at TIMESTAMP,
60
+ last_verified_notes TEXT,
61
+ created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
62
+ updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
63
+ UNIQUE(project, name)
64
+ );
65
+
66
+ INSERT INTO features_new SELECT * FROM features;
67
+ DROP TABLE features;
68
+ ALTER TABLE features_new RENAME TO features;
69
+
70
+ -- Recreate FTS triggers for features (dropped with table)
71
+ CREATE TRIGGER features_ai AFTER INSERT ON features BEGIN
72
+ INSERT INTO features_fts(rowid, name, description)
73
+ VALUES (new.id, new.name, new.description);
74
+ END;
75
+
76
+ CREATE TRIGGER features_ad AFTER DELETE ON features BEGIN
77
+ INSERT INTO features_fts(features_fts, rowid, name, description)
78
+ VALUES ('delete', old.id, old.name, old.description);
79
+ END;
80
+
81
+ CREATE TRIGGER features_au AFTER UPDATE ON features BEGIN
82
+ INSERT INTO features_fts(features_fts, rowid, name, description)
83
+ VALUES ('delete', old.id, old.name, old.description);
84
+ INSERT INTO features_fts(rowid, name, description)
85
+ VALUES (new.id, new.name, new.description);
86
+ END;
87
+
88
+ -- Rebuild child tables with CASCADE FKs (they reference features.id)
89
+ -- feature_dependencies: recreate to pick up new parent table
90
+ CREATE TABLE feature_dependencies_new (
91
+ id INTEGER PRIMARY KEY,
92
+ feature_id INTEGER NOT NULL,
93
+ kind TEXT NOT NULL,
94
+ target TEXT NOT NULL,
95
+ name TEXT NOT NULL,
96
+ notes TEXT,
97
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
98
+ UNIQUE(feature_id, kind, target, name)
99
+ );
100
+ INSERT INTO feature_dependencies_new SELECT * FROM feature_dependencies;
101
+ DROP TABLE feature_dependencies;
102
+ ALTER TABLE feature_dependencies_new RENAME TO feature_dependencies;
103
+
104
+ -- feature_files: recreate to pick up new parent table
105
+ CREATE TABLE feature_files_new (
106
+ id INTEGER PRIMARY KEY,
107
+ feature_id INTEGER NOT NULL,
108
+ file_path TEXT NOT NULL,
109
+ role TEXT,
110
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
111
+ UNIQUE(feature_id, file_path)
112
+ );
113
+ INSERT INTO feature_files_new SELECT * FROM feature_files;
114
+ DROP TABLE feature_files;
115
+ ALTER TABLE feature_files_new RENAME TO feature_files;
116
+
117
+ -- ─── 2. feature_smoke_tests: deduplicate + add UNIQUE constraint ──
118
+
119
+ -- Remove duplicates, keeping the row with the lowest id per (feature_id, command)
120
+ DELETE FROM feature_smoke_tests WHERE id NOT IN (
121
+ SELECT MIN(id) FROM feature_smoke_tests GROUP BY feature_id, command
122
+ );
123
+
124
+ CREATE TABLE feature_smoke_tests_new (
125
+ id INTEGER PRIMARY KEY,
126
+ feature_id INTEGER NOT NULL,
127
+ command TEXT NOT NULL,
128
+ description TEXT,
129
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
130
+ UNIQUE(feature_id, command)
131
+ );
132
+ INSERT INTO feature_smoke_tests_new SELECT * FROM feature_smoke_tests;
133
+ DROP TABLE feature_smoke_tests;
134
+ ALTER TABLE feature_smoke_tests_new RENAME TO feature_smoke_tests;
135
+
136
+ -- ─── 3. command_rules: add CHECK on strategy ─────────────────
137
+
138
+ CREATE TABLE command_rules_new (
139
+ id INTEGER PRIMARY KEY,
140
+ project TEXT,
141
+ pattern TEXT NOT NULL,
142
+ strategy TEXT NOT NULL DEFAULT 'summary' CHECK (strategy IN ('summary', 'truncate')),
143
+ max_lines INTEGER,
144
+ reason TEXT,
145
+ times_seen INTEGER DEFAULT 0,
146
+ avg_output_bytes INTEGER DEFAULT 0,
147
+ enabled INTEGER DEFAULT 1,
148
+ created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
149
+ updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
150
+ UNIQUE(project, pattern)
151
+ );
152
+ INSERT INTO command_rules_new SELECT * FROM command_rules;
153
+ DROP TABLE command_rules;
154
+ ALTER TABLE command_rules_new RENAME TO command_rules;
155
+
156
+ CREATE INDEX IF NOT EXISTS idx_command_rules_pattern ON command_rules(pattern);
package/db/migrate.sh CHANGED
@@ -9,6 +9,9 @@ EAGLE_MEM_DIR="${EAGLE_MEM_DIR:-$HOME/.eagle-mem}"
9
9
  DB="$EAGLE_MEM_DIR/memory.db"
10
10
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
11
 
12
+ # Restrict permissions: DB and config contain session data and may contain
13
+ # residual secrets despite redaction. Owner-only access (700 dir, 600 files).
14
+ umask 077
12
15
  mkdir -p "$EAGLE_MEM_DIR"
13
16
 
14
17
  run_migration() {
@@ -2,7 +2,7 @@
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — PostToolUse hook
4
4
  # Fires after every tool use
5
- # Captures file read/write operations as lightweight observations
5
+ # Captures observations + dispatches to extracted responsibilities
6
6
  # ═══════════════════════════════════════════════════════════
7
7
  set +e
8
8
 
@@ -11,6 +11,7 @@ LIB_DIR="$SCRIPT_DIR/../lib"
11
11
 
12
12
  . "$LIB_DIR/common.sh"
13
13
  . "$LIB_DIR/db.sh"
14
+ . "$LIB_DIR/hooks-posttool.sh"
14
15
 
15
16
  input=$(eagle_read_stdin)
16
17
  [ -z "$input" ] && exit 0
@@ -31,6 +32,13 @@ esac
31
32
 
32
33
  project=$(eagle_project_from_cwd "$cwd")
33
34
 
35
+ # Ensure session row exists before inserting observations (FK constraint).
36
+ # PostToolUse can race SessionStart — the session row might not exist yet.
37
+ eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
38
+
39
+ # ─── Extract observation data from tool call ──────────────
40
+
41
+ fp=""
34
42
  files_read="[]"
35
43
  files_modified="[]"
36
44
  tool_summary=""
@@ -59,14 +67,12 @@ case "$tool_name" in
59
67
  cmd=$(echo "$cmd" | eagle_redact)
60
68
  tool_summary="Bash: $cmd"
61
69
 
62
- # Output metrics
63
70
  tool_output=$(echo "$input" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
64
71
  if [ -n "$tool_output" ]; then
65
72
  output_bytes=${#tool_output}
66
73
  output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
67
74
  fi
68
75
 
69
- # Command category extraction
70
76
  first_word=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
71
77
  case "$first_word" in
72
78
  git|gh) command_category="git" ;;
@@ -91,152 +97,17 @@ case "$tool_name" in
91
97
  ;;
92
98
  esac
93
99
 
94
- # ─── Claude memory + plan mirror ─────────────────────────
95
- # Intercept writes to Claude Code's auto-memory and plan files
96
- case "$tool_name" in
97
- Write|Edit)
98
- if [ -n "$fp" ]; then
99
- # Reject path traversal: bash case `*` matches `/`, so
100
- # patterns like projects/*/memory/*.md would match paths
101
- # containing /../ segments. Block any path with `..` first.
102
- case "$fp" in
103
- *..*) ;; # path traversal — skip
104
- "$HOME/.claude/projects"/*/memory/*.md)
105
- mem_base=$(basename "$fp")
106
- if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
107
- eagle_capture_claude_memory "$fp" "$session_id" "$project"
108
- fi
109
- ;;
110
- "$HOME/.claude/plans/"*.md)
111
- if [ -f "$fp" ]; then
112
- eagle_capture_claude_plan "$fp" "$session_id" "$project"
113
- fi
114
- ;;
115
- esac
116
- fi
117
- ;;
118
- esac
100
+ # ─── Dispatch to extracted responsibilities ───────────────
119
101
 
120
- # ─── Claude task mirror ─────────────────────────────────
121
- # Intercept TaskCreate/TaskUpdate and capture the resulting JSON files
122
- case "$tool_name" in
123
- TaskCreate|TaskUpdate)
124
- if eagle_validate_session_id "$session_id"; then
125
- task_dir="$HOME/.claude/tasks/$session_id"
126
- if [ -d "$task_dir" ]; then
127
- task_id=$(echo "$input" | jq -r '.tool_input.id // empty')
128
- if [ -z "$task_id" ]; then
129
- newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
130
- [ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
131
- elif eagle_validate_session_id "$task_id"; then
132
- task_json="$task_dir/$task_id.json"
133
- [ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
134
- fi
135
- fi
136
- fi
137
- ;;
138
- esac
102
+ eagle_posttool_mirror_writes "$tool_name" "$fp" "$session_id" "$project"
103
+ eagle_posttool_mirror_tasks "$tool_name" "$session_id" "$project" "$input"
104
+ eagle_posttool_stale_hint "$tool_name" "$fp" "$project"
105
+ eagle_posttool_decision_surface "$tool_name" "$fp" "$project"
139
106
 
140
- # ─── Stale memory hint ──────────────────────────────────
141
- # After editing a project file, FTS5-search memories for the filename.
142
- # If a memory mentions this file, remind Claude to check for staleness.
143
- case "$tool_name" in
144
- Write|Edit)
145
- if [ -n "$fp" ]; then
146
- fname=$(basename "$fp")
147
- fname_stem="${fname%.*}"
148
- case "$fp" in
149
- "$HOME/.claude/"*) ;; # skip Claude config files
150
- *)
151
- if [ ${#fname_stem} -ge 3 ]; then
152
- fts_query=$(eagle_fts_sanitize "$fname_stem")
153
- if [ -n "$fts_query" ]; then
154
- fts_esc=$(eagle_sql_escape "$fts_query")
155
- p_esc=$(eagle_sql_escape "$project")
156
- stale_hit=$(eagle_db "SELECT m.memory_name
157
- FROM claude_memories m
158
- JOIN claude_memories_fts f ON f.rowid = m.id
159
- WHERE claude_memories_fts MATCH '$fts_esc'
160
- AND m.project = '$p_esc'
161
- LIMIT 1;")
162
- if [ -n "$stale_hit" ]; then
163
- stale_msg="Eagle Mem: Memory '${stale_hit}' may reference '${fname}'. If your edit contradicts it, update the memory."
164
- jq -nc --arg ctx "$stale_msg" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
165
- fi
166
- fi
167
- fi
168
- ;;
169
- esac
170
- fi
171
- ;;
172
- esac
173
-
174
- # ─── Decision + feature surfacing on Read ──────────────────
175
- # When Claude reads a file, surface past decisions and feature pipeline context.
176
- case "$tool_name" in
177
- Read)
178
- if [ -n "$fp" ]; then
179
- fname=$(basename "$fp")
180
- fname_stem="${fname%.*}"
181
- read_context=""
182
- case "$fp" in
183
- "$HOME/.claude/"*) ;; # skip Claude config files
184
- *)
185
- p_esc=$(eagle_sql_escape "$project")
186
-
187
- # Decision history from summaries
188
- if [ ${#fname_stem} -ge 3 ]; then
189
- fts_query=$(eagle_fts_sanitize "$fname_stem")
190
- if [ -n "$fts_query" ]; then
191
- fts_esc=$(eagle_sql_escape "$fts_query")
192
- decision_hit=$(eagle_db "SELECT s.decisions
193
- FROM summaries s
194
- JOIN summaries_fts f ON f.rowid = s.id
195
- WHERE summaries_fts MATCH '$fts_esc'
196
- AND s.project = '$p_esc'
197
- AND s.decisions IS NOT NULL
198
- AND s.decisions != ''
199
- ORDER BY s.created_at DESC
200
- LIMIT 1;")
201
- if [ -n "$decision_hit" ]; then
202
- read_context+="Eagle Mem decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request. "
203
- fi
204
- fi
205
- fi
206
-
207
- # Feature pipeline context
208
- feature_hit=$(eagle_find_features_for_file "$project" "$fp")
209
- if [ -n "$feature_hit" ]; then
210
- while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
211
- [ -z "$feat_name" ] && continue
212
- read_context+="Eagle Mem: '${fname}' is part of feature '${feat_name}'"
213
- [ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
214
- read_context+="."
215
- if [ -n "$feat_verified" ]; then
216
- read_context+=" Last verified: ${feat_verified}."
217
- fi
218
- if [ -n "$feat_deps" ]; then
219
- read_context+=" Dependencies: ${feat_deps}."
220
- fi
221
- if [ -n "$feat_other_files" ]; then
222
- read_context+=" Other files in pipeline: ${feat_other_files}."
223
- fi
224
- if [ -n "$feat_smoke" ]; then
225
- read_context+=" Smoke tests: ${feat_smoke}."
226
- fi
227
- read_context+=" Changes require re-testing after deploy. "
228
- done <<< "$feature_hit"
229
- fi
230
-
231
- if [ -n "$read_context" ]; then
232
- jq -nc --arg ctx "$read_context" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
233
- fi
234
- ;;
235
- esac
236
- fi
237
- ;;
238
- esac
107
+ # ─── Record observation ──────────────────────────────────
239
108
 
240
- eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"
109
+ if ! eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"; then
110
+ eagle_log "ERROR" "PostToolUse: observation insert failed for session=$session_id tool=$tool_name"
111
+ fi
241
112
 
242
113
  exit 0
@@ -27,7 +27,6 @@ cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
27
27
  session_id=$(echo "$input" | jq -r '.session_id // empty')
28
28
  cwd=$(echo "$input" | jq -r '.cwd // empty')
29
29
  project=$(eagle_project_from_cwd "$cwd")
30
- p_esc=$(eagle_sql_escape "$project")
31
30
 
32
31
  context=""
33
32
 
@@ -35,7 +34,7 @@ context=""
35
34
 
36
35
  case "$cmd" in
37
36
  *"git push"*|*"gh pr create"*)
38
- has_features=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
37
+ has_features=$(eagle_count_active_features "$project")
39
38
  if [ "${has_features:-0}" -gt 0 ]; then
40
39
  changed_files=""
41
40
  if [ -n "$cwd" ] && [ -d "$cwd" ]; then
@@ -50,17 +49,7 @@ case "$cmd" in
50
49
  fname=$(basename "$changed_file")
51
50
  fname_esc=$(eagle_sql_escape "$fname")
52
51
 
53
- feature_hits=$(eagle_db "SELECT DISTINCT f.name,
54
- (SELECT GROUP_CONCAT(fst.command, '; ')
55
- FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke,
56
- (SELECT GROUP_CONCAT(fd.target || ':' || fd.name, ', ')
57
- FROM feature_dependencies fd WHERE fd.feature_id = f.id) as deps,
58
- f.last_verified_at
59
- FROM features f
60
- JOIN feature_files ff ON ff.feature_id = f.id
61
- WHERE f.project = '$p_esc'
62
- AND f.status = 'active'
63
- AND (ff.file_path LIKE '%$fname_esc' OR ff.file_path LIKE '%$fname_esc%');")
52
+ feature_hits=$(eagle_find_feature_for_push "$project" "$fname_esc")
64
53
 
65
54
  while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
66
55
  [ -z "$feat_name" ] && continue
@@ -92,16 +81,8 @@ esac
92
81
 
93
82
  # Extract the base command for rule matching
94
83
  base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
95
- cmd_esc=$(eagle_sql_escape "$base_cmd")
96
-
97
- rule=$(eagle_db "SELECT strategy, max_lines, reason
98
- FROM command_rules
99
- WHERE enabled = 1
100
- AND (project = '$p_esc' OR project IS NULL)
101
- AND ('$cmd_esc' LIKE pattern OR '$cmd_esc' = pattern)
102
- ORDER BY
103
- CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END
104
- LIMIT 1;")
84
+
85
+ rule=$(eagle_get_command_rule "$project" "$base_cmd")
105
86
 
106
87
  if [ -n "$rule" ]; then
107
88
  IFS='|' read -r strategy max_lines reason <<< "$rule"
@@ -25,7 +25,7 @@ project=$(eagle_project_from_cwd "$cwd")
25
25
  # Final sweep: re-capture all task files to catch status changes
26
26
  # Claude Code may update task status without triggering PostToolUse
27
27
  if eagle_validate_session_id "$session_id"; then
28
- task_dir="$HOME/.claude/tasks/$session_id"
28
+ task_dir="$EAGLE_CLAUDE_TASKS_DIR/$session_id"
29
29
  if [ -d "$task_dir" ]; then
30
30
  for task_file in "$task_dir"/*.json; do
31
31
  [ ! -f "$task_file" ] && continue
@@ -38,4 +38,7 @@ fi
38
38
  eagle_end_session "$session_id"
39
39
  eagle_log "INFO" "SessionEnd: session=$session_id marked completed"
40
40
 
41
+ # Prune observations older than 90 days (keeps DB size bounded)
42
+ eagle_prune_observations 90 "$project"
43
+
41
44
  exit 0
@@ -33,10 +33,7 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
33
33
  # ─── Sweep stuck sessions (no activity for 7 days) ─────────
34
34
  # Uses last_activity_at (updated by trigger on every observation insert)
35
35
  # so long-lived sessions with regular compactions aren't falsely abandoned
36
- eagle_db "UPDATE sessions SET status = 'abandoned'
37
- WHERE status = 'active'
38
- AND id != '$(eagle_sql_escape "$session_id")'
39
- AND COALESCE(last_activity_at, started_at) < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days');"
36
+ eagle_abandon_stale_sessions "$session_id"
40
37
 
41
38
  # ─── Version check (non-blocking) ────────────────────────────
42
39
 
@@ -68,30 +65,27 @@ fi
68
65
 
69
66
  # ─── Gather stats ───────────────────────────────────────────
70
67
 
71
- p_esc=$(eagle_sql_escape "$project")
72
-
73
- stat_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$p_esc';")
74
- stat_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc';")
75
- stat_memories=$(eagle_db "SELECT COUNT(*) FROM claude_memories WHERE project = '$p_esc';")
76
- stat_tasks_pending=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'pending';")
77
- stat_tasks_progress=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'in_progress';")
78
- stat_tasks_done=$(eagle_db "SELECT COUNT(*) FROM claude_tasks WHERE project = '$p_esc' AND status = 'completed';")
79
- stat_chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$p_esc';")
80
- stat_observations=$(eagle_db "SELECT COUNT(*) FROM observations WHERE session_id IN (SELECT id FROM sessions WHERE project = '$p_esc');")
81
- stat_plans=$(eagle_db "SELECT COUNT(*) FROM claude_plans WHERE project = '$p_esc';")
82
- stat_last_active=$(eagle_db "SELECT COALESCE(MAX(date(COALESCE(last_activity_at, started_at))), 'never') FROM sessions WHERE project = '$p_esc';")
83
- stat_last_summary=$(eagle_db "SELECT request FROM summaries WHERE project = '$p_esc' ORDER BY created_at DESC LIMIT 1;")
84
-
85
- # Trim to defaults
86
- stat_sessions="${stat_sessions:-0}"
87
- stat_summaries="${stat_summaries:-0}"
88
- stat_memories="${stat_memories:-0}"
89
- stat_tasks_pending="${stat_tasks_pending:-0}"
90
- stat_tasks_progress="${stat_tasks_progress:-0}"
91
- stat_tasks_done="${stat_tasks_done:-0}"
92
- stat_chunks="${stat_chunks:-0}"
93
- stat_observations="${stat_observations:-0}"
94
- stat_plans="${stat_plans:-0}"
68
+ stat_sessions=0; stat_summaries=0; stat_with_summaries=0; stat_memories=0
69
+ stat_tasks_pending=0; stat_tasks_progress=0; stat_tasks_done=0
70
+ stat_chunks=0; stat_observations=0; stat_plans=0
71
+ stat_last_active="never"; stat_last_summary=""
72
+
73
+ while IFS='|' read -r key val; do
74
+ case "$key" in
75
+ sessions) stat_sessions="$val" ;;
76
+ summaries) stat_summaries="$val" ;;
77
+ with_summaries) stat_with_summaries="$val" ;;
78
+ memories) stat_memories="$val" ;;
79
+ plans) stat_plans="$val" ;;
80
+ tasks_pending) stat_tasks_pending="$val" ;;
81
+ tasks_progress) stat_tasks_progress="$val" ;;
82
+ tasks_done) stat_tasks_done="$val" ;;
83
+ chunks) stat_chunks="$val" ;;
84
+ observations) stat_observations="$val" ;;
85
+ last_active) stat_last_active="$val" ;;
86
+ last_summary) stat_last_summary="$val" ;;
87
+ esac
88
+ done <<< "$(eagle_get_project_stats "$project")"
95
89
 
96
90
  # Build task summary line
97
91
  task_parts=""
@@ -117,7 +111,7 @@ eagle_banner="======================================
117
111
  Eagle Mem Loaded
118
112
  ======================================
119
113
  Project | $project
120
- Sessions | $stat_sessions total ($stat_summaries with summaries)
114
+ Sessions | $stat_sessions total ($stat_with_summaries with summaries)
121
115
  Memories | $stat_memories stored
122
116
  Plans | $stat_plans saved
123
117
  Tasks | $task_parts
package/hooks/stop.sh CHANGED
@@ -116,7 +116,7 @@ fi
116
116
  if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
117
117
  # Skip heuristic if we already have a summary for this session.
118
118
  # Stop fires every turn -- without this guard, each turn creates a duplicate row.
119
- existing_count=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE session_id = '$(eagle_sql_escape "$session_id")';")
119
+ existing_count=$(eagle_count_session_summaries "$session_id")
120
120
  if [ "${existing_count:-0}" -gt 0 ]; then
121
121
  eagle_log "INFO" "Stop: skipping heuristic — summary already exists for session=$session_id (count=$existing_count)"
122
122
  else
@@ -154,8 +154,11 @@ key_files=$(echo "$key_files" | eagle_redact)
154
154
  # ─── Write to database ─────────────────────────────────────
155
155
 
156
156
  if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
157
- eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"
158
- eagle_log "INFO" "Stop: summary saved for session=$session_id"
157
+ if eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"; then
158
+ eagle_log "INFO" "Stop: summary saved for session=$session_id"
159
+ else
160
+ eagle_log "ERROR" "Stop: summary insert FAILED for session=$session_id — check DB constraints"
161
+ fi
159
162
  fi
160
163
 
161
164
  exit 0
package/lib/common.sh CHANGED
@@ -9,10 +9,17 @@ EAGLE_MEM_DB="$EAGLE_MEM_DIR/memory.db"
9
9
  EAGLE_MEM_LOG="$EAGLE_MEM_DIR/eagle-mem.log"
10
10
  EAGLE_SETTINGS="${EAGLE_SETTINGS:-$HOME/.claude/settings.json}"
11
11
  EAGLE_SKILLS_DIR="$HOME/.claude/skills"
12
+ EAGLE_CLAUDE_PROJECTS_DIR="$HOME/.claude/projects"
13
+ EAGLE_CLAUDE_PLANS_DIR="$HOME/.claude/plans"
14
+ EAGLE_CLAUDE_TASKS_DIR="$HOME/.claude/tasks"
12
15
 
13
16
  eagle_log() {
14
17
  local level="$1"
15
18
  shift
19
+ # Ensure log file is owner-only (may contain debug data)
20
+ if [ ! -f "$EAGLE_MEM_LOG" ]; then
21
+ touch "$EAGLE_MEM_LOG" 2>/dev/null && chmod 600 "$EAGLE_MEM_LOG" 2>/dev/null
22
+ fi
16
23
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
17
24
  }
18
25
 
@@ -42,6 +49,12 @@ eagle_fts_sanitize() {
42
49
  printf '%s' "$1" | sed 's/[*"(){}^~:]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
43
50
  }
44
51
 
52
+ # Escape SQL LIKE wildcards (% and _) so literal filenames match exactly.
53
+ # Apply AFTER eagle_sql_escape, since this only handles LIKE metacharacters.
54
+ eagle_like_escape() {
55
+ printf '%s' "$1" | sed 's/%/\\%/g; s/_/\\_/g'
56
+ }
57
+
45
58
  # Validate a session ID is safe for use in file paths (no traversal).
46
59
  # Claude Code session IDs are UUIDs or hex strings — reject anything else.
47
60
  eagle_validate_session_id() {
@@ -64,21 +77,34 @@ eagle_read_stdin() {
64
77
  # Stripe/AWS/GitHub/Anthropic/OpenAI key patterns, named env vars.
65
78
  eagle_redact() {
66
79
  sed -E \
67
- -e 's/(Bearer )[^ ]*/\1[REDACTED]/gi' \
68
- -e 's/(api[_-]?key[= :])[^ ]*/\1[REDACTED]/gi' \
69
- -e 's/(password[= :])[^ ]*/\1[REDACTED]/gi' \
70
- -e 's/(secret[= :])[^ ]*/\1[REDACTED]/gi' \
71
- -e 's/(token[= :])[^ ]*/\1[REDACTED]/gi' \
72
- -e 's/(Authorization: )[^ ]*/\1[REDACTED]/gi' \
80
+ -e 's/(Bearer )[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
81
+ -e 's/(api[_-]?key[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
82
+ -e 's/(password[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
83
+ -e 's/(secret[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
84
+ -e 's/(token[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
85
+ -e 's/(Authorization: )[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
86
+ -e 's/(client_secret[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
87
+ -e 's/(private_key[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
88
+ -e 's/(access_token[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
73
89
  -e 's/sk_live_[A-Za-z0-9]+/[REDACTED]/g' \
74
90
  -e 's/sk_test_[A-Za-z0-9]+/[REDACTED]/g' \
91
+ -e 's/whsec_[A-Za-z0-9]+/[REDACTED]/g' \
75
92
  -e 's/AKIA[A-Z0-9]{16}/[REDACTED]/g' \
76
93
  -e 's/ghp_[A-Za-z0-9]{36}/[REDACTED]/g' \
77
94
  -e 's/gho_[A-Za-z0-9]{36}/[REDACTED]/g' \
95
+ -e 's/glpat-[A-Za-z0-9_-]{20,}/[REDACTED]/g' \
78
96
  -e 's/sk-ant-[A-Za-z0-9_-]+/[REDACTED]/g' \
79
97
  -e 's/sk-[A-Za-z0-9]{20,}/[REDACTED]/g' \
80
- -e 's/(ANTHROPIC_API_KEY[= :])[^ ]*/\1[REDACTED]/g' \
81
- -e 's/(OPENAI_API_KEY[= :])[^ ]*/\1[REDACTED]/g'
98
+ -e 's/AIza[0-9A-Za-z_-]{35}/[REDACTED]/g' \
99
+ -e 's/xox[abps]-[A-Za-z0-9-]+/[REDACTED]/g' \
100
+ -e 's/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/[REDACTED_JWT]/g' \
101
+ -e 's|(https?://[^/:]+:)[^@]+(@)|\1[REDACTED]\2|g' \
102
+ -e 's/-----BEGIN [A-Z ]*PRIVATE KEY-----/[REDACTED_PRIVATE_KEY]/g' \
103
+ -e 's/(ANTHROPIC_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
104
+ -e 's/(OPENAI_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
105
+ -e 's/(GOOGLE_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
106
+ -e 's/(SLACK_TOKEN[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
107
+ -e 's/(DATABASE_URL[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g'
82
108
  }
83
109
 
84
110
  # Collect project files into a destination file.