eagle-mem 1.1.0 → 1.3.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/bin/eagle-mem CHANGED
@@ -24,6 +24,8 @@ case "$command" in
24
24
  search) bash "$SCRIPTS_DIR/search.sh" "$@" ;;
25
25
  tasks) bash "$SCRIPTS_DIR/tasks.sh" "$@" ;;
26
26
  overview) bash "$SCRIPTS_DIR/overview.sh" "$@" ;;
27
+ prune) bash "$SCRIPTS_DIR/prune.sh" "$@" ;;
28
+ memories) bash "$SCRIPTS_DIR/memories.sh" "$@" ;;
27
29
  help|--help|-h)
28
30
  bash "$SCRIPTS_DIR/help.sh" ;;
29
31
  version|--version|-v|-V)
@@ -0,0 +1,9 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Eagle Mem — Migration 004: Observation indexes
3
+ -- Adds created_at index for efficient pruning and time-bound queries
4
+ -- ═══════════════════════════════════════════════════════════
5
+
6
+ PRAGMA foreign_keys = ON;
7
+
8
+ CREATE INDEX IF NOT EXISTS idx_observations_created_at ON observations(created_at);
9
+ CREATE INDEX IF NOT EXISTS idx_sessions_ended_at ON sessions(status, ended_at);
@@ -0,0 +1,49 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Eagle Mem — Migration 005: Claude Code memory mirror
3
+ -- Captures Claude Code auto-memory writes into Eagle Mem
4
+ -- ═══════════════════════════════════════════════════════════
5
+
6
+ PRAGMA foreign_keys = ON;
7
+
8
+ CREATE TABLE IF NOT EXISTS claude_memories (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ project TEXT NOT NULL,
11
+ file_path TEXT NOT NULL UNIQUE,
12
+ memory_name TEXT,
13
+ description TEXT,
14
+ memory_type TEXT,
15
+ content TEXT,
16
+ content_hash TEXT,
17
+ origin_session_id TEXT,
18
+ captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
19
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
20
+ );
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_claude_memories_project ON claude_memories(project);
23
+ CREATE INDEX IF NOT EXISTS idx_claude_memories_type ON claude_memories(memory_type);
24
+
25
+ -- FTS5 for searching across memory content
26
+ CREATE VIRTUAL TABLE IF NOT EXISTS claude_memories_fts USING fts5(
27
+ memory_name,
28
+ description,
29
+ content,
30
+ content='claude_memories',
31
+ content_rowid='id'
32
+ );
33
+
34
+ CREATE TRIGGER IF NOT EXISTS claude_memories_ai AFTER INSERT ON claude_memories BEGIN
35
+ INSERT INTO claude_memories_fts(rowid, memory_name, description, content)
36
+ VALUES (new.id, new.memory_name, new.description, new.content);
37
+ END;
38
+
39
+ CREATE TRIGGER IF NOT EXISTS claude_memories_ad AFTER DELETE ON claude_memories BEGIN
40
+ INSERT INTO claude_memories_fts(claude_memories_fts, rowid, memory_name, description, content)
41
+ VALUES ('delete', old.id, old.memory_name, old.description, old.content);
42
+ END;
43
+
44
+ CREATE TRIGGER IF NOT EXISTS claude_memories_au AFTER UPDATE ON claude_memories BEGIN
45
+ INSERT INTO claude_memories_fts(claude_memories_fts, rowid, memory_name, description, content)
46
+ VALUES ('delete', old.id, old.memory_name, old.description, old.content);
47
+ INSERT INTO claude_memories_fts(rowid, memory_name, description, content)
48
+ VALUES (new.id, new.memory_name, new.description, new.content);
49
+ END;
@@ -0,0 +1,45 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Eagle Mem — Migration 006: Claude Code plan mirror
3
+ -- Captures Claude Code plan documents into Eagle Mem
4
+ -- ═══════════════════════════════════════════════════════════
5
+
6
+ PRAGMA foreign_keys = ON;
7
+
8
+ CREATE TABLE IF NOT EXISTS claude_plans (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ project TEXT NOT NULL DEFAULT '',
11
+ file_path TEXT NOT NULL UNIQUE,
12
+ title TEXT,
13
+ content TEXT,
14
+ content_hash TEXT,
15
+ origin_session_id TEXT,
16
+ captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
17
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
18
+ );
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_claude_plans_project ON claude_plans(project);
21
+
22
+ -- FTS5 for searching across plan content
23
+ CREATE VIRTUAL TABLE IF NOT EXISTS claude_plans_fts USING fts5(
24
+ title,
25
+ content,
26
+ content='claude_plans',
27
+ content_rowid='id'
28
+ );
29
+
30
+ CREATE TRIGGER IF NOT EXISTS claude_plans_ai AFTER INSERT ON claude_plans BEGIN
31
+ INSERT INTO claude_plans_fts(rowid, title, content)
32
+ VALUES (new.id, new.title, new.content);
33
+ END;
34
+
35
+ CREATE TRIGGER IF NOT EXISTS claude_plans_ad AFTER DELETE ON claude_plans BEGIN
36
+ INSERT INTO claude_plans_fts(claude_plans_fts, rowid, title, content)
37
+ VALUES ('delete', old.id, old.title, old.content);
38
+ END;
39
+
40
+ CREATE TRIGGER IF NOT EXISTS claude_plans_au AFTER UPDATE ON claude_plans BEGIN
41
+ INSERT INTO claude_plans_fts(claude_plans_fts, rowid, title, content)
42
+ VALUES ('delete', old.id, old.title, old.content);
43
+ INSERT INTO claude_plans_fts(rowid, title, content)
44
+ VALUES (new.id, new.title, new.content);
45
+ END;
@@ -0,0 +1,50 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Migration 007: Claude Code task mirror
3
+ -- Mirrors TaskCreate/TaskUpdate artifacts from ~/.claude/tasks/
4
+ -- ═══════════════════════════════════════════════════════════
5
+
6
+ CREATE TABLE IF NOT EXISTS claude_tasks (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ project TEXT NOT NULL DEFAULT '',
9
+ source_session_id TEXT NOT NULL,
10
+ source_task_id TEXT NOT NULL,
11
+ file_path TEXT UNIQUE,
12
+ subject TEXT,
13
+ description TEXT,
14
+ active_form TEXT,
15
+ status TEXT NOT NULL DEFAULT 'pending',
16
+ blocks TEXT DEFAULT '[]',
17
+ blocked_by TEXT DEFAULT '[]',
18
+ content_hash TEXT,
19
+ captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_project ON claude_tasks(project);
24
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_session ON claude_tasks(source_session_id);
25
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_status ON claude_tasks(status);
26
+
27
+ -- FTS5 for full-text search on subject + description
28
+ CREATE VIRTUAL TABLE IF NOT EXISTS claude_tasks_fts USING fts5(
29
+ subject,
30
+ description,
31
+ content='claude_tasks',
32
+ content_rowid='id'
33
+ );
34
+
35
+ CREATE TRIGGER IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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;
package/db/migrate.sh CHANGED
@@ -44,4 +44,16 @@ run_migration "002_overviews" "$SCRIPT_DIR/002_overviews.sql"
44
44
  # ─── Migration 003: Code chunks ───────────────────────────
45
45
  run_migration "003_code_chunks" "$SCRIPT_DIR/003_code_chunks.sql"
46
46
 
47
+ # ─── Migration 004: Observation indexes ──────────────────
48
+ run_migration "004_observation_indexes" "$SCRIPT_DIR/004_observation_indexes.sql"
49
+
50
+ # ─── Migration 005: Claude Code memory mirror ────────────
51
+ run_migration "005_claude_memories" "$SCRIPT_DIR/005_claude_memories.sql"
52
+
53
+ # ─── Migration 006: Claude Code plan mirror ──────────────
54
+ run_migration "006_claude_plans" "$SCRIPT_DIR/006_claude_plans.sql"
55
+
56
+ # ─── Migration 007: Claude Code task mirror ──────────────
57
+ run_migration "007_claude_tasks" "$SCRIPT_DIR/007_claude_tasks.sql"
58
+
47
59
  echo " Eagle Mem database ready: $DB"
package/db/schema.sql CHANGED
@@ -20,6 +20,8 @@ CREATE TABLE IF NOT EXISTS _migrations (
20
20
  );
21
21
 
22
22
  -- ─── Sessions ──────────────────────────────────────────────
23
+ -- Sessions are parent rows for summaries, tasks, and observations (FK).
24
+ -- Never delete sessions — prune children instead.
23
25
 
24
26
  CREATE TABLE IF NOT EXISTS sessions (
25
27
  id TEXT PRIMARY KEY,
@@ -19,11 +19,11 @@ session_id=$(echo "$input" | jq -r '.session_id // empty')
19
19
  cwd=$(echo "$input" | jq -r '.cwd // empty')
20
20
  tool_name=$(echo "$input" | jq -r '.tool_name // empty')
21
21
 
22
- [ -z "$session_id" ] || [ -z "$tool_name" ] && exit 0
22
+ if [ -z "$session_id" ] || [ -z "$tool_name" ]; then exit 0; fi
23
23
 
24
- # Only track file-related tools
24
+ # Only track relevant tools
25
25
  case "$tool_name" in
26
- Read|Write|Edit|Bash) ;;
26
+ Read|Write|Edit|Bash|TaskCreate|TaskUpdate) ;;
27
27
  *) exit 0 ;;
