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.
- package/db/015_integrity_fixes.sql +156 -0
- package/db/migrate.sh +3 -0
- package/hooks/post-tool-use.sh +18 -147
- package/hooks/pre-tool-use.sh +4 -23
- package/hooks/session-end.sh +4 -1
- package/hooks/session-start.sh +23 -29
- package/hooks/stop.sh +6 -3
- package/lib/common.sh +34 -8
- package/lib/db-backfill.sh +96 -0
- package/lib/db-core.sh +65 -0
- package/lib/db-features.sh +160 -0
- package/lib/db-mirrors.sh +264 -0
- package/lib/db-observations.sh +77 -0
- package/lib/db-sessions.sh +68 -0
- package/lib/db-summaries.sh +142 -0
- package/lib/db.sh +14 -706
- package/lib/hooks-posttool.sh +138 -0
- package/lib/provider.sh +22 -6
- package/package.json +1 -1
- package/scripts/curate.sh +16 -0
- package/scripts/install.sh +5 -2
- package/scripts/memories.sh +3 -3
- package/scripts/uninstall.sh +1 -1
- package/scripts/update.sh +5 -1
|
@@ -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() {
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ═══════════════════════════════════════════════════════════
|
|
3
3
|
# Eagle Mem — PostToolUse hook
|
|
4
4
|
# Fires after every tool use
|
|
5
|
-
# Captures
|
|
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
|
-
# ───
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
# ───
|
|
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
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -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=$(
|
|
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=$(
|
|
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
|
-
|
|
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"
|
package/hooks/session-end.sh
CHANGED
|
@@ -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="$
|
|
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
|
package/hooks/session-start.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 ($
|
|
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=$(
|
|
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
|
-
|
|
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 )[^
|
|
68
|
-
-e 's/(api[_-]?key[= :])[^
|
|
69
|
-
-e 's/(password[= :])[^
|
|
70
|
-
-e 's/(secret[= :])[^
|
|
71
|
-
-e 's/(token[= :])[^
|
|
72
|
-
-e 's/(Authorization: )[^
|
|
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/
|
|
81
|
-
-e 's/
|
|
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.
|