eagle-mem 2.0.7 → 3.0.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/bin/eagle-mem CHANGED
@@ -27,6 +27,9 @@ case "$command" in
27
27
  refresh) bash "$SCRIPTS_DIR/refresh.sh" "$@" ;;
28
28
  prune) bash "$SCRIPTS_DIR/prune.sh" "$@" ;;
29
29
  memories) bash "$SCRIPTS_DIR/memories.sh" "$@" ;;
30
+ config) bash "$SCRIPTS_DIR/config.sh" "$@" ;;
31
+ curate) bash "$SCRIPTS_DIR/curate.sh" "$@" ;;
32
+ feature) bash "$SCRIPTS_DIR/feature.sh" "$@" ;;
30
33
  help|--help|-h)
31
34
  bash "$SCRIPTS_DIR/help.sh" ;;
32
35
  version|--version|-v|-V)
@@ -0,0 +1,69 @@
1
+ -- Migration 013: Feature graph for deployment regression prevention
2
+ -- Features are persistent entities with lifecycle, dependencies, files, and smoke tests.
3
+ -- Auto-discovered by curator from accumulated session data (tasks, key_files, decisions).
4
+
5
+ CREATE TABLE IF NOT EXISTS features (
6
+ id INTEGER PRIMARY KEY,
7
+ project TEXT NOT NULL,
8
+ name TEXT NOT NULL,
9
+ description TEXT,
10
+ status TEXT DEFAULT 'active',
11
+ last_verified_at TIMESTAMP,
12
+ last_verified_notes TEXT,
13
+ created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
14
+ updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
15
+ UNIQUE(project, name)
16
+ );
17
+
18
+ CREATE TABLE IF NOT EXISTS feature_dependencies (
19
+ id INTEGER PRIMARY KEY,
20
+ feature_id INTEGER NOT NULL,
21
+ kind TEXT NOT NULL,
22
+ target TEXT NOT NULL,
23
+ name TEXT NOT NULL,
24
+ notes TEXT,
25
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
26
+ UNIQUE(feature_id, kind, target, name)
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS feature_files (
30
+ id INTEGER PRIMARY KEY,
31
+ feature_id INTEGER NOT NULL,
32
+ file_path TEXT NOT NULL,
33
+ role TEXT,
34
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
35
+ UNIQUE(feature_id, file_path)
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS feature_smoke_tests (
39
+ id INTEGER PRIMARY KEY,
40
+ feature_id INTEGER NOT NULL,
41
+ command TEXT NOT NULL,
42
+ description TEXT,
43
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE
44
+ );
45
+
46
+ -- FTS5 for feature search
47
+ CREATE VIRTUAL TABLE IF NOT EXISTS features_fts USING fts5(
48
+ name,
49
+ description,
50
+ content='features',
51
+ content_rowid='id'
52
+ );
53
+
54
+ CREATE TRIGGER IF NOT EXISTS features_ai AFTER INSERT ON features BEGIN
55
+ INSERT INTO features_fts(rowid, name, description)
56
+ VALUES (new.id, new.name, new.description);
57
+ END;
58
+
59
+ CREATE TRIGGER IF NOT EXISTS features_ad AFTER DELETE ON features BEGIN
60
+ INSERT INTO features_fts(features_fts, rowid, name, description)
61
+ VALUES ('delete', old.id, old.name, old.description);
62
+ END;
63
+
64
+ CREATE TRIGGER IF NOT EXISTS features_au AFTER UPDATE ON features BEGIN
65
+ INSERT INTO features_fts(features_fts, rowid, name, description)
66
+ VALUES ('delete', old.id, old.name, old.description);
67
+ INSERT INTO features_fts(rowid, name, description)
68
+ VALUES (new.id, new.name, new.description);
69
+ END;
@@ -0,0 +1,25 @@
1
+ -- Migration 014: Command intelligence — output size tracking + command rules
2
+ -- Adds output metrics to observations for adaptive command filtering.
3
+
4
+ ALTER TABLE observations ADD COLUMN output_bytes INTEGER;
5
+ ALTER TABLE observations ADD COLUMN output_lines INTEGER;
6
+ ALTER TABLE observations ADD COLUMN command_category TEXT;
7
+
8
+ -- Command rules table — populated by curator, consumed by PreToolUse hook
9
+ CREATE TABLE IF NOT EXISTS command_rules (
10
+ id INTEGER PRIMARY KEY,
11
+ project TEXT,
12
+ pattern TEXT NOT NULL,
13
+ strategy TEXT NOT NULL DEFAULT 'summary',
14
+ max_lines INTEGER,
15
+ reason TEXT,
16
+ times_seen INTEGER DEFAULT 0,
17
+ avg_output_bytes INTEGER DEFAULT 0,
18
+ enabled INTEGER DEFAULT 1,
19
+ created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20
+ updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
21
+ UNIQUE(project, pattern)
22
+ );
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_command_rules_pattern ON command_rules(pattern);
25
+ CREATE INDEX IF NOT EXISTS idx_observations_category ON observations(command_category);
@@ -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() {
@@ -31,9 +31,16 @@ esac
31
31
 
32
32
  project=$(eagle_project_from_cwd "$cwd")
33
33
 
34
+ # Ensure session row exists before inserting observations (FK constraint).
35
+ # PostToolUse can race SessionStart — the session row might not exist yet.
36
+ eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
37
+
34
38
  files_read="[]"
35
39
  files_modified="[]"
36
40
  tool_summary=""
41
+ output_bytes=""
42
+ output_lines=""
43
+ command_category=""
37
44
 
38
45
  case "$tool_name" in
39
46
  Read)
@@ -53,24 +60,34 @@ case "$tool_name" in
53
60
  ;;
54
61
  Bash)
55
62
  cmd=$(echo "$input" | jq -r '.tool_input.command // empty' | cut -c1-200)
56
- # Redact common secret patterns before storing
57
- cmd=$(echo "$cmd" | sed -E \
58
- -e 's/(Bearer )[^ ]*/\1[REDACTED]/gi' \
59
- -e 's/(api[_-]?key[= :])[^ ]*/\1[REDACTED]/gi' \
60
- -e 's/(password[= :])[^ ]*/\1[REDACTED]/gi' \
61
- -e 's/(secret[= :])[^ ]*/\1[REDACTED]/gi' \
62
- -e 's/(token[= :])[^ ]*/\1[REDACTED]/gi' \
63
- -e 's/(Authorization: )[^ ]*/\1[REDACTED]/gi' \
64
- -e 's/sk_live_[A-Za-z0-9]+/[REDACTED]/g' \
65
- -e 's/sk_test_[A-Za-z0-9]+/[REDACTED]/g' \
66
- -e 's/AKIA[A-Z0-9]{16}/[REDACTED]/g' \
67
- -e 's/ghp_[A-Za-z0-9]{36}/[REDACTED]/g' \
68
- -e 's/gho_[A-Za-z0-9]{36}/[REDACTED]/g' \
69
- -e 's/sk-ant-[A-Za-z0-9_-]+/[REDACTED]/g' \
70
- -e 's/sk-[A-Za-z0-9]{20,}/[REDACTED]/g' \
71
- -e 's/(ANTHROPIC_API_KEY[= :])[^ ]*/\1[REDACTED]/g' \
72
- -e 's/(OPENAI_API_KEY[= :])[^ ]*/\1[REDACTED]/g')
63
+ cmd=$(echo "$cmd" | eagle_redact)
73
64
  tool_summary="Bash: $cmd"
65
+
66
+ # Output metrics
67
+ tool_output=$(echo "$input" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
68
+ if [ -n "$tool_output" ]; then
69
+ output_bytes=${#tool_output}
70
+ output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
71
+ fi
72
+
73
+ # Command category extraction
74
+ first_word=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
75
+ case "$first_word" in
76
+ git|gh) command_category="git" ;;
77
+ npm|npx|pnpm|yarn|bun) command_category="js" ;;
78
+ pip|pip3|python|python3|uv) command_category="python" ;;
79
+ cargo|rustc) command_category="rust" ;;
80
+ go) command_category="go" ;;
81
+ docker|docker-compose|podman) command_category="docker" ;;
82
+ kubectl|helm|k9s) command_category="k8s" ;;
83
+ aws|gcloud|az) command_category="cloud" ;;
84
+ make|cmake|ninja) command_category="build" ;;
85
+ grep|find|ls|cat|head|tail|wc|sort|sed|awk) command_category="files" ;;
86
+ curl|wget|http) command_category="http" ;;
87
+ *test*|jest|pytest|vitest|mocha) command_category="test" ;;
88
+ *lint*|eslint|ruff|golangci-lint) command_category="lint" ;;
89
+ *) command_category="other" ;;
90
+ esac
74
91
  ;;