28
28
  esac
29
29
 
@@ -38,17 +38,17 @@ tool_summary=""
38
38
  case "$tool_name" in
39
39
  Read)
40
40
  fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
41
- [ -n "$fp" ] && files_read="[\"$fp\"]"
41
+ [ -n "$fp" ] && files_read=$(printf '%s' "$fp" | jq -Rsc '[.]')
42
42
  tool_summary="Read $fp"
43
43
  ;;
44
44
  Write)
45
45
  fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
46
- [ -n "$fp" ] && files_modified="[\"$fp\"]"
46
+ [ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
47
47
  tool_summary="Write $fp"
48
48
  ;;
49
49
  Edit)
50
50
  fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
51
- [ -n "$fp" ] && files_modified="[\"$fp\"]"
51
+ [ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
52
52
  tool_summary="Edit $fp"
53
53
  ;;
54
54
  Bash)
@@ -63,6 +63,50 @@ case "$tool_name" in
63
63
  -e 's/(Authorization: )[^ ]*/\1[REDACTED]/gi')
64
64
  tool_summary="Bash: $cmd"
65
65
  ;;
66
+ TaskCreate|TaskUpdate)
67
+ task_subject=$(echo "$input" | jq -r '.tool_input.subject // empty')
68
+ tool_summary="$tool_name: $task_subject"
69
+ ;;
70
+ esac
71
+
72
+ # ─── Claude memory + plan mirror ─────────────────────────
73
+ # Intercept writes to Claude Code's auto-memory and plan files
74
+ case "$tool_name" in
75
+ Write|Edit)
76
+ if [ -n "$fp" ]; then
77
+ case "$fp" in
78
+ "$HOME/.claude/projects"/*/memory/*.md)
79
+ mem_base=$(basename "$fp")
80
+ if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
81
+ eagle_capture_claude_memory "$fp" "$session_id" "$project"
82
+ fi
83
+ ;;
84
+ "$HOME/.claude/plans/"*.md)
85
+ if [ -f "$fp" ]; then
86
+ eagle_capture_claude_plan "$fp" "$session_id" "$project"
87
+ fi
88
+ ;;
89
+ esac
90
+ fi
91
+ ;;
92
+ esac
93
+
94
+ # ─── Claude task mirror ─────────────────────────────────
95
+ # Intercept TaskCreate/TaskUpdate and capture the resulting JSON files
96
+ case "$tool_name" in
97
+ TaskCreate|TaskUpdate)
98
+ task_dir="$HOME/.claude/tasks/$session_id"
99
+ if [ -d "$task_dir" ]; then
100
+ task_id=$(echo "$input" | jq -r '.tool_input.id // empty')
101
+ if [ -z "$task_id" ]; then
102
+ newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
103
+ [ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
104
+ else
105
+ task_json="$task_dir/$task_id.json"
106
+ [ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
107
+ fi
108
+ fi
109
+ ;;
66
110
  esac
67
111
 
68
112
  # Deduplicate: skip if exact same observation within last 5 seconds
@@ -25,7 +25,6 @@ model=$(echo "$input" | jq -r '.model // empty')
25
25
  [ -z "$session_id" ] && exit 0
26
26
 
27
27
  project=$(eagle_project_from_cwd "$cwd")
28
- project_sql=$(eagle_sql_escape "$project")
29
28
 
30
29
  eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
31
30
 
@@ -48,13 +47,7 @@ $overview
48
47
  fi
49
48
 
50
49
  # Recent summaries for this project (last 5 sessions)
51
- recent=$(eagle_db "
52
- SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at
53
- FROM summaries s
54
- WHERE s.project = '$project_sql'
55
- ORDER BY s.created_at DESC
56
- LIMIT 5;
57
- ")
50
+ recent=$(eagle_get_recent_summaries "$project" 5)
58
51
 
59
52
  if [ -n "$recent" ]; then
60
53
  context+="=== EAGLE MEM ===
@@ -78,13 +71,7 @@ Next steps: $next_steps"
78
71
  fi
79
72
 
80
73
  # Pending tasks from TaskAware loop
81
- pending_tasks=$(eagle_db "
82
- SELECT id, title, instructions, status
83
- FROM tasks
84
- WHERE project = '$project_sql' AND status IN ('pending', 'active')
85
- ORDER BY ordinal ASC, id ASC
86
- LIMIT 10;
87
- ")
74
+ pending_tasks=$(eagle_get_pending_tasks "$project")
88
75
 
89
76
  if [ -n "$pending_tasks" ]; then
90
77
  context+="
@@ -92,7 +79,7 @@ if [ -n "$pending_tasks" ]; then
92
79
  Pending tasks for '$project':
93
80
  "
94
81
  first_pending=""
95
- while IFS='|' read -r tid title instructions status; do
82
+ while IFS='|' read -r tid title instructions status _ordinal; do
96
83
  [ -z "$tid" ] && continue
97
84
  local_marker=""
98
85
  if [ "$status" = "active" ]; then
@@ -108,22 +95,12 @@ Pending tasks for '$project':
108
95
  done <<< "$pending_tasks"
109
96
 
110
97
  # Load context snapshot for the active/next task
111
- active_task=$(eagle_db "
112
- SELECT id, title, instructions, context_snapshot
113
- FROM tasks
114
- WHERE project = '$project_sql' AND status = 'active'
115
- ORDER BY ordinal ASC, id ASC
116
- LIMIT 1;
117
- ")
98
+ active_task=$(eagle_get_active_task "$project")
118
99
 
119
100
  if [ -z "$active_task" ] && [ -n "$first_pending" ]; then
120
101
  # Auto-activate the next pending task
121
- eagle_db "UPDATE tasks SET status = 'active', started_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = $first_pending;"
122
- active_task=$(eagle_db "
123
- SELECT id, title, instructions, context_snapshot
124
- FROM tasks
125
- WHERE id = $first_pending;
126
- ")
102
+ eagle_activate_task "$first_pending"
103
+ active_task=$(eagle_get_active_task "$project")
127
104
  fi
128
105
 
129
106
  if [ -n "$active_task" ]; then
package/hooks/stop.sh CHANGED
@@ -29,7 +29,6 @@ agent_type=$(echo "$input" | jq -r '.agent_type // empty')
29
29
  [ -n "$agent_type" ] && [ "$agent_type" != "main" ] && exit 0
30
30
 
31
31
  project=$(eagle_project_from_cwd "$cwd")
32
- project_sql=$(eagle_sql_escape "$project")
33
32
 
34
33
  eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path"
35
34
 
@@ -137,10 +136,9 @@ fi
137
136
 
138
137
  # Mark active task as done if eagle-summary mentions completion
139
138
  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"
139
+ completed_task_id=$(eagle_complete_active_task "$project")
140
+ if [ -n "$completed_task_id" ]; then
141
+ eagle_log "INFO" "Stop: marked task #$completed_task_id as done"
144
142
  fi
145
143
  fi
146
144
 
@@ -24,7 +24,6 @@ user_prompt=$(echo "$input" | jq -r '.prompt // empty')
24
24
  [ -z "$user_prompt" ] && exit 0
25
25
 
26
26
  project=$(eagle_project_from_cwd "$cwd")
27
- project_sql=$(eagle_sql_escape "$project")
28
27
 
29
28
  # Skip short prompts — not enough signal for meaningful search
30
29
  word_count=$(echo "$user_prompt" | wc -w | tr -d ' ')
@@ -45,23 +44,15 @@ fts_query=$(echo "$user_prompt" | tr -cs '[:alnum:]' ' ' | tr '[:upper:]' '[:low
45
44
 
46
45
  [ -z "$fts_query" ] && exit 0
47
46
 
48
- fts_query_escaped=$(eagle_sql_escape "$fts_query")
49
-
50
47
  # 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;")
48
+ results=$(eagle_search_summaries "$fts_query" "$project" 3)
58
49
 
59
50
  context=""
60
51
 
61
52
  if [ -n "$results" ]; then
62
53
  context+="=== EAGLE MEM — Relevant Memory ===
63
54
  "
64
- while IFS='|' read -r req learned completed created_at; do
55
+ while IFS='|' read -r req completed learned _next_steps created_at _proj; do
65
56
  [ -z "$req" ] && [ -z "$completed" ] && continue
66
57
  context+="[$created_at] "
67
58
  [ -n "$req" ] && context+="$req"
@@ -73,15 +64,9 @@ if [ -n "$results" ]; then
73
64
  fi
74
65
 
75
66
  # 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;")
67
+ has_chunks=$(eagle_count_code_chunks "$project")
77
68
  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;")
69
+ code_results=$(eagle_search_code_chunks "$fts_query" "$project" 5)
85
70
 
86
71
  if [ -n "$code_results" ]; then
87
72
  context+="=== EAGLE MEM — Relevant Code ===
package/lib/common.sh CHANGED
@@ -7,6 +7,8 @@
7
7
  EAGLE_MEM_DIR="${EAGLE_MEM_DIR:-$HOME/.eagle-mem}"
8
8
  EAGLE_MEM_DB="$EAGLE_MEM_DIR/memory.db"
9
9
  EAGLE_MEM_LOG="$EAGLE_MEM_DIR/eagle-mem.log"
10
+ EAGLE_SETTINGS="${EAGLE_SETTINGS:-$HOME/.claude/settings.json}"
11
+ EAGLE_SKILLS_DIR="$HOME/.claude/skills"
10
12
 
11
13
  eagle_log() {
12
14
  local level="$1"
@@ -23,6 +25,17 @@ eagle_sql_escape() {
23
25
  printf '%s' "$1" | sed "s/'/''/g"
24
26
  }
25
27
 
28
+ eagle_sql_int() {
29
+ case "$1" in
30
+ ''|*[!0-9]*) echo "0" ;;
31
+ *) printf '%s' "$1" ;;
32
+ esac
33
+ }
34
+
35
+ eagle_fts_sanitize() {
36
+ printf '%s' "$1" | sed 's/[*"(){}^~:]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
37
+ }
38
+
26
39
  eagle_read_stdin() {
27
40
  local input=""
28
41
  if [ ! -t 0 ]; then