eagle-mem 1.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 +227 -0
- package/bin/eagle-mem +36 -0
- package/db/002_overviews.sql +16 -0
- package/db/003_code_chunks.sql +46 -0
- package/db/migrate.sh +47 -0
- package/db/schema.sql +154 -0
- package/hooks/post-tool-use.sh +76 -0
- package/hooks/session-end.sh +25 -0
- package/hooks/session-start.sh +161 -0
- package/hooks/stop.sh +147 -0
- package/hooks/user-prompt-submit.sh +102 -0
- package/lib/common.sh +63 -0
- package/lib/db.sh +186 -0
- package/package.json +31 -0
- package/scripts/help.sh +50 -0
- package/scripts/index.sh +196 -0
- package/scripts/install.sh +238 -0
- package/scripts/scan.sh +362 -0
- package/scripts/style.sh +89 -0
- package/scripts/uninstall.sh +63 -0
- package/scripts/update.sh +106 -0
- package/skills/eagle-mem-overview/SKILL.md +103 -0
- package/skills/eagle-mem-search/SKILL.md +126 -0
- package/skills/eagle-mem-tasks/SKILL.md +123 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — SessionStart hook
|
|
4
|
+
# Fires on: startup, resume, clear, compact
|
|
5
|
+
# Injects project memory + pending tasks into Claude's context
|
|
6
|
+
# ═══════════════════════════════════════════════════════════
|
|
7
|
+
set +e
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
LIB_DIR="$SCRIPT_DIR/../lib"
|
|
11
|
+
|
|
12
|
+
. "$LIB_DIR/common.sh"
|
|
13
|
+
. "$LIB_DIR/db.sh"
|
|
14
|
+
|
|
15
|
+
eagle_ensure_db
|
|
16
|
+
|
|
17
|
+
input=$(eagle_read_stdin)
|
|
18
|
+
[ -z "$input" ] && exit 0
|
|
19
|
+
|
|
20
|
+
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
21
|
+
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
22
|
+
source_type=$(echo "$input" | jq -r '.source // empty')
|
|
23
|
+
model=$(echo "$input" | jq -r '.model // empty')
|
|
24
|
+
|
|
25
|
+
[ -z "$session_id" ] && exit 0
|
|
26
|
+
|
|
27
|
+
project=$(eagle_project_from_cwd "$cwd")
|
|
28
|
+
project_sql=$(eagle_sql_escape "$project")
|
|
29
|
+
|
|
30
|
+
eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
|
|
31
|
+
|
|
32
|
+
eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
|
|
33
|
+
|
|
34
|
+
# ─── Build context injection ────────────────────────────────
|
|
35
|
+
|
|
36
|
+
context=""
|
|
37
|
+
|
|
38
|
+
# Project overview (if one exists)
|
|
39
|
+
overview=$(eagle_get_overview "$project")
|
|
40
|
+
if [ -n "$overview" ]; then
|
|
41
|
+
context+="=== EAGLE MEM — Project Overview ===
|
|
42
|
+
$overview
|
|
43
|
+
|
|
44
|
+
"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Recent summaries for this project (last 5 sessions)
|
|
48
|
+
recent=$(eagle_db "
|
|
49
|
+
SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at
|
|
50
|
+
FROM summaries s
|
|
51
|
+
WHERE s.project = '$project_sql'
|
|
52
|
+
ORDER BY s.created_at DESC
|
|
53
|
+
LIMIT 5;
|
|
54
|
+
")
|
|
55
|
+
|
|
56
|
+
if [ -n "$recent" ]; then
|
|
57
|
+
context+="=== EAGLE MEM ===
|
|
58
|
+
Recent sessions for project '$project':
|
|
59
|
+
"
|
|
60
|
+
while IFS='|' read -r request completed learned next_steps created_at; do
|
|
61
|
+
[ -z "$request" ] && [ -z "$completed" ] && continue
|
|
62
|
+
context+="
|
|
63
|
+
--- $created_at ---"
|
|
64
|
+
[ -n "$request" ] && context+="
|
|
65
|
+
Request: $request"
|
|
66
|
+
[ -n "$completed" ] && context+="
|
|
67
|
+
Completed: $completed"
|
|
68
|
+
[ -n "$learned" ] && context+="
|
|
69
|
+
Learned: $learned"
|
|
70
|
+
[ -n "$next_steps" ] && context+="
|
|
71
|
+
Next steps: $next_steps"
|
|
72
|
+
done <<< "$recent"
|
|
73
|
+
context+="
|
|
74
|
+
"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Pending tasks from TaskAware loop
|
|
78
|
+
pending_tasks=$(eagle_db "
|
|
79
|
+
SELECT id, title, instructions, status
|
|
80
|
+
FROM tasks
|
|
81
|
+
WHERE project = '$project_sql' AND status IN ('pending', 'active')
|
|
82
|
+
ORDER BY ordinal ASC, id ASC
|
|
83
|
+
LIMIT 10;
|
|
84
|
+
")
|
|
85
|
+
|
|
86
|
+
if [ -n "$pending_tasks" ]; then
|
|
87
|
+
context+="
|
|
88
|
+
=== EAGLE MEM — Tasks ===
|
|
89
|
+
Pending tasks for '$project':
|
|
90
|
+
"
|
|
91
|
+
first_pending=""
|
|
92
|
+
while IFS='|' read -r tid title instructions status; do
|
|
93
|
+
[ -z "$tid" ] && continue
|
|
94
|
+
local_marker=""
|
|
95
|
+
if [ "$status" = "active" ]; then
|
|
96
|
+
local_marker=" [ACTIVE]"
|
|
97
|
+
elif [ -z "$first_pending" ]; then
|
|
98
|
+
local_marker=" [NEXT]"
|
|
99
|
+
first_pending="$tid"
|
|
100
|
+
fi
|
|
101
|
+
context+=" $tid. $title$local_marker"
|
|
102
|
+
[ -n "$instructions" ] && context+=" — $instructions"
|
|
103
|
+
context+="
|
|
104
|
+
"
|
|
105
|
+
done <<< "$pending_tasks"
|
|
106
|
+
|
|
107
|
+
# Load context snapshot for the active/next task
|
|
108
|
+
active_task=$(eagle_db "
|
|
109
|
+
SELECT id, title, instructions, context_snapshot
|
|
110
|
+
FROM tasks
|
|
111
|
+
WHERE project = '$project_sql' AND status = 'active'
|
|
112
|
+
ORDER BY ordinal ASC, id ASC
|
|
113
|
+
LIMIT 1;
|
|
114
|
+
")
|
|
115
|
+
|
|
116
|
+
if [ -z "$active_task" ] && [ -n "$first_pending" ]; then
|
|
117
|
+
# Auto-activate the next pending task
|
|
118
|
+
eagle_db "UPDATE tasks SET status = 'active', started_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = $first_pending;"
|
|
119
|
+
active_task=$(eagle_db "
|
|
120
|
+
SELECT id, title, instructions, context_snapshot
|
|
121
|
+
FROM tasks
|
|
122
|
+
WHERE id = $first_pending;
|
|
123
|
+
")
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
if [ -n "$active_task" ]; then
|
|
127
|
+
IFS='|' read -r atid atitle ainstructions asnapshot <<< "$active_task"
|
|
128
|
+
context+="
|
|
129
|
+
Current task (#$atid): $atitle
|
|
130
|
+
"
|
|
131
|
+
[ -n "$ainstructions" ] && context+="Instructions: $ainstructions
|
|
132
|
+
"
|
|
133
|
+
[ -n "$asnapshot" ] && context+="Context: $asnapshot
|
|
134
|
+
"
|
|
135
|
+
context+="When done, tell the user to run /compact so Eagle Mem can save progress and load the next task.
|
|
136
|
+
"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Emit the eagle-summary instruction
|
|
141
|
+
context+="
|
|
142
|
+
=== EAGLE MEM INSTRUCTIONS ===
|
|
143
|
+
Before your final response in this session, emit a summary block:
|
|
144
|
+
<eagle-summary>
|
|
145
|
+
request: What the user asked for
|
|
146
|
+
investigated: Key files/areas explored
|
|
147
|
+
learned: Non-obvious discoveries
|
|
148
|
+
completed: What was accomplished
|
|
149
|
+
next_steps: What should happen next
|
|
150
|
+
files_read: [list of files read]
|
|
151
|
+
files_modified: [list of files modified]
|
|
152
|
+
</eagle-summary>
|
|
153
|
+
This helps Eagle Mem track what happened for future sessions.
|
|
154
|
+
"
|
|
155
|
+
|
|
156
|
+
# Output context (plain text stdout = additionalContext for SessionStart)
|
|
157
|
+
if [ -n "$context" ]; then
|
|
158
|
+
echo "$context"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
exit 0
|
package/hooks/stop.sh
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Stop hook
|
|
4
|
+
# Fires when Claude's turn ends
|
|
5
|
+
# Parses <eagle-summary> from transcript, saves to DB
|
|
6
|
+
# Falls back to heuristic extraction if no summary block
|
|
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
|
+
eagle_ensure_db
|
|
17
|
+
|
|
18
|
+
input=$(eagle_read_stdin)
|
|
19
|
+
[ -z "$input" ] && exit 0
|
|
20
|
+
|
|
21
|
+
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
22
|
+
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
23
|
+
transcript_path=$(echo "$input" | jq -r '.transcript_path // empty')
|
|
24
|
+
|
|
25
|
+
[ -z "$session_id" ] && exit 0
|
|
26
|
+
|
|
27
|
+
# Skip subagent contexts
|
|
28
|
+
agent_type=$(echo "$input" | jq -r '.agent_type // empty')
|
|
29
|
+
[ -n "$agent_type" ] && [ "$agent_type" != "main" ] && exit 0
|
|
30
|
+
|
|
31
|
+
project=$(eagle_project_from_cwd "$cwd")
|
|
32
|
+
project_sql=$(eagle_sql_escape "$project")
|
|
33
|
+
|
|
34
|
+
eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path"
|
|
35
|
+
|
|
36
|
+
# Ensure session exists (may not if SessionStart didn't fire)
|
|
37
|
+
eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
|
|
38
|
+
|
|
39
|
+
# ─── Try to parse <eagle-summary> from transcript ──────────
|
|
40
|
+
|
|
41
|
+
summary_block=""
|
|
42
|
+
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
43
|
+
# Extract all text from the last assistant message using jq on JSONL
|
|
44
|
+
# Real transcript format: top-level .type == "assistant", content in .message.content[]
|
|
45
|
+
text_content=$(jq -rs '
|
|
46
|
+
[.[] | select(.type == "assistant")] | last |
|
|
47
|
+
if . then
|
|
48
|
+
[.message.content[]? | select(.type == "text") | .text] | join("\n")
|
|
49
|
+
else "" end
|
|
50
|
+
' "$transcript_path" 2>/dev/null)
|
|
51
|
+
|
|
52
|
+
# Strip <private>...</private> blocks before processing
|
|
53
|
+
text_content=$(echo "$text_content" | sed '/<private>/,/<\/private>/d')
|
|
54
|
+
|
|
55
|
+
# Parse <eagle-summary> block
|
|
56
|
+
if [ -n "$text_content" ] && echo "$text_content" | grep -q '<eagle-summary>' 2>/dev/null; then
|
|
57
|
+
summary_block=$(echo "$text_content" | sed -n '/<eagle-summary>/,/<\/eagle-summary>/p' | sed '1d;$d')
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# ─── Extract fields from summary block ─────────────────────
|
|
62
|
+
|
|
63
|
+
parse_field() {
|
|
64
|
+
local block="$1"
|
|
65
|
+
local field="$2"
|
|
66
|
+
echo "$block" | awk -v f="$field" '
|
|
67
|
+
BEGIN { IGNORECASE=1; found=0 }
|
|
68
|
+
$0 ~ "^"f":" {
|
|
69
|
+
sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
|
|
70
|
+
}
|
|
71
|
+
found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes):/ { exit }
|
|
72
|
+
found { val = val " " $0 }
|
|
73
|
+
END { if (found) print val }
|
|
74
|
+
'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
request=""
|
|
78
|
+
investigated=""
|
|
79
|
+
learned=""
|
|
80
|
+
completed=""
|
|
81
|
+
next_steps=""
|
|
82
|
+
files_read="[]"
|
|
83
|
+
files_modified="[]"
|
|
84
|
+
notes=""
|
|
85
|
+
|
|
86
|
+
if [ -n "$summary_block" ]; then
|
|
87
|
+
request=$(parse_field "$summary_block" "request")
|
|
88
|
+
investigated=$(parse_field "$summary_block" "investigated")
|
|
89
|
+
learned=$(parse_field "$summary_block" "learned")
|
|
90
|
+
completed=$(parse_field "$summary_block" "completed")
|
|
91
|
+
next_steps=$(parse_field "$summary_block" "next_steps")
|
|
92
|
+
|
|
93
|
+
raw_fr=$(parse_field "$summary_block" "files_read")
|
|
94
|
+
raw_fm=$(parse_field "$summary_block" "files_modified")
|
|
95
|
+
|
|
96
|
+
# Convert bracket-list to JSON array (handles special chars safely)
|
|
97
|
+
to_json_array() {
|
|
98
|
+
local raw="$1"
|
|
99
|
+
raw=$(echo "$raw" | sed 's/^\[//;s/\]$//')
|
|
100
|
+
echo "$raw" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | jq -Rsc 'split("\n") | map(select(. != ""))'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[ -n "$raw_fr" ] && files_read=$(to_json_array "$raw_fr")
|
|
104
|
+
[ -n "$raw_fm" ] && files_modified=$(to_json_array "$raw_fm")
|
|
105
|
+
|
|
106
|
+
eagle_log "INFO" "Stop: parsed eagle-summary block"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# ─── Heuristic fallback: extract from tool calls ───────────
|
|
110
|
+
|
|
111
|
+
if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
112
|
+
eagle_log "INFO" "Stop: no eagle-summary found, using heuristic fallback"
|
|
113
|
+
|
|
114
|
+
# Extract first user prompt as "request"
|
|
115
|
+
request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null | head -1 | cut -c1-500)
|
|
116
|
+
|
|
117
|
+
# Extract files from Read/Write/Edit tool calls
|
|
118
|
+
heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
|
|
119
|
+
heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
|
|
120
|
+
|
|
121
|
+
if [ -n "$heuristic_reads" ]; then
|
|
122
|
+
files_read=$(echo "$heuristic_reads" | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
123
|
+
fi
|
|
124
|
+
if [ -n "$heuristic_writes" ]; then
|
|
125
|
+
files_modified=$(echo "$heuristic_writes" | jq -Rsc 'split("\n") | map(select(. != ""))')
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
completed="(auto-captured from tool usage)"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# ─── Write to database ─────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
|
|
134
|
+
eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes"
|
|
135
|
+
eagle_log "INFO" "Stop: summary saved for session=$session_id"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# Mark active task as done if eagle-summary mentions completion
|
|
139
|
+
if [ -n "$completed" ]; then
|
|
140
|
+
active_task_id=$(eagle_db "SELECT id FROM tasks WHERE project = '$project_sql' AND status = 'active' LIMIT 1;")
|
|
141
|
+
if [ -n "$active_task_id" ]; then
|
|
142
|
+
eagle_db "UPDATE tasks SET status = 'done', completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = $active_task_id;"
|
|
143
|
+
eagle_log "INFO" "Stop: marked task #$active_task_id as done"
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
exit 0
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — UserPromptSubmit hook
|
|
4
|
+
# Fires when the user submits a prompt
|
|
5
|
+
# Searches memory for relevant context and injects it
|
|
6
|
+
# ═══════════════════════════════════════════════════════════
|
|
7
|
+
set +e
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
LIB_DIR="$SCRIPT_DIR/../lib"
|
|
11
|
+
|
|
12
|
+
. "$LIB_DIR/common.sh"
|
|
13
|
+
. "$LIB_DIR/db.sh"
|
|
14
|
+
|
|
15
|
+
[ ! -f "$EAGLE_MEM_DB" ] && exit 0
|
|
16
|
+
|
|
17
|
+
input=$(eagle_read_stdin)
|
|
18
|
+
[ -z "$input" ] && exit 0
|
|
19
|
+
|
|
20
|
+
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
21
|
+
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
22
|
+
user_prompt=$(echo "$input" | jq -r '.prompt // empty')
|
|
23
|
+
|
|
24
|
+
[ -z "$user_prompt" ] && exit 0
|
|
25
|
+
|
|
26
|
+
project=$(eagle_project_from_cwd "$cwd")
|
|
27
|
+
project_sql=$(eagle_sql_escape "$project")
|
|
28
|
+
|
|
29
|
+
# Skip short prompts — not enough signal for meaningful search
|
|
30
|
+
word_count=$(echo "$user_prompt" | wc -w | tr -d ' ')
|
|
31
|
+
[ "$word_count" -lt 3 ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Build FTS5 query from significant words (drop stop words, take first 6)
|
|
34
|
+
fts_query=$(echo "$user_prompt" | tr -cs '[:alnum:]' ' ' | tr '[:upper:]' '[:lower:]' | \
|
|
35
|
+
awk '{
|
|
36
|
+
split("the a an is are was were be been being have has had do does did will would shall should may might can could of in to for on with at by from", sw, " ")
|
|
37
|
+
for (i in sw) stop[sw[i]]=1
|
|
38
|
+
n=0
|
|
39
|
+
for (i=1; i<=NF && n<6; i++) {
|
|
40
|
+
if (length($i) > 2 && !($i in stop)) {
|
|
41
|
+
printf "%s%s", (n>0?" OR ":""), $i; n++
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}')
|
|
45
|
+
|
|
46
|
+
[ -z "$fts_query" ] && exit 0
|
|
47
|
+
|
|
48
|
+
fts_query_escaped=$(eagle_sql_escape "$fts_query")
|
|
49
|
+
|
|
50
|
+
# Search for relevant past summaries (cross-session)
|
|
51
|
+
results=$(eagle_db "SELECT s.request, s.learned, s.completed, s.created_at
|
|
52
|
+
FROM summaries s
|
|
53
|
+
JOIN summaries_fts f ON f.rowid = s.id
|
|
54
|
+
WHERE summaries_fts MATCH '$fts_query_escaped'
|
|
55
|
+
AND s.project = '$project_sql'
|
|
56
|
+
ORDER BY rank
|
|
57
|
+
LIMIT 3;")
|
|
58
|
+
|
|
59
|
+
context=""
|
|
60
|
+
|
|
61
|
+
if [ -n "$results" ]; then
|
|
62
|
+
context+="=== EAGLE MEM — Relevant Memory ===
|
|
63
|
+
"
|
|
64
|
+
while IFS='|' read -r req learned completed created_at; do
|
|
65
|
+
[ -z "$req" ] && [ -z "$completed" ] && continue
|
|
66
|
+
context+="[$created_at] "
|
|
67
|
+
[ -n "$req" ] && context+="$req"
|
|
68
|
+
[ -n "$completed" ] && context+=" → $completed"
|
|
69
|
+
[ -n "$learned" ] && context+=" (Learned: $learned)"
|
|
70
|
+
context+="
|
|
71
|
+
"
|
|
72
|
+
done <<< "$results"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Search indexed code chunks (if any exist for this project)
|
|
76
|
+
has_chunks=$(eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$project_sql' LIMIT 1;")
|
|
77
|
+
if [ "${has_chunks:-0}" -gt 0 ]; then
|
|
78
|
+
code_results=$(eagle_db "SELECT c.file_path, c.start_line, c.end_line, c.language
|
|
79
|
+
FROM code_chunks c
|
|
80
|
+
JOIN code_chunks_fts f ON f.rowid = c.id
|
|
81
|
+
WHERE code_chunks_fts MATCH '$fts_query_escaped'
|
|
82
|
+
AND c.project = '$project_sql'
|
|
83
|
+
ORDER BY rank
|
|
84
|
+
LIMIT 5;")
|
|
85
|
+
|
|
86
|
+
if [ -n "$code_results" ]; then
|
|
87
|
+
context+="=== EAGLE MEM — Relevant Code ===
|
|
88
|
+
"
|
|
89
|
+
while IFS='|' read -r fpath sline eline lang; do
|
|
90
|
+
[ -z "$fpath" ] && continue
|
|
91
|
+
context+="$fpath:${sline}-${eline}"
|
|
92
|
+
[ -n "$lang" ] && context+=" ($lang)"
|
|
93
|
+
context+="
|
|
94
|
+
"
|
|
95
|
+
done <<< "$code_results"
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
[ -z "$context" ] && exit 0
|
|
100
|
+
|
|
101
|
+
echo "$context"
|
|
102
|
+
exit 0
|
package/lib/common.sh
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Shared constants and helpers
|
|
4
|
+
# Source this file: . "$(dirname "$0")/../lib/common.sh"
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
EAGLE_MEM_DIR="${EAGLE_MEM_DIR:-$HOME/.eagle-mem}"
|
|
8
|
+
EAGLE_MEM_DB="$EAGLE_MEM_DIR/memory.db"
|
|
9
|
+
EAGLE_MEM_LOG="$EAGLE_MEM_DIR/eagle-mem.log"
|
|
10
|
+
|
|
11
|
+
eagle_log() {
|
|
12
|
+
local level="$1"
|
|
13
|
+
shift
|
|
14
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
eagle_project_from_cwd() {
|
|
18
|
+
local cwd="${1:-$(pwd)}"
|
|
19
|
+
basename "$cwd"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
eagle_sql_escape() {
|
|
23
|
+
printf '%s' "$1" | sed "s/'/''/g"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
eagle_read_stdin() {
|
|
27
|
+
local input=""
|
|
28
|
+
if [ ! -t 0 ]; then
|
|
29
|
+
input=$(cat)
|
|
30
|
+
fi
|
|
31
|
+
echo "$input"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Collect project files into a destination file.
|
|
35
|
+
# Uses git ls-files when available, falls back to find with common exclusions.
|
|
36
|
+
# Usage: eagle_collect_files <target_dir> <output_file>
|
|
37
|
+
eagle_collect_files() {
|
|
38
|
+
local target_dir="$1"
|
|
39
|
+
local output_file="$2"
|
|
40
|
+
|
|
41
|
+
if git -C "$target_dir" rev-parse --is-inside-work-tree &>/dev/null; then
|
|
42
|
+
git -C "$target_dir" ls-files --cached --others --exclude-standard > "$output_file"
|
|
43
|
+
else
|
|
44
|
+
(cd "$target_dir" && find . -type f \
|
|
45
|
+
-not -path '*/node_modules/*' \
|
|
46
|
+
-not -path '*/.git/*' \
|
|
47
|
+
-not -path '*/dist/*' \
|
|
48
|
+
-not -path '*/build/*' \
|
|
49
|
+
-not -path '*/.next/*' \
|
|
50
|
+
-not -path '*/target/*' \
|
|
51
|
+
-not -path '*/vendor/*' \
|
|
52
|
+
-not -path '*/__pycache__/*' \
|
|
53
|
+
-not -path '*/.venv/*' \
|
|
54
|
+
-not -path '*/venv/*' \
|
|
55
|
+
-not -path '*/.egg-info/*' \
|
|
56
|
+
-not -name '*.pyc' \
|
|
57
|
+
-not -name '*.lock' \
|
|
58
|
+
-not -name 'package-lock.json' \
|
|
59
|
+
-not -name 'yarn.lock' \
|
|
60
|
+
-not -name 'pnpm-lock.yaml' \
|
|
61
|
+
| sed 's|^\./||') > "$output_file"
|
|
62
|
+
fi
|
|
63
|
+
}
|
package/lib/db.sh
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Database helpers
|
|
4
|
+
# Source after common.sh: . "$(dirname "$0")/../lib/db.sh"
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
EAGLE_DB_SETUP=".headers off
|
|
8
|
+
.output /dev/null
|
|
9
|
+
PRAGMA journal_mode=WAL;
|
|
10
|
+
PRAGMA synchronous=NORMAL;
|
|
11
|
+
PRAGMA busy_timeout=5000;
|
|
12
|
+
PRAGMA foreign_keys=ON;
|
|
13
|
+
PRAGMA trusted_schema=ON;
|
|
14
|
+
.output stdout"
|
|
15
|
+
|
|
16
|
+
eagle_db() {
|
|
17
|
+
{ echo "$EAGLE_DB_SETUP"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>/dev/null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
eagle_db_pipe() {
|
|
21
|
+
{ echo "$EAGLE_DB_SETUP"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>/dev/null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
eagle_db_json() {
|
|
25
|
+
{ echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>/dev/null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
eagle_ensure_db() {
|
|
29
|
+
if [ ! -f "$EAGLE_MEM_DB" ]; then
|
|
30
|
+
local script_dir
|
|
31
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../db" && pwd)"
|
|
32
|
+
"$script_dir/migrate.sh"
|
|
33
|
+
fi
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
eagle_upsert_session() {
|
|
37
|
+
local session_id="$1"
|
|
38
|
+
local project; project=$(eagle_sql_escape "$2")
|
|
39
|
+
local cwd; cwd=$(eagle_sql_escape "${3:-}")
|
|
40
|
+
local model; model=$(eagle_sql_escape "${4:-}")
|
|
41
|
+
local source; source=$(eagle_sql_escape "${5:-}")
|
|
42
|
+
|
|
43
|
+
eagle_db "INSERT INTO sessions (id, project, cwd, model, source)
|
|
44
|
+
VALUES ('$session_id', '$project', '$cwd', '$model', '$source')
|
|
45
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
46
|
+
cwd = COALESCE(excluded.cwd, sessions.cwd),
|
|
47
|
+
model = COALESCE(excluded.model, sessions.model),
|
|
48
|
+
source = COALESCE(excluded.source, sessions.source);"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
eagle_end_session() {
|
|
52
|
+
local session_id="$1"
|
|
53
|
+
eagle_db "UPDATE sessions SET status = 'completed', ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = '$session_id';"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
eagle_insert_observation() {
|
|
57
|
+
local session_id="$1"
|
|
58
|
+
local project; project=$(eagle_sql_escape "$2")
|
|
59
|
+
local tool_name; tool_name=$(eagle_sql_escape "$3")
|
|
60
|
+
local tool_input_summary; tool_input_summary=$(eagle_sql_escape "$4")
|
|
61
|
+
local files_read="$5"
|
|
62
|
+
local files_modified="$6"
|
|
63
|
+
|
|
64
|
+
eagle_db "INSERT INTO observations (session_id, project, tool_name, tool_input_summary, files_read, files_modified)
|
|
65
|
+
VALUES ('$session_id', '$project', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified');"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
eagle_insert_summary() {
|
|
69
|
+
local session_id; session_id=$(eagle_sql_escape "$1")
|
|
70
|
+
local project; project=$(eagle_sql_escape "$2")
|
|
71
|
+
local request; request=$(eagle_sql_escape "$3")
|
|
72
|
+
local investigated; investigated=$(eagle_sql_escape "$4")
|
|
73
|
+
local learned; learned=$(eagle_sql_escape "$5")
|
|
74
|
+
local completed; completed=$(eagle_sql_escape "$6")
|
|
75
|
+
local next_steps; next_steps=$(eagle_sql_escape "$7")
|
|
76
|
+
local files_read; files_read=$(eagle_sql_escape "$8")
|
|
77
|
+
local files_modified; files_modified=$(eagle_sql_escape "$9")
|
|
78
|
+
local notes; notes=$(eagle_sql_escape "${10:-}")
|
|
79
|
+
|
|
80
|
+
eagle_db_pipe <<SQL
|
|
81
|
+
INSERT INTO summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_modified, notes)
|
|
82
|
+
VALUES (
|
|
83
|
+
'$session_id',
|
|
84
|
+
'$project',
|
|
85
|
+
'$request',
|
|
86
|
+
'$investigated',
|
|
87
|
+
'$learned',
|
|
88
|
+
'$completed',
|
|
89
|
+
'$next_steps',
|
|
90
|
+
'$files_read',
|
|
91
|
+
'$files_modified',
|
|
92
|
+
'$notes'
|
|
93
|
+
);
|
|
94
|
+
SQL
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
eagle_get_recent_summaries() {
|
|
98
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
99
|
+
local limit="${2:-5}"
|
|
100
|
+
|
|
101
|
+
eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at
|
|
102
|
+
FROM summaries s
|
|
103
|
+
WHERE s.project = '$project'
|
|
104
|
+
ORDER BY s.created_at DESC
|
|
105
|
+
LIMIT $limit;"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
eagle_search_summaries() {
|
|
109
|
+
local query; query=$(eagle_sql_escape "$1")
|
|
110
|
+
local project="${2:-}"
|
|
111
|
+
local limit="${3:-10}"
|
|
112
|
+
|
|
113
|
+
local where_clause=""
|
|
114
|
+
if [ -n "$project" ]; then
|
|
115
|
+
project=$(eagle_sql_escape "$project")
|
|
116
|
+
where_clause="AND s.project = '$project'"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.project
|
|
120
|
+
FROM summaries s
|
|
121
|
+
JOIN summaries_fts f ON f.rowid = s.id
|
|
122
|
+
WHERE summaries_fts MATCH '$query'
|
|
123
|
+
$where_clause
|
|
124
|
+
ORDER BY rank
|
|
125
|
+
LIMIT $limit;"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
eagle_get_pending_tasks() {
|
|
129
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
130
|
+
|
|
131
|
+
eagle_db "SELECT id, title, instructions, status, ordinal
|
|
132
|
+
FROM tasks
|
|
133
|
+
WHERE project = '$project' AND status IN ('pending', 'active')
|
|
134
|
+
ORDER BY ordinal ASC, id ASC;"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
eagle_get_next_task() {
|
|
138
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
139
|
+
|
|
140
|
+
eagle_db "SELECT id, title, instructions, context_snapshot
|
|
141
|
+
FROM tasks
|
|
142
|
+
WHERE project = '$project' AND status = 'pending'
|
|
143
|
+
ORDER BY ordinal ASC, id ASC
|
|
144
|
+
LIMIT 1;"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
eagle_get_active_files() {
|
|
148
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
149
|
+
local limit="${2:-20}"
|
|
150
|
+
|
|
151
|
+
eagle_db "SELECT json_each.value
|
|
152
|
+
FROM observations, json_each(observations.files_modified)
|
|
153
|
+
WHERE observations.project = '$project'
|
|
154
|
+
GROUP BY json_each.value
|
|
155
|
+
ORDER BY MAX(observations.created_at) DESC
|
|
156
|
+
LIMIT $limit;"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
eagle_observation_exists() {
|
|
160
|
+
local session_id="$1"
|
|
161
|
+
local tool_name; tool_name=$(eagle_sql_escape "$2")
|
|
162
|
+
local tool_summary; tool_summary=$(eagle_sql_escape "$3")
|
|
163
|
+
|
|
164
|
+
eagle_db "SELECT COUNT(*) FROM observations
|
|
165
|
+
WHERE session_id = '$session_id'
|
|
166
|
+
AND tool_name = '$tool_name'
|
|
167
|
+
AND tool_input_summary = '$tool_summary'
|
|
168
|
+
AND created_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-5 seconds');"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
eagle_upsert_overview() {
|
|
172
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
173
|
+
local content; content=$(eagle_sql_escape "$2")
|
|
174
|
+
|
|
175
|
+
eagle_db "INSERT INTO overviews (project, content, updated_at)
|
|
176
|
+
VALUES ('$project', '$content', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
177
|
+
ON CONFLICT(project) DO UPDATE SET
|
|
178
|
+
content = excluded.content,
|
|
179
|
+
updated_at = excluded.updated_at;"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
eagle_get_overview() {
|
|
183
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
184
|
+
|
|
185
|
+
eagle_db "SELECT content FROM overviews WHERE project = '$project';"
|
|
186
|
+
}
|