eagle-mem 2.0.6 → 3.0.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.
- package/README.md +16 -8
- package/bin/eagle-mem +3 -0
- package/db/012_enriched_summaries.sql +51 -0
- package/db/013_features.sql +69 -0
- package/db/014_command_intelligence.sql +25 -0
- package/hooks/post-tool-use.sh +131 -18
- package/hooks/pre-tool-use.sh +124 -0
- package/hooks/session-start.sh +140 -26
- package/hooks/stop.sh +19 -2
- package/hooks/user-prompt-submit.sh +7 -1
- package/lib/common.sh +22 -0
- package/lib/db.sh +158 -7
- package/lib/provider.sh +330 -0
- package/package.json +1 -1
- package/scripts/config.sh +72 -0
- package/scripts/curate.sh +349 -0
- package/scripts/feature.sh +110 -0
- package/scripts/help.sh +3 -0
- package/scripts/index.sh +1 -1
- package/scripts/install.sh +14 -0
- package/scripts/style.sh +4 -6
- package/scripts/update.sh +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
```
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
======================================
|
|
3
|
+
Eagle Mem
|
|
4
|
+
======================================
|
|
4
5
|
```
|
|
5
6
|
|
|
6
7
|
# Eagle Mem
|
|
@@ -21,12 +22,19 @@ eagle-mem install
|
|
|
21
22
|
**2. Open Claude Code** in any project directory. Eagle Mem activates and shows what it loaded:
|
|
22
23
|
|
|
23
24
|
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Project
|
|
28
|
-
Sessions
|
|
29
|
-
|
|
25
|
+
======================================
|
|
26
|
+
Eagle Mem Loaded
|
|
27
|
+
======================================
|
|
28
|
+
Project | my-app
|
|
29
|
+
Sessions | 42 total (18 with summaries)
|
|
30
|
+
Memories | 7 stored
|
|
31
|
+
Plans | 2 saved
|
|
32
|
+
Tasks | 1 in progress, 3 pending, 12 completed
|
|
33
|
+
Code Index | 156 chunks indexed
|
|
34
|
+
Observations | 340 captured
|
|
35
|
+
Last Active | 2026-04-28
|
|
36
|
+
Last Work | Added auth middleware with JWT validation
|
|
37
|
+
======================================
|
|
30
38
|
```
|
|
31
39
|
|
|
32
40
|
From here, everything is automatic. You don't run any commands — Eagle Mem captures session summaries, mirrors Claude's memories and tasks, and re-injects context after every `/compact` or new session.
|
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,51 @@
|
|
|
1
|
+
-- Migration 012: Add decisions, gotchas, key_files columns to summaries
|
|
2
|
+
-- These capture WHY choices were made, WHAT went wrong, and WHERE to start reading.
|
|
3
|
+
-- Also adds them to FTS5 so they're searchable across sessions.
|
|
4
|
+
|
|
5
|
+
-- Add new columns
|
|
6
|
+
ALTER TABLE summaries ADD COLUMN decisions TEXT;
|
|
7
|
+
ALTER TABLE summaries ADD COLUMN gotchas TEXT;
|
|
8
|
+
ALTER TABLE summaries ADD COLUMN key_files TEXT;
|
|
9
|
+
|
|
10
|
+
-- Rebuild FTS5 to include new columns
|
|
11
|
+
DROP TRIGGER IF EXISTS summaries_ai;
|
|
12
|
+
DROP TRIGGER IF EXISTS summaries_ad;
|
|
13
|
+
DROP TRIGGER IF EXISTS summaries_au;
|
|
14
|
+
|
|
15
|
+
DROP TABLE IF EXISTS summaries_fts;
|
|
16
|
+
|
|
17
|
+
CREATE VIRTUAL TABLE summaries_fts USING fts5(
|
|
18
|
+
request,
|
|
19
|
+
investigated,
|
|
20
|
+
learned,
|
|
21
|
+
completed,
|
|
22
|
+
next_steps,
|
|
23
|
+
notes,
|
|
24
|
+
decisions,
|
|
25
|
+
gotchas,
|
|
26
|
+
key_files,
|
|
27
|
+
content='summaries',
|
|
28
|
+
content_rowid='id'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- Backfill FTS from existing data
|
|
32
|
+
INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
|
|
33
|
+
SELECT id, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files FROM summaries;
|
|
34
|
+
|
|
35
|
+
-- Recreate content-sync triggers with new columns
|
|
36
|
+
CREATE TRIGGER summaries_ai AFTER INSERT ON summaries BEGIN
|
|
37
|
+
INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
|
|
38
|
+
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes, new.decisions, new.gotchas, new.key_files);
|
|
39
|
+
END;
|
|
40
|
+
|
|
41
|
+
CREATE TRIGGER summaries_ad AFTER DELETE ON summaries BEGIN
|
|
42
|
+
INSERT INTO summaries_fts(summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
|
|
43
|
+
VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes, old.decisions, old.gotchas, old.key_files);
|
|
44
|
+
END;
|
|
45
|
+
|
|
46
|
+
CREATE TRIGGER summaries_au AFTER UPDATE ON summaries BEGIN
|
|
47
|
+
INSERT INTO summaries_fts(summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
|
|
48
|
+
VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes, old.decisions, old.gotchas, old.key_files);
|
|
49
|
+
INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
|
|
50
|
+
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes, new.decisions, new.gotchas, new.key_files);
|
|
51
|
+
END;
|
|
@@ -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);
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -34,6 +34,9 @@ project=$(eagle_project_from_cwd "$cwd")
|
|
|
34
34
|
files_read="[]"
|
|
35
35
|
files_modified="[]"
|
|
36
36
|
tool_summary=""
|
|
37
|
+
output_bytes=""
|
|
38
|
+
output_lines=""
|
|
39
|
+
command_category=""
|
|
37
40
|
|
|
38
41
|
case "$tool_name" in
|
|
39
42
|
Read)
|
|
@@ -53,24 +56,34 @@ case "$tool_name" in
|
|
|
53
56
|
;;
|
|
54
57
|
Bash)
|
|
55
58
|
cmd=$(echo "$input" | jq -r '.tool_input.command // empty' | cut -c1-200)
|
|
56
|
-
|
|
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')
|
|
59
|
+
cmd=$(echo "$cmd" | eagle_redact)
|
|
73
60
|
tool_summary="Bash: $cmd"
|
|
61
|
+
|
|
62
|
+
# Output metrics
|
|
63
|
+
tool_output=$(echo "$input" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
|
|
64
|
+
if [ -n "$tool_output" ]; then
|
|
65
|
+
output_bytes=${#tool_output}
|
|
66
|
+
output_lines=$(echo "$tool_output" | wc -l | tr -d ' ')
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Command category extraction
|
|
70
|
+
first_word=$(echo "$cmd" | awk '{print $1}' | sed 's|.*/||')
|
|
71
|
+
case "$first_word" in
|
|
72
|
+
git|gh) command_category="git" ;;
|
|
73
|
+
npm|npx|pnpm|yarn|bun) command_category="js" ;;
|
|
74
|
+
pip|pip3|python|python3|uv) command_category="python" ;;
|
|
75
|
+
cargo|rustc) command_category="rust" ;;
|
|
76
|
+
go) command_category="go" ;;
|
|
77
|
+
docker|docker-compose|podman) command_category="docker" ;;
|
|
78
|
+
kubectl|helm|k9s) command_category="k8s" ;;
|
|
79
|
+
aws|gcloud|az) command_category="cloud" ;;
|
|
80
|
+
make|cmake|ninja) command_category="build" ;;
|
|
81
|
+
grep|find|ls|cat|head|tail|wc|sort|sed|awk) command_category="files" ;;
|
|
82
|
+
curl|wget|http) command_category="http" ;;
|
|
83
|
+
*test*|jest|pytest|vitest|mocha) command_category="test" ;;
|
|
84
|
+
*lint*|eslint|ruff|golangci-lint) command_category="lint" ;;
|
|
85
|
+
*) command_category="other" ;;
|
|
86
|
+
esac
|
|
74
87
|
;;
|
|
75
88
|
TaskCreate|TaskUpdate)
|
|
76
89
|
task_subject=$(echo "$input" | jq -r '.tool_input.subject // empty')
|
|
@@ -124,6 +137,106 @@ case "$tool_name" in
|
|
|
124
137
|
;;
|
|
125
138
|
esac
|
|
126
139
|
|
|
127
|
-
|
|
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
|
|
239
|
+
|
|
240
|
+
eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"
|
|
128
241
|
|
|
129
242
|
exit 0
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
|
|
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%');")
|
|
64
|
+
|
|
65
|
+
while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
|
|
66
|
+
[ -z "$feat_name" ] && continue
|
|
67
|
+
case "$seen_features" in *"|$feat_name|"*) continue ;; esac
|
|
68
|
+
seen_features+="|$feat_name|"
|
|
69
|
+
|
|
70
|
+
context+=" - $feat_name"
|
|
71
|
+
[ -n "$feat_smoke" ] && context+=" | smoke: $feat_smoke"
|
|
72
|
+
[ -n "$feat_deps" ] && context+=" | deps: $feat_deps"
|
|
73
|
+
if [ -n "$feat_verified" ]; then
|
|
74
|
+
context+=" | last verified: $feat_verified"
|
|
75
|
+
else
|
|
76
|
+
context+=" | never verified"
|
|
77
|
+
fi
|
|
78
|
+
context+=$'\n'
|
|
79
|
+
done <<< "$feature_hits"
|
|
80
|
+
done <<< "$changed_files"
|
|
81
|
+
|
|
82
|
+
if [ -n "$context" ]; then
|
|
83
|
+
context="Eagle Mem: This push affects the following features. After deploy, verify each works and run 'eagle-mem feature verify <name>'.
|
|
84
|
+
${context}"
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
|
|
91
|
+
# ─── Command output filtering (learned rules) ─────────────
|
|
92
|
+
|
|
93
|
+
# Extract the base command for rule matching
|
|
94
|
+
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;")
|
|
105
|
+
|
|
106
|
+
if [ -n "$rule" ]; then
|
|
107
|
+
IFS='|' read -r strategy max_lines reason <<< "$rule"
|
|
108
|
+
case "$strategy" in
|
|
109
|
+
summary)
|
|
110
|
+
context+="Eagle Mem command hint: '${base_cmd}' output is typically noisy (${reason}). Consider piping through 'tail -5' or checking exit code only."
|
|
111
|
+
;;
|
|
112
|
+
truncate)
|
|
113
|
+
if [ -n "$max_lines" ] && [ "$max_lines" -gt 0 ] 2>/dev/null; then
|
|
114
|
+
context+="Eagle Mem command hint: '${base_cmd}' produces long output (${reason}). Consider: ${cmd} | head -${max_lines}"
|
|
115
|
+
fi
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
[ -z "$context" ] && exit 0
|
|
121
|
+
|
|
122
|
+
jq -nc --arg ctx "$context" '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":$ctx}}'
|
|
123
|
+
|
|
124
|
+
exit 0
|