75
92
  TaskCreate|TaskUpdate)
76
93
  task_subject=$(echo "$input" | jq -r '.tool_input.subject // empty')
@@ -158,6 +175,74 @@ case "$tool_name" in
158
175
  ;;
159
176
  esac
160
177
 
161
- eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified"
178
+ # ─── Decision + feature surfacing on Read ──────────────────
179
+ # When Claude reads a file, surface past decisions and feature pipeline context.
180
+ case "$tool_name" in
181
+ Read)
182
+ if [ -n "$fp" ]; then
183
+ fname=$(basename "$fp")
184
+ fname_stem="${fname%.*}"
185
+ read_context=""
186
+ case "$fp" in
187
+ "$HOME/.claude/"*) ;; # skip Claude config files
188
+ *)
189
+ p_esc=$(eagle_sql_escape "$project")
190
+
191
+ # Decision history from summaries
192
+ if [ ${#fname_stem} -ge 3 ]; then
193
+ fts_query=$(eagle_fts_sanitize "$fname_stem")
194
+ if [ -n "$fts_query" ]; then
195
+ fts_esc=$(eagle_sql_escape "$fts_query")
196
+ decision_hit=$(eagle_db "SELECT s.decisions
197
+ FROM summaries s
198
+ JOIN summaries_fts f ON f.rowid = s.id
199
+ WHERE summaries_fts MATCH '$fts_esc'
200
+ AND s.project = '$p_esc'
201
+ AND s.decisions IS NOT NULL
202
+ AND s.decisions != ''
203
+ ORDER BY s.created_at DESC
204
+ LIMIT 1;")
205
+ if [ -n "$decision_hit" ]; then
206
+ read_context+="Eagle Mem decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request. "
207
+ fi
208
+ fi
209
+ fi
210
+
211
+ # Feature pipeline context
212
+ feature_hit=$(eagle_find_features_for_file "$project" "$fp")
213
+ if [ -n "$feature_hit" ]; then
214
+ while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
215
+ [ -z "$feat_name" ] && continue
216
+ read_context+="Eagle Mem: '${fname}' is part of feature '${feat_name}'"
217
+ [ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
218
+ read_context+="."
219
+ if [ -n "$feat_verified" ]; then
220
+ read_context+=" Last verified: ${feat_verified}."
221
+ fi
222
+ if [ -n "$feat_deps" ]; then
223
+ read_context+=" Dependencies: ${feat_deps}."
224
+ fi
225
+ if [ -n "$feat_other_files" ]; then
226
+ read_context+=" Other files in pipeline: ${feat_other_files}."
227
+ fi
228
+ if [ -n "$feat_smoke" ]; then
229
+ read_context+=" Smoke tests: ${feat_smoke}."
230
+ fi
231
+ read_context+=" Changes require re-testing after deploy. "
232
+ done <<< "$feature_hit"
233
+ fi
234
+
235
+ if [ -n "$read_context" ]; then
236
+ jq -nc --arg ctx "$read_context" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
237
+ fi
238
+ ;;
239
+ esac
240
+ fi
241
+ ;;
242
+ esac
243
+
244
+ if ! eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"; then
245
+ eagle_log "ERROR" "PostToolUse: observation insert failed for session=$session_id tool=$tool_name"
246
+ fi
162
247
 
163
248
  exit 0
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — PreToolUse hook
4
+ # Fires before every Bash tool use
5
+ # 1. Surfaces feature verification checklists before git push
6
+ # 2. Applies learned command filtering rules (RTK-style adaptive)
7
+ # ═══════════════════════════════════════════════════════════
8
+ set +e
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
+ LIB_DIR="$SCRIPT_DIR/../lib"
12
+
13
+ . "$LIB_DIR/common.sh"
14
+ . "$LIB_DIR/db.sh"
15
+
16
+ input=$(eagle_read_stdin)
17
+ [ -z "$input" ] && exit 0
18
+
19
+ tool_name=$(echo "$input" | jq -r '.tool_name // empty')
20
+ [ "$tool_name" != "Bash" ] && exit 0
21
+
22
+ [ ! -f "$EAGLE_MEM_DB" ] && exit 0
23
+
24
+ cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
25
+ [ -z "$cmd" ] && exit 0
26
+
27
+ session_id=$(echo "$input" | jq -r '.session_id // empty')
28
+ cwd=$(echo "$input" | jq -r '.cwd // empty')
29
+ project=$(eagle_project_from_cwd "$cwd")
30
+ p_esc=$(eagle_sql_escape "$project")
31
+
32
+ context=""
33
+
34
+ # ─── Feature verification on git push ─────────────────────
35
+
36
+ case "$cmd" in
37
+ *"git push"*|*"gh pr create"*)
38
+ has_features=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
39
+ if [ "${has_features:-0}" -gt 0 ]; then
40
+ changed_files=""
41
+ if [ -n "$cwd" ] && [ -d "$cwd" ]; then
42
+ changed_files=$(git -C "$cwd" diff --name-only HEAD 2>/dev/null)
43
+ [ -z "$changed_files" ] && changed_files=$(git -C "$cwd" diff --cached --name-only 2>/dev/null)
44
+ fi
45
+
46
+ if [ -n "$changed_files" ]; then
47
+ seen_features=""
48
+ while IFS= read -r changed_file; do
49
+ [ -z "$changed_file" ] && continue
50
+ fname=$(basename "$changed_file")
51
+ fname_esc=$(eagle_sql_escape "$fname")
52
+ fname_like=$(eagle_like_escape "$fname_esc")
53
+
54
+ feature_hits=$(eagle_db "SELECT DISTINCT f.name,
55
+ (SELECT GROUP_CONCAT(fst.command, '; ')
56
+ FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke,
57
+ (SELECT GROUP_CONCAT(fd.target || ':' || fd.name, ', ')
58
+ FROM feature_dependencies fd WHERE fd.feature_id = f.id) as deps,
59
+ f.last_verified_at
60
+ FROM features f
61
+ JOIN feature_files ff ON ff.feature_id = f.id
62
+ WHERE f.project = '$p_esc'
63
+ AND f.status = 'active'
64
+ AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\');")
65
+
66
+ while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
67
+ [ -z "$feat_name" ] && continue
68
+ case "$seen_features" in *"|$feat_name|"*) continue ;; esac
69
+ seen_features+="|$feat_name|"
70
+
71
+ context+=" - $feat_name"
72
+ [ -n "$feat_smoke" ] && context+=" | smoke: $feat_smoke"
73
+ [ -n "$feat_deps" ] && context+=" | deps: $feat_deps"
74
+ if [ -n "$feat_verified" ]; then
75
+ context+=" | last verified: $feat_verified"
76
+ else
77
+ context+=" | never verified"
78
+ fi
79
+ context+=$'\n'
80
+ done <<< "$feature_hits"
81
+ done <<< "$changed_files"
82
+
83
+ if [ -n "$context" ]; then
84
+ context="Eagle Mem: This push affects the following features. After deploy, verify each works and run 'eagle-mem feature verify <name>'.
85
+ ${context}"
86
+ fi
87
+ fi
88
+ fi
89
+ ;;
90
+ esac
91
+
92
+ # ─── Command output filtering (learned rules) ─────────────
93
+
94
+ # Extract the base command for rule matching
95
+ base_cmd=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
96
+ cmd_esc=$(eagle_sql_escape "$base_cmd")
97
+
98
+ rule=$(eagle_db "SELECT strategy, max_lines, reason
99
+ FROM command_rules
100
+ WHERE enabled = 1
101
+ AND (project = '$p_esc' OR project IS NULL)
102
+ AND ('$cmd_esc' LIKE pattern OR '$cmd_esc' = pattern)
103
+ ORDER BY
104
+ CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END
105
+ LIMIT 1;")
106
+
107
+ if [ -n "$rule" ]; then
108
+ IFS='|' read -r strategy max_lines reason <<< "$rule"
109
+ case "$strategy" in
110
+ summary)
111
+ context+="Eagle Mem command hint: '${base_cmd}' output is typically noisy (${reason}). Consider piping through 'tail -5' or checking exit code only."
112
+ ;;
113
+ truncate)
114
+ if [ -n "$max_lines" ] && [ "$max_lines" -gt 0 ] 2>/dev/null; then
115
+ context+="Eagle Mem command hint: '${base_cmd}' produces long output (${reason}). Consider: ${cmd} | head -${max_lines}"
116
+ fi
117
+ ;;
118
+ esac
119
+ fi
120
+
121
+ [ -z "$context" ] && exit 0
122
+
123
+ jq -nc --arg ctx "$context" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":$ctx}}'
124
+
125
+ exit 0
@@ -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
@@ -268,6 +268,10 @@ $eagle_banner
268
268
 
269
269
  This gives the user visibility into the full context Eagle Mem loaded for this session.
270
270
 
271
+ ANTI-REGRESSION: When Eagle Mem surfaces decision history about a file you are reading (via PostToolUse context), those decisions were made deliberately in past sessions. Do NOT revert or change the implementation approach without explicit user request. If you believe a past decision should change, state why and ask the user before proceeding. This prevents the common regression where Claude 'improves' code back to an older approach that was already rejected.
272
+
273
+ SECRET SAFETY: Never include raw API keys, tokens, passwords, or secrets in eagle-summary fields or any text that Eagle Mem stores. Reference secrets by name (e.g., 'the Stripe API key', 'GOOGLE_APPLICATION_CREDENTIALS_JSON') not by value. Eagle Mem redacts common patterns automatically, but prevention is better than redaction.
274
+
271
275
  MEMORY FRESHNESS: The memories above include age indicators. If you make a change (edit a file, update a config, change a pattern) that contradicts what a loaded memory says, you MUST update that memory file immediately. Read the memory file, edit it to reflect the new reality, and the PostToolUse hook will sync the update to Eagle Mem. Stale memories mislead future sessions — keeping them current is as important as writing good code.
272
276
 
273
277
  === EAGLE MEM — SESSION SUMMARY (MANDATORY) ===
package/hooks/stop.sh CHANGED
@@ -140,11 +140,25 @@ if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ];
140
140
  fi
141
141
  fi
142
142
 
143
+ # ─── Redact secrets from all text fields before storage ────
144
+
145
+ request=$(echo "$request" | eagle_redact)
146
+ investigated=$(echo "$investigated" | eagle_redact)
147
+ learned=$(echo "$learned" | eagle_redact)
148
+ completed=$(echo "$completed" | eagle_redact)
149
+ next_steps=$(echo "$next_steps" | eagle_redact)
150
+ decisions=$(echo "$decisions" | eagle_redact)
151
+ gotchas=$(echo "$gotchas" | eagle_redact)
152
+ key_files=$(echo "$key_files" | eagle_redact)
153
+
143
154
  # ─── Write to database ─────────────────────────────────────
144
155
 
145
156
  if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
146
- eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"
147
- 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
148
162
  fi
149
163
 
150
164
  exit 0
package/lib/common.sh CHANGED
@@ -13,6 +13,10 @@ EAGLE_SKILLS_DIR="$HOME/.claude/skills"
13
13
  eagle_log() {
14
14
  local level="$1"
15
15
  shift
16
+ # Ensure log file is owner-only (may contain debug data)
17
+ if [ ! -f "$EAGLE_MEM_LOG" ]; then
18
+ touch "$EAGLE_MEM_LOG" 2>/dev/null && chmod 600 "$EAGLE_MEM_LOG" 2>/dev/null
19
+ fi
16
20
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
17
21
  }
18
22
 
@@ -42,6 +46,12 @@ eagle_fts_sanitize() {
42
46
  printf '%s' "$1" | sed 's/[*"(){}^~:]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
43
47
  }
44
48
 
49
+ # Escape SQL LIKE wildcards (% and _) so literal filenames match exactly.
50
+ # Apply AFTER eagle_sql_escape, since this only handles LIKE metacharacters.
51
+ eagle_like_escape() {
52
+ printf '%s' "$1" | sed 's/%/\\%/g; s/_/\\_/g'
53
+ }
54
+
45
55
  # Validate a session ID is safe for use in file paths (no traversal).
46
56
  # Claude Code session IDs are UUIDs or hex strings — reject anything else.
47
57
  eagle_validate_session_id() {
@@ -59,6 +69,41 @@ eagle_read_stdin() {
59
69
  echo "$input"
60
70
  }
61
71
 
72
+ # Redact secrets from text before storage.
73
+ # Covers: Bearer tokens, API keys, passwords, secrets, tokens,
74
+ # Stripe/AWS/GitHub/Anthropic/OpenAI key patterns, named env vars.
75
+ eagle_redact() {
76
+ sed -E \
77
+ -e 's/(Bearer )[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
78
+ -e 's/(api[_-]?key[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
79
+ -e 's/(password[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
80
+ -e 's/(secret[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
81
+ -e 's/(token[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
82
+ -e 's/(Authorization: )[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
83
+ -e 's/(client_secret[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
84
+ -e 's/(private_key[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
85
+ -e 's/(access_token[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
86
+ -e 's/sk_live_[A-Za-z0-9]+/[REDACTED]/g' \
87
+ -e 's/sk_test_[A-Za-z0-9]+/[REDACTED]/g' \
88
+ -e 's/whsec_[A-Za-z0-9]+/[REDACTED]/g' \
89
+ -e 's/AKIA[A-Z0-9]{16}/[REDACTED]/g' \
90
+ -e 's/ghp_[A-Za-z0-9]{36}/[REDACTED]/g' \
91
+ -e 's/gho_[A-Za-z0-9]{36}/[REDACTED]/g' \
92
+ -e 's/glpat-[A-Za-z0-9_-]{20,}/[REDACTED]/g' \
93
+ -e 's/sk-ant-[A-Za-z0-9_-]+/[REDACTED]/g' \
94
+ -e 's/sk-[A-Za-z0-9]{20,}/[REDACTED]/g' \
95
+ -e 's/AIza[0-9A-Za-z_-]{35}/[REDACTED]/g' \
96
+ -e 's/xox[abps]-[A-Za-z0-9-]+/[REDACTED]/g' \
97
+ -e 's/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/[REDACTED_JWT]/g' \
98
+ -e 's|(https?://[^/:]+:)[^@]+(@)|\1[REDACTED]\2|g' \
99
+ -e 's/-----BEGIN [A-Z ]*PRIVATE KEY-----/[REDACTED_PRIVATE_KEY]/g' \
100
+ -e 's/(ANTHROPIC_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
101
+ -e 's/(OPENAI_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
102
+ -e 's/(GOOGLE_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
103
+ -e 's/(SLACK_TOKEN[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
104
+ -e 's/(DATABASE_URL[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g'
105
+ }
106
+
62
107
  # Collect project files into a destination file.
63
108
  # Uses git ls-files when available, falls back to find with common exclusions.
64
109
  # Usage: eagle_collect_files <target_dir> <output_file>