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/hooks/session-start.sh
CHANGED
|
@@ -66,12 +66,68 @@ if [ -f "$version_file" ] && [ -s "$version_file" ]; then
|
|
|
66
66
|
fi
|
|
67
67
|
fi
|
|
68
68
|
|
|
69
|
-
# ───
|
|
69
|
+
# ─── Gather stats ───────────────────────────────────────────
|
|
70
|
+
|
|
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}"
|
|
95
|
+
|
|
96
|
+
# Build task summary line
|
|
97
|
+
task_parts=""
|
|
98
|
+
[ "$stat_tasks_progress" -gt 0 ] && task_parts="${stat_tasks_progress} in progress"
|
|
99
|
+
if [ "$stat_tasks_pending" -gt 0 ]; then
|
|
100
|
+
[ -n "$task_parts" ] && task_parts+=", "
|
|
101
|
+
task_parts+="${stat_tasks_pending} pending"
|
|
102
|
+
fi
|
|
103
|
+
if [ "$stat_tasks_done" -gt 0 ]; then
|
|
104
|
+
[ -n "$task_parts" ] && task_parts+=", "
|
|
105
|
+
task_parts+="${stat_tasks_done} completed"
|
|
106
|
+
fi
|
|
107
|
+
[ -z "$task_parts" ] && task_parts="none"
|
|
70
108
|
|
|
71
|
-
|
|
72
|
-
|
|
109
|
+
# Truncate last summary for display
|
|
110
|
+
stat_last_display="${stat_last_summary:0:60}"
|
|
111
|
+
[ ${#stat_last_summary} -gt 60 ] && stat_last_display+="..."
|
|
112
|
+
[ -z "$stat_last_display" ] && stat_last_display="(no sessions yet)"
|
|
113
|
+
|
|
114
|
+
# ─── Build context injection ────────────────────────────────
|
|
73
115
|
|
|
74
|
-
|
|
116
|
+
eagle_banner="======================================
|
|
117
|
+
Eagle Mem Loaded
|
|
118
|
+
======================================
|
|
119
|
+
Project | $project
|
|
120
|
+
Sessions | $stat_sessions total ($stat_summaries with summaries)
|
|
121
|
+
Memories | $stat_memories stored
|
|
122
|
+
Plans | $stat_plans saved
|
|
123
|
+
Tasks | $task_parts
|
|
124
|
+
Code Index | $stat_chunks chunks indexed
|
|
125
|
+
Observations | $stat_observations captured
|
|
126
|
+
Last Active | $stat_last_active
|
|
127
|
+
Last Work | $stat_last_display
|
|
128
|
+
======================================"
|
|
129
|
+
|
|
130
|
+
context="$eagle_banner
|
|
75
131
|
|
|
76
132
|
=== EAGLE MEM — Active (trigger: $source_type) ===
|
|
77
133
|
Eagle Mem (https://github.com/eagleisbatman/eagle-mem) is providing persistent memory for this session. It tracks summaries, observations, tasks, and code context across sessions via SQLite + FTS5. Mention Eagle Mem by name when referencing recalled context.
|
|
@@ -105,7 +161,7 @@ if [ -n "$recent" ]; then
|
|
|
105
161
|
context+="=== EAGLE MEM ===
|
|
106
162
|
Recent sessions for project '$project':
|
|
107
163
|
"
|
|
108
|
-
while IFS='|' read -r request completed learned next_steps created_at; do
|
|
164
|
+
while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files; do
|
|
109
165
|
[ -z "$request" ] && [ -z "$completed" ] && continue
|
|
110
166
|
context+="
|
|
111
167
|
--- $created_at ---"
|
|
@@ -115,6 +171,12 @@ Request: $request"
|
|
|
115
171
|
Completed: $completed"
|
|
116
172
|
[ -n "$learned" ] && context+="
|
|
117
173
|
Learned: $learned"
|
|
174
|
+
[ -n "$decisions" ] && context+="
|
|
175
|
+
Decisions: $decisions"
|
|
176
|
+
[ -n "$gotchas" ] && context+="
|
|
177
|
+
Gotchas: $gotchas"
|
|
178
|
+
[ -n "$key_files" ] && context+="
|
|
179
|
+
Key files: $key_files"
|
|
118
180
|
[ -n "$next_steps" ] && context+="
|
|
119
181
|
Next steps: $next_steps"
|
|
120
182
|
done <<< "$recent"
|
|
@@ -122,17 +184,32 @@ Next steps: $next_steps"
|
|
|
122
184
|
"
|
|
123
185
|
fi
|
|
124
186
|
|
|
125
|
-
# ─── Mirrored Claude memories
|
|
187
|
+
# ─── Mirrored Claude memories (with age) ─────────────────
|
|
126
188
|
|
|
127
|
-
memories=$(
|
|
189
|
+
memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at,
|
|
190
|
+
CAST(julianday('now') - julianday(updated_at) AS INTEGER) as days_ago
|
|
191
|
+
FROM claude_memories
|
|
192
|
+
WHERE project = '$p_esc'
|
|
193
|
+
ORDER BY updated_at DESC
|
|
194
|
+
LIMIT 5;")
|
|
128
195
|
if [ -n "$memories" ]; then
|
|
129
196
|
context+="
|
|
130
197
|
=== EAGLE MEM — Memories ===
|
|
131
198
|
Recent memories for '$project':
|
|
132
199
|
"
|
|
133
|
-
while IFS='|' read -r mname mtype mdesc _fpath _updated; do
|
|
200
|
+
while IFS='|' read -r mname mtype mdesc _fpath _updated days_ago; do
|
|
134
201
|
[ -z "$mname" ] && continue
|
|
135
|
-
|
|
202
|
+
age_label=""
|
|
203
|
+
if [ -n "$days_ago" ] && [ "$days_ago" -gt 0 ] 2>/dev/null; then
|
|
204
|
+
if [ "$days_ago" -eq 1 ]; then
|
|
205
|
+
age_label=" (1 day ago)"
|
|
206
|
+
else
|
|
207
|
+
age_label=" (${days_ago} days ago)"
|
|
208
|
+
fi
|
|
209
|
+
else
|
|
210
|
+
age_label=" (today)"
|
|
211
|
+
fi
|
|
212
|
+
context+=" - [$mtype] $mname: $mdesc$age_label
|
|
136
213
|
"
|
|
137
214
|
done <<< "$memories"
|
|
138
215
|
fi
|
|
@@ -155,7 +232,7 @@ fi
|
|
|
155
232
|
# ─── Claude Code tasks ───────────────────────────────────
|
|
156
233
|
|
|
157
234
|
synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
|
|
158
|
-
WHERE project = '$
|
|
235
|
+
WHERE project = '$p_esc'
|
|
159
236
|
AND status IN ('in_progress', 'pending')
|
|
160
237
|
AND updated_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days')
|
|
161
238
|
ORDER BY
|
|
@@ -183,29 +260,66 @@ context+="
|
|
|
183
260
|
=== EAGLE MEM INSTRUCTIONS ===
|
|
184
261
|
You have persistent memory powered by Eagle Mem. When you recall context from a previous session or use injected memory, attribute it: \"From Eagle Mem:\" or \"Eagle Mem recalls:\". This helps the user understand where the context came from.
|
|
185
262
|
|
|
186
|
-
IMPORTANT: At the start of your VERY NEXT response (this fires on session start, /clear, AND context compaction — always show this block, even if you think you showed it before, because prior context may have been compressed away). Show the user what Eagle Mem loaded
|
|
263
|
+
IMPORTANT: At the start of your VERY NEXT response (this fires on session start, /clear, AND context compaction — always show this block, even if you think you showed it before, because prior context may have been compressed away). Show the user what Eagle Mem loaded by reproducing this exact banner:
|
|
187
264
|
|
|
188
265
|
\`\`\`
|
|
189
|
-
$
|
|
190
|
-
|
|
191
|
-
Project: <project name>
|
|
192
|
-
Sessions: N recent | Memories: N | Tasks: N pending
|
|
193
|
-
Last: [one-line summary of most recent session]
|
|
266
|
+
$eagle_banner
|
|
194
267
|
\`\`\`
|
|
195
268
|
|
|
196
|
-
This gives the user visibility into the context
|
|
269
|
+
This gives the user visibility into the full context Eagle Mem loaded for this session.
|
|
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
|
+
|
|
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.
|
|
276
|
+
|
|
277
|
+
=== EAGLE MEM — SESSION SUMMARY (MANDATORY) ===
|
|
278
|
+
You MUST emit an <eagle-summary> block before your FINAL response in this session. This is how Eagle Mem captures what happened — without it, the next session starts blind and wastes tokens rediscovering context.
|
|
279
|
+
|
|
280
|
+
FORMAT — emit this block exactly. Every field is REQUIRED. Do not skip fields, do not leave them empty, do not write \"N/A\".
|
|
281
|
+
|
|
282
|
+
<eagle-summary>
|
|
283
|
+
request: [One sentence: what did the user ask for?]
|
|
284
|
+
investigated: [Comma-separated file paths you read or explored]
|
|
285
|
+
learned: [Non-obvious technical discoveries — things a future session could not guess from reading the code]
|
|
286
|
+
completed: [What was accomplished — be specific about what shipped, not what was \"worked on\"]
|
|
287
|
+
next_steps: [Concrete actions for the next session, not vague aspirations]
|
|
288
|
+
decisions:
|
|
289
|
+
- [Choice made] Why: [the reason — what constraint or tradeoff drove this choice]
|
|
290
|
+
- [Choice made] Why: [reason]
|
|
291
|
+
gotchas:
|
|
292
|
+
- [What failed, surprised, or does not work the obvious way. Be specific — \"X does not work because Y\" not just \"X was tricky\"]
|
|
293
|
+
key_files:
|
|
294
|
+
- [path/to/file.ext] — [one-line role: what this file does in the context of this work]
|
|
295
|
+
- [path/to/other.ext] — [role]
|
|
296
|
+
files_read: [file1, file2, ...]
|
|
297
|
+
files_modified: [file1, file2, ...]
|
|
298
|
+
</eagle-summary>
|
|
299
|
+
|
|
300
|
+
EXAMPLE — this is what a well-written summary looks like:
|
|
197
301
|
|
|
198
|
-
Before your final response in this session, emit a summary block:
|
|
199
302
|
<eagle-summary>
|
|
200
|
-
request:
|
|
201
|
-
investigated:
|
|
202
|
-
learned:
|
|
203
|
-
completed:
|
|
204
|
-
next_steps:
|
|
205
|
-
|
|
206
|
-
|
|
303
|
+
request: Add JWT authentication middleware to the API
|
|
304
|
+
investigated: src/middleware/auth.ts, src/routes/users.ts, package.json, src/config/env.ts
|
|
305
|
+
learned: express-jwt v8 changed its API — req.auth replaces req.user. The error handler must check err.name === 'UnauthorizedError', not err.status === 401.
|
|
306
|
+
completed: JWT middleware deployed on all /api routes. Token validation, role-based guards, and 401/403 error responses all working. Added JWKS endpoint support for key rotation.
|
|
307
|
+
next_steps: Add refresh token rotation; rate-limit the /auth/token endpoint
|
|
308
|
+
decisions:
|
|
309
|
+
- Chose RS256 over HS256 for JWT signing. Why: allows key rotation via JWKS without redeploying; HS256 requires shared secret on every service.
|
|
310
|
+
- Put auth middleware at router level, not app level. Why: healthcheck and public routes must remain unauthenticated; per-router mounting is explicit about what is protected.
|
|
311
|
+
gotchas:
|
|
312
|
+
- express-jwt v8 is ESM-only — require() fails silently and returns undefined. Must use dynamic import().
|
|
313
|
+
- Setting token expiry below 5 min causes refresh storms under load — the refresh endpoint itself requires a valid (but expired) token, creating a chicken-and-egg problem.
|
|
314
|
+
key_files:
|
|
315
|
+
- src/middleware/auth.ts — JWT validation + role guard middleware
|
|
316
|
+
- src/config/env.ts — JWKS_URI and JWT_ISSUER environment config
|
|
317
|
+
- src/routes/users.ts — first route to use the new auth guard (reference implementation)
|
|
318
|
+
files_read: [src/middleware/auth.ts, src/routes/users.ts, package.json, src/config/env.ts]
|
|
319
|
+
files_modified: [src/middleware/auth.ts, src/config/env.ts, src/routes/users.ts, package.json]
|
|
207
320
|
</eagle-summary>
|
|
208
|
-
|
|
321
|
+
|
|
322
|
+
WHY THIS MATTERS: Eagle Mem re-injects this summary at the start of future sessions. The 'decisions' field prevents re-debating settled choices. The 'gotchas' field prevents repeating the same mistakes. The 'key_files' field tells the next session exactly where to start reading instead of exploring blindly. Write these fields as if you are briefing a colleague who will pick up your work tomorrow — because that is exactly what happens.
|
|
209
323
|
"
|
|
210
324
|
|
|
211
325
|
# Output context (plain text stdout = additionalContext for SessionStart)
|
package/hooks/stop.sh
CHANGED
|
@@ -67,7 +67,7 @@ parse_field() {
|
|
|
67
67
|
$0 ~ "^"f":" {
|
|
68
68
|
sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
|
|
69
69
|
}
|
|
70
|
-
found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes):/ { exit }
|
|
70
|
+
found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files):/ { exit }
|
|
71
71
|
found { val = val " " $0 }
|
|
72
72
|
END { if (found) print val }
|
|
73
73
|
'
|
|
@@ -81,6 +81,9 @@ next_steps=""
|
|
|
81
81
|
files_read="[]"
|
|
82
82
|
files_modified="[]"
|
|
83
83
|
notes=""
|
|
84
|
+
decisions=""
|
|
85
|
+
gotchas=""
|
|
86
|
+
key_files=""
|
|
84
87
|
|
|
85
88
|
if [ -n "$summary_block" ]; then
|
|
86
89
|
request=$(parse_field "$summary_block" "request")
|
|
@@ -88,6 +91,9 @@ if [ -n "$summary_block" ]; then
|
|
|
88
91
|
learned=$(parse_field "$summary_block" "learned")
|
|
89
92
|
completed=$(parse_field "$summary_block" "completed")
|
|
90
93
|
next_steps=$(parse_field "$summary_block" "next_steps")
|
|
94
|
+
decisions=$(parse_field "$summary_block" "decisions")
|
|
95
|
+
gotchas=$(parse_field "$summary_block" "gotchas")
|
|
96
|
+
key_files=$(parse_field "$summary_block" "key_files")
|
|
91
97
|
|
|
92
98
|
raw_fr=$(parse_field "$summary_block" "files_read")
|
|
93
99
|
raw_fm=$(parse_field "$summary_block" "files_modified")
|
|
@@ -134,10 +140,21 @@ if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ];
|
|
|
134
140
|
fi
|
|
135
141
|
fi
|
|
136
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
|
+
|
|
137
154
|
# ─── Write to database ─────────────────────────────────────
|
|
138
155
|
|
|
139
156
|
if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
|
|
140
|
-
eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes"
|
|
157
|
+
eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"
|
|
141
158
|
eagle_log "INFO" "Stop: summary saved for session=$session_id"
|
|
142
159
|
fi
|
|
143
160
|
|
|
@@ -52,12 +52,18 @@ context=""
|
|
|
52
52
|
if [ -n "$results" ]; then
|
|
53
53
|
context+="=== EAGLE MEM — Relevant Memory ===
|
|
54
54
|
"
|
|
55
|
-
while IFS='|' read -r req completed learned _next_steps created_at _proj; do
|
|
55
|
+
while IFS='|' read -r req completed learned _next_steps created_at _proj decisions gotchas key_files; do
|
|
56
56
|
[ -z "$req" ] && [ -z "$completed" ] && continue
|
|
57
57
|
context+="[$created_at] "
|
|
58
58
|
[ -n "$req" ] && context+="$req"
|
|
59
59
|
[ -n "$completed" ] && context+=" → $completed"
|
|
60
60
|
[ -n "$learned" ] && context+=" (Learned: $learned)"
|
|
61
|
+
[ -n "$decisions" ] && context+="
|
|
62
|
+
Decisions: $decisions"
|
|
63
|
+
[ -n "$gotchas" ] && context+="
|
|
64
|
+
Gotchas: $gotchas"
|
|
65
|
+
[ -n "$key_files" ] && context+="
|
|
66
|
+
Key files: $key_files"
|
|
61
67
|
context+="
|
|
62
68
|
"
|
|
63
69
|
done <<< "$results"
|
package/lib/common.sh
CHANGED
|
@@ -59,6 +59,28 @@ eagle_read_stdin() {
|
|
|
59
59
|
echo "$input"
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
# Redact secrets from text before storage.
|
|
63
|
+
# Covers: Bearer tokens, API keys, passwords, secrets, tokens,
|
|
64
|
+
# Stripe/AWS/GitHub/Anthropic/OpenAI key patterns, named env vars.
|
|
65
|
+
eagle_redact() {
|
|
66
|
+
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' \
|
|
73
|
+
-e 's/sk_live_[A-Za-z0-9]+/[REDACTED]/g' \
|
|
74
|
+
-e 's/sk_test_[A-Za-z0-9]+/[REDACTED]/g' \
|
|
75
|
+
-e 's/AKIA[A-Z0-9]{16}/[REDACTED]/g' \
|
|
76
|
+
-e 's/ghp_[A-Za-z0-9]{36}/[REDACTED]/g' \
|
|
77
|
+
-e 's/gho_[A-Za-z0-9]{36}/[REDACTED]/g' \
|
|
78
|
+
-e 's/sk-ant-[A-Za-z0-9_-]+/[REDACTED]/g' \
|
|
79
|
+
-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'
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
# Collect project files into a destination file.
|
|
63
85
|
# Uses git ls-files when available, falls back to find with common exclusions.
|
|
64
86
|
# Usage: eagle_collect_files <target_dir> <output_file>
|
package/lib/db.sh
CHANGED
|
@@ -62,9 +62,19 @@ eagle_insert_observation() {
|
|
|
62
62
|
local tool_input_summary; tool_input_summary=$(eagle_sql_escape "$4")
|
|
63
63
|
local files_read; files_read=$(eagle_sql_escape "$5")
|
|
64
64
|
local files_modified; files_modified=$(eagle_sql_escape "$6")
|
|
65
|
+
local output_bytes="${7:-}"
|
|
66
|
+
local output_lines="${8:-}"
|
|
67
|
+
local command_category; command_category=$(eagle_sql_escape "${9:-}")
|
|
68
|
+
|
|
69
|
+
local extra_cols=""
|
|
70
|
+
local extra_vals=""
|
|
71
|
+
if [ -n "$output_bytes" ]; then
|
|
72
|
+
extra_cols=", output_bytes, output_lines, command_category"
|
|
73
|
+
extra_vals=", $(eagle_sql_int "$output_bytes"), $(eagle_sql_int "$output_lines"), '$command_category'"
|
|
74
|
+
fi
|
|
65
75
|
|
|
66
|
-
eagle_db "INSERT INTO observations (session_id, project, tool_name, tool_input_summary, files_read, files_modified)
|
|
67
|
-
SELECT '$session_id', '$project', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'
|
|
76
|
+
eagle_db "INSERT INTO observations (session_id, project, tool_name, tool_input_summary, files_read, files_modified${extra_cols})
|
|
77
|
+
SELECT '$session_id', '$project', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'${extra_vals}
|
|
68
78
|
WHERE NOT EXISTS (
|
|
69
79
|
SELECT 1 FROM observations
|
|
70
80
|
WHERE session_id = '$session_id'
|
|
@@ -85,9 +95,12 @@ eagle_insert_summary() {
|
|
|
85
95
|
local files_read; files_read=$(eagle_sql_escape "$8")
|
|
86
96
|
local files_modified; files_modified=$(eagle_sql_escape "$9")
|
|
87
97
|
local notes; notes=$(eagle_sql_escape "${10:-}")
|
|
98
|
+
local decisions; decisions=$(eagle_sql_escape "${11:-}")
|
|
99
|
+
local gotchas; gotchas=$(eagle_sql_escape "${12:-}")
|
|
100
|
+
local key_files; key_files=$(eagle_sql_escape "${13:-}")
|
|
88
101
|
|
|
89
102
|
eagle_db_pipe <<SQL
|
|
90
|
-
INSERT INTO summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_modified, notes)
|
|
103
|
+
INSERT INTO summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files)
|
|
91
104
|
VALUES (
|
|
92
105
|
'$session_id',
|
|
93
106
|
'$project',
|
|
@@ -98,7 +111,10 @@ VALUES (
|
|
|
98
111
|
'$next_steps',
|
|
99
112
|
'$files_read',
|
|
100
113
|
'$files_modified',
|
|
101
|
-
'$notes'
|
|
114
|
+
'$notes',
|
|
115
|
+
'$decisions',
|
|
116
|
+
'$gotchas',
|
|
117
|
+
'$key_files'
|
|
102
118
|
)
|
|
103
119
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
104
120
|
project = excluded.project,
|
|
@@ -109,7 +125,10 @@ ON CONFLICT(session_id) DO UPDATE SET
|
|
|
109
125
|
next_steps = COALESCE(NULLIF(excluded.next_steps, ''), summaries.next_steps),
|
|
110
126
|
files_read = COALESCE(NULLIF(excluded.files_read, '[]'), summaries.files_read),
|
|
111
127
|
files_modified = COALESCE(NULLIF(excluded.files_modified, '[]'), summaries.files_modified),
|
|
112
|
-
notes = COALESCE(NULLIF(excluded.notes, ''), summaries.notes)
|
|
128
|
+
notes = COALESCE(NULLIF(excluded.notes, ''), summaries.notes),
|
|
129
|
+
decisions = COALESCE(NULLIF(excluded.decisions, ''), summaries.decisions),
|
|
130
|
+
gotchas = COALESCE(NULLIF(excluded.gotchas, ''), summaries.gotchas),
|
|
131
|
+
key_files = COALESCE(NULLIF(excluded.key_files, ''), summaries.key_files);
|
|
113
132
|
SQL
|
|
114
133
|
}
|
|
115
134
|
|
|
@@ -117,7 +136,7 @@ eagle_get_recent_summaries() {
|
|
|
117
136
|
local project; project=$(eagle_sql_escape "$1")
|
|
118
137
|
local limit; limit=$(eagle_sql_int "${2:-5}")
|
|
119
138
|
|
|
120
|
-
eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at
|
|
139
|
+
eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.decisions, s.gotchas, s.key_files
|
|
121
140
|
FROM summaries s
|
|
122
141
|
WHERE s.project = '$project'
|
|
123
142
|
AND s.request NOT LIKE '%<local-command-caveat>%'
|
|
@@ -137,7 +156,7 @@ eagle_search_summaries() {
|
|
|
137
156
|
where_clause="AND s.project = '$project'"
|
|
138
157
|
fi
|
|
139
158
|
|
|
140
|
-
eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.project
|
|
159
|
+
eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.project, s.decisions, s.gotchas, s.key_files
|
|
141
160
|
FROM summaries s
|
|
142
161
|
JOIN summaries_fts f ON f.rowid = s.id
|
|
143
162
|
WHERE summaries_fts MATCH '$query'
|
|
@@ -556,3 +575,135 @@ COMMIT;"
|
|
|
556
575
|
fi
|
|
557
576
|
echo "$removed"
|
|
558
577
|
}
|
|
578
|
+
|
|
579
|
+
# ─── Feature graph helpers ─────��───────────────────────────
|
|
580
|
+
|
|
581
|
+
eagle_upsert_feature() {
|
|
582
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
583
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
584
|
+
local description; description=$(eagle_sql_escape "${3:-}")
|
|
585
|
+
|
|
586
|
+
eagle_db "INSERT INTO features (project, name, description)
|
|
587
|
+
VALUES ('$project', '$name', '$description')
|
|
588
|
+
ON CONFLICT(project, name) DO UPDATE SET
|
|
589
|
+
description = COALESCE(NULLIF('$description', ''), features.description),
|
|
590
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
eagle_add_feature_dependency() {
|
|
594
|
+
local feature_id; feature_id=$(eagle_sql_int "$1")
|
|
595
|
+
local kind; kind=$(eagle_sql_escape "$2")
|
|
596
|
+
local target; target=$(eagle_sql_escape "$3")
|
|
597
|
+
local name; name=$(eagle_sql_escape "$4")
|
|
598
|
+
local notes; notes=$(eagle_sql_escape "${5:-}")
|
|
599
|
+
|
|
600
|
+
eagle_db "INSERT OR IGNORE INTO feature_dependencies (feature_id, kind, target, name, notes)
|
|
601
|
+
VALUES ($feature_id, '$kind', '$target', '$name', '$notes');"
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
eagle_add_feature_file() {
|
|
605
|
+
local feature_id; feature_id=$(eagle_sql_int "$1")
|
|
606
|
+
local file_path; file_path=$(eagle_sql_escape "$2")
|
|
607
|
+
local role; role=$(eagle_sql_escape "${3:-}")
|
|
608
|
+
|
|
609
|
+
eagle_db "INSERT OR IGNORE INTO feature_files (feature_id, file_path, role)
|
|
610
|
+
VALUES ($feature_id, '$file_path', '$role');"
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
eagle_add_feature_smoke_test() {
|
|
614
|
+
local feature_id; feature_id=$(eagle_sql_int "$1")
|
|
615
|
+
local command; command=$(eagle_sql_escape "$2")
|
|
616
|
+
local description; description=$(eagle_sql_escape "${3:-}")
|
|
617
|
+
|
|
618
|
+
eagle_db "INSERT INTO feature_smoke_tests (feature_id, command, description)
|
|
619
|
+
VALUES ($feature_id, '$command', '$description');"
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
eagle_verify_feature() {
|
|
623
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
624
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
625
|
+
local notes; notes=$(eagle_sql_escape "${3:-}")
|
|
626
|
+
|
|
627
|
+
eagle_db "UPDATE features SET
|
|
628
|
+
last_verified_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
629
|
+
last_verified_notes = '$notes',
|
|
630
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
631
|
+
WHERE project = '$project' AND name = '$name';"
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
eagle_get_feature_id() {
|
|
635
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
636
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
637
|
+
eagle_db "SELECT id FROM features WHERE project = '$project' AND name = '$name';"
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
eagle_list_features() {
|
|
641
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
642
|
+
local limit; limit=$(eagle_sql_int "${2:-20}")
|
|
643
|
+
|
|
644
|
+
eagle_db "SELECT f.name, f.description, f.status, f.last_verified_at,
|
|
645
|
+
(SELECT COUNT(*) FROM feature_dependencies WHERE feature_id = f.id) as dep_count,
|
|
646
|
+
(SELECT COUNT(*) FROM feature_files WHERE feature_id = f.id) as file_count,
|
|
647
|
+
(SELECT COUNT(*) FROM feature_smoke_tests WHERE feature_id = f.id) as test_count
|
|
648
|
+
FROM features f
|
|
649
|
+
WHERE f.project = '$project' AND f.status = 'active'
|
|
650
|
+
ORDER BY f.updated_at DESC
|
|
651
|
+
LIMIT $limit;"
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
eagle_show_feature() {
|
|
655
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
656
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
657
|
+
|
|
658
|
+
local feature_id
|
|
659
|
+
feature_id=$(eagle_get_feature_id "$1" "$2")
|
|
660
|
+
[ -z "$feature_id" ] && return 1
|
|
661
|
+
|
|
662
|
+
echo "=== Feature: $2 ==="
|
|
663
|
+
eagle_db "SELECT name, description, status, last_verified_at, last_verified_notes
|
|
664
|
+
FROM features WHERE id = $feature_id;"
|
|
665
|
+
|
|
666
|
+
local deps
|
|
667
|
+
deps=$(eagle_db "SELECT kind, target, name, notes FROM feature_dependencies WHERE feature_id = $feature_id;")
|
|
668
|
+
if [ -n "$deps" ]; then
|
|
669
|
+
echo "--- Dependencies ---"
|
|
670
|
+
echo "$deps"
|
|
671
|
+
fi
|
|
672
|
+
|
|
673
|
+
local files
|
|
674
|
+
files=$(eagle_db "SELECT file_path, role FROM feature_files WHERE feature_id = $feature_id;")
|
|
675
|
+
if [ -n "$files" ]; then
|
|
676
|
+
echo "--- Files ---"
|
|
677
|
+
echo "$files"
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
local tests
|
|
681
|
+
tests=$(eagle_db "SELECT command, description FROM feature_smoke_tests WHERE feature_id = $feature_id;")
|
|
682
|
+
if [ -n "$tests" ]; then
|
|
683
|
+
echo "--- Smoke Tests ---"
|
|
684
|
+
echo "$tests"
|
|
685
|
+
fi
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
eagle_find_features_for_file() {
|
|
689
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
690
|
+
local file_path="$2"
|
|
691
|
+
local fname; fname=$(basename "$file_path")
|
|
692
|
+
local fname_esc; fname_esc=$(eagle_sql_escape "$fname")
|
|
693
|
+
|
|
694
|
+
eagle_db "SELECT f.name, f.description, f.last_verified_at,
|
|
695
|
+
ff.role,
|
|
696
|
+
(SELECT GROUP_CONCAT(fd.target || ':' || fd.name, ', ')
|
|
697
|
+
FROM feature_dependencies fd WHERE fd.feature_id = f.id) as deps,
|
|
698
|
+
(SELECT GROUP_CONCAT(ff2.file_path, ', ')
|
|
699
|
+
FROM feature_files ff2 WHERE ff2.feature_id = f.id AND ff2.file_path != ff.file_path) as other_files,
|
|
700
|
+
(SELECT GROUP_CONCAT(fst.command, ', ')
|
|
701
|
+
FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke_tests
|
|
702
|
+
FROM features f
|
|
703
|
+
JOIN feature_files ff ON ff.feature_id = f.id
|
|
704
|
+
WHERE f.project = '$project'
|
|
705
|
+
AND f.status = 'active'
|
|
706
|
+
AND (ff.file_path LIKE '%$fname_esc' OR ff.file_path LIKE '%$fname_esc%')
|
|
707
|
+
ORDER BY f.updated_at DESC
|
|
708
|
+
LIMIT 3;"
|
|
709
|
+
}
|