eagle-mem 4.9.1 → 4.9.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/README.md CHANGED
@@ -152,6 +152,12 @@ Eagle Mem prevents Claude from repeating past mistakes:
152
152
  | `eagle-mem scan` | Scan codebase and generate overview |
153
153
  | `eagle-mem index` | Index source files for FTS5 code search |
154
154
 
155
+ ### v4.9.2 Patch
156
+
157
+ Nested-repo Claude Code projects now use one stable project key. When a Claude workspace contains a git repo subdirectory, hooks prefer the Claude transcript workspace root while repo-local CLI commands can still use git-root keys where appropriate. Memory sync and backfill also repair unchanged memory rows whose content hash stayed the same but whose project key was stale. FTS5 update triggers now ignore metadata-only project rekeys, avoiding SQLite virtual-table errors during safe repairs.
158
+
159
+ Installer parity also improved: first-time install now auto-provisions RTK when Cargo is available, the Eagle Mem statusline shows version/session/memory/turn counts, `eagle-mem statusline` is available as a CLI command, and Codex instructions explicitly call out that Codex currently has hook recall plus the statusline command rather than Claude Code's persistent custom statusline UI.
160
+
155
161
  ### v4.9.1 Patch
156
162
 
157
163
  `eagle-mem updates status` now refreshes the npm version live, and install/update seed the local latest-version cache with the installed version. This avoids confusing status output immediately after an update.
package/bin/eagle-mem CHANGED
@@ -23,6 +23,7 @@ case "$command" in
23
23
  health) bash "$SCRIPTS_DIR/health.sh" "$@" ;;
24
24
  config) bash "$SCRIPTS_DIR/config.sh" "$@" ;;
25
25
  updates) bash "$SCRIPTS_DIR/updates.sh" "$@" ;;
26
+ statusline) "$SCRIPTS_DIR/statusline-em.sh" "$@" ;;
26
27
  guard) bash "$SCRIPTS_DIR/guard.sh" "$@" ;;
27
28
  overview) bash "$SCRIPTS_DIR/overview.sh" "$@" ;;
28
29
  session|sessions)
@@ -38,7 +38,9 @@ CREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON code_chunks BEGIN
38
38
  VALUES ('delete', old.id, old.file_path, old.content);
39
39
  END;
40
40
 
41
- CREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON code_chunks BEGIN
41
+ CREATE TRIGGER IF NOT EXISTS chunks_au
42
+ AFTER UPDATE OF file_path, content ON code_chunks
43
+ BEGIN
42
44
  INSERT INTO code_chunks_fts(code_chunks_fts, rowid, file_path, content)
43
45
  VALUES ('delete', old.id, old.file_path, old.content);
44
46
  INSERT INTO code_chunks_fts(rowid, file_path, content)
@@ -41,7 +41,9 @@ CREATE TRIGGER IF NOT EXISTS claude_memories_ad AFTER DELETE ON claude_memories
41
41
  VALUES ('delete', old.id, old.memory_name, old.description, old.content);
42
42
  END;
43
43
 
44
- CREATE TRIGGER IF NOT EXISTS claude_memories_au AFTER UPDATE ON claude_memories BEGIN
44
+ CREATE TRIGGER IF NOT EXISTS claude_memories_au
45
+ AFTER UPDATE OF memory_name, description, content ON claude_memories
46
+ BEGIN
45
47
  INSERT INTO claude_memories_fts(claude_memories_fts, rowid, memory_name, description, content)
46
48
  VALUES ('delete', old.id, old.memory_name, old.description, old.content);
47
49
  INSERT INTO claude_memories_fts(rowid, memory_name, description, content)
@@ -37,7 +37,9 @@ CREATE TRIGGER IF NOT EXISTS claude_plans_ad AFTER DELETE ON claude_plans BEGIN
37
37
  VALUES ('delete', old.id, old.title, old.content);
38
38
  END;
39
39
 
40
- CREATE TRIGGER IF NOT EXISTS claude_plans_au AFTER UPDATE ON claude_plans BEGIN
40
+ CREATE TRIGGER IF NOT EXISTS claude_plans_au
41
+ AFTER UPDATE OF title, content ON claude_plans
42
+ BEGIN
41
43
  INSERT INTO claude_plans_fts(claude_plans_fts, rowid, title, content)
42
44
  VALUES ('delete', old.id, old.title, old.content);
43
45
  INSERT INTO claude_plans_fts(rowid, title, content)
@@ -42,7 +42,9 @@ CREATE TRIGGER IF NOT EXISTS claude_tasks_ad AFTER DELETE ON claude_tasks BEGIN
42
42
  VALUES ('delete', old.id, old.subject, old.description);
43
43
  END;
44
44
 
45
- CREATE TRIGGER IF NOT EXISTS claude_tasks_au AFTER UPDATE ON claude_tasks BEGIN
45
+ CREATE TRIGGER IF NOT EXISTS claude_tasks_au
46
+ AFTER UPDATE OF subject, description ON claude_tasks
47
+ BEGIN
46
48
  INSERT INTO claude_tasks_fts(claude_tasks_fts, rowid, subject, description)
47
49
  VALUES ('delete', old.id, old.subject, old.description);
48
50
  INSERT INTO claude_tasks_fts(rowid, subject, description)
@@ -49,7 +49,9 @@ CREATE TRIGGER summaries_ad AFTER DELETE ON summaries BEGIN
49
49
  VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
50
50
  END;
51
51
 
52
- CREATE TRIGGER summaries_au AFTER UPDATE ON summaries BEGIN
52
+ CREATE TRIGGER summaries_au
53
+ AFTER UPDATE OF request, investigated, learned, completed, next_steps, notes ON summaries
54
+ BEGIN
53
55
  INSERT INTO summaries_fts(summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
54
56
  VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
55
57
  INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
@@ -43,7 +43,9 @@ CREATE TRIGGER summaries_ad AFTER DELETE ON summaries BEGIN
43
43
  VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes, old.decisions, old.gotchas, old.key_files);
44
44
  END;
45
45
 
46
- CREATE TRIGGER summaries_au AFTER UPDATE ON summaries BEGIN
46
+ CREATE TRIGGER summaries_au
47
+ AFTER UPDATE OF request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files ON summaries
48
+ BEGIN
47
49
  INSERT INTO summaries_fts(summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
48
50
  VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes, old.decisions, old.gotchas, old.key_files);
49
51
  INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
@@ -78,7 +78,9 @@ CREATE TRIGGER IF NOT EXISTS agent_memories_ad AFTER DELETE ON agent_memories BE
78
78
  VALUES ('delete', old.id, old.memory_name, old.description, old.content);
79
79
  END;
80
80
 
81
- CREATE TRIGGER IF NOT EXISTS agent_memories_au AFTER UPDATE ON agent_memories BEGIN
81
+ CREATE TRIGGER IF NOT EXISTS agent_memories_au
82
+ AFTER UPDATE OF memory_name, description, content ON agent_memories
83
+ BEGIN
82
84
  INSERT INTO agent_memories_fts(agent_memories_fts, rowid, memory_name, description, content)
83
85
  VALUES ('delete', old.id, old.memory_name, old.description, old.content);
84
86
  INSERT INTO agent_memories_fts(rowid, memory_name, description, content)
@@ -95,7 +97,9 @@ CREATE TRIGGER IF NOT EXISTS agent_plans_ad AFTER DELETE ON agent_plans BEGIN
95
97
  VALUES ('delete', old.id, old.title, old.content);
96
98
  END;
97
99
 
98
- CREATE TRIGGER IF NOT EXISTS agent_plans_au AFTER UPDATE ON agent_plans BEGIN
100
+ CREATE TRIGGER IF NOT EXISTS agent_plans_au
101
+ AFTER UPDATE OF title, content ON agent_plans
102
+ BEGIN
99
103
  INSERT INTO agent_plans_fts(agent_plans_fts, rowid, title, content)
100
104
  VALUES ('delete', old.id, old.title, old.content);
101
105
  INSERT INTO agent_plans_fts(rowid, title, content)
@@ -112,7 +116,9 @@ CREATE TRIGGER IF NOT EXISTS agent_tasks_ad AFTER DELETE ON agent_tasks BEGIN
112
116
  VALUES ('delete', old.id, old.subject, old.description);
113
117
  END;
114
118
 
115
- CREATE TRIGGER IF NOT EXISTS agent_tasks_au AFTER UPDATE ON agent_tasks BEGIN
119
+ CREATE TRIGGER IF NOT EXISTS agent_tasks_au
120
+ AFTER UPDATE OF subject, description ON agent_tasks
121
+ BEGIN
116
122
  INSERT INTO agent_tasks_fts(agent_tasks_fts, rowid, subject, description)
117
123
  VALUES ('delete', old.id, old.subject, old.description);
118
124
  INSERT INTO agent_tasks_fts(rowid, subject, description)
@@ -0,0 +1,57 @@
1
+ -- Migration 033: Make FTS triggers safe for project-key repairs.
2
+ --
3
+ -- SQLite FTS5 external-content tables can throw unsafe virtual-table errors
4
+ -- when UPDATE triggers try to delete/reinsert FTS rows during metadata-only
5
+ -- repairs. Project rekeys do not change searchable text, so the FTS UPDATE
6
+ -- triggers should only run when indexed columns change.
7
+
8
+ DROP TRIGGER IF EXISTS summaries_au;
9
+ DROP TRIGGER IF EXISTS chunks_au;
10
+ DROP TRIGGER IF EXISTS agent_memories_au;
11
+ DROP TRIGGER IF EXISTS agent_plans_au;
12
+ DROP TRIGGER IF EXISTS agent_tasks_au;
13
+
14
+ CREATE TRIGGER summaries_au
15
+ AFTER UPDATE OF request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files ON summaries
16
+ BEGIN
17
+ INSERT INTO summaries_fts(summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
18
+ VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes, old.decisions, old.gotchas, old.key_files);
19
+ INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes, decisions, gotchas, key_files)
20
+ VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes, new.decisions, new.gotchas, new.key_files);
21
+ END;
22
+
23
+ CREATE TRIGGER chunks_au
24
+ AFTER UPDATE OF file_path, content ON code_chunks
25
+ BEGIN
26
+ INSERT INTO code_chunks_fts(code_chunks_fts, rowid, file_path, content)
27
+ VALUES ('delete', old.id, old.file_path, old.content);
28
+ INSERT INTO code_chunks_fts(rowid, file_path, content)
29
+ VALUES (new.id, new.file_path, new.content);
30
+ END;
31
+
32
+ CREATE TRIGGER agent_memories_au
33
+ AFTER UPDATE OF memory_name, description, content ON agent_memories
34
+ BEGIN
35
+ INSERT INTO agent_memories_fts(agent_memories_fts, rowid, memory_name, description, content)
36
+ VALUES ('delete', old.id, old.memory_name, old.description, old.content);
37
+ INSERT INTO agent_memories_fts(rowid, memory_name, description, content)
38
+ VALUES (new.id, new.memory_name, new.description, new.content);
39
+ END;
40
+
41
+ CREATE TRIGGER agent_plans_au
42
+ AFTER UPDATE OF title, content ON agent_plans
43
+ BEGIN
44
+ INSERT INTO agent_plans_fts(agent_plans_fts, rowid, title, content)
45
+ VALUES ('delete', old.id, old.title, old.content);
46
+ INSERT INTO agent_plans_fts(rowid, title, content)
47
+ VALUES (new.id, new.title, new.content);
48
+ END;
49
+
50
+ CREATE TRIGGER agent_tasks_au
51
+ AFTER UPDATE OF subject, description ON agent_tasks
52
+ BEGIN
53
+ INSERT INTO agent_tasks_fts(agent_tasks_fts, rowid, subject, description)
54
+ VALUES ('delete', old.id, old.subject, old.description);
55
+ INSERT INTO agent_tasks_fts(rowid, subject, description)
56
+ VALUES (new.id, new.subject, new.description);
57
+ END;
package/db/schema.sql CHANGED
@@ -99,7 +99,9 @@ CREATE TRIGGER IF NOT EXISTS summaries_ad AFTER DELETE ON summaries BEGIN
99
99
  VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
100
100
  END;
101
101
 
102
- CREATE TRIGGER IF NOT EXISTS summaries_au AFTER UPDATE ON summaries BEGIN
102
+ CREATE TRIGGER IF NOT EXISTS summaries_au
103
+ AFTER UPDATE OF request, investigated, learned, completed, next_steps, notes ON summaries
104
+ BEGIN
103
105
  INSERT INTO summaries_fts(summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
104
106
  VALUES ('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
105
107
  INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
@@ -30,7 +30,7 @@ if [ -z "$session_id" ]; then exit 0; fi
30
30
  case "$hook_event" in
31
31
  TaskCreated|TaskCompleted)
32
32
  [ ! -f "$EAGLE_MEM_DB" ] && exit 0
33
- project=$(eagle_project_from_cwd "$cwd")
33
+ project=$(eagle_project_from_hook_input "$input")
34
34
  [ -z "$project" ] && exit 0
35
35
  eagle_upsert_session "$session_id" "$project" "$cwd" "" "" "$agent"
36
36
 
@@ -79,7 +79,7 @@ esac
79
79
 
80
80
  [ ! -f "$EAGLE_MEM_DB" ] && exit 0
81
81
 
82
- project=$(eagle_project_from_cwd "$cwd")
82
+ project=$(eagle_project_from_hook_input "$input")
83
83
  [ -z "$project" ] && exit 0
84
84
 
85
85
  # Ensure session row exists before inserting observations (FK constraint).
@@ -33,7 +33,7 @@ esac
33
33
 
34
34
  session_id=$(echo "$input" | jq -r '.session_id // empty')
35
35
  cwd=$(echo "$input" | jq -r '.cwd // empty')
36
- project=$(eagle_project_from_cwd "$cwd")
36
+ project=$(eagle_project_from_hook_input "$input")
37
37
  [ -z "$project" ] && exit 0
38
38
 
39
39
  context=""
@@ -22,7 +22,7 @@ agent=$(eagle_agent_source_from_json "$input")
22
22
  [ ! -f "$EAGLE_MEM_DB" ] && exit 0
23
23
 
24
24
  cwd=$(echo "$input" | jq -r '.cwd // empty')
25
- project=$(eagle_project_from_cwd "$cwd")
25
+ project=$(eagle_project_from_hook_input "$input")
26
26
  [ -z "$project" ] && exit 0
27
27
 
28
28
  # Final sweep: re-capture all task files to catch status changes
@@ -33,7 +33,7 @@ codex_compact=0
33
33
 
34
34
  [ -z "$session_id" ] && exit 0
35
35
 
36
- project=$(eagle_project_from_cwd "$cwd")
36
+ project=$(eagle_project_from_hook_input "$input")
37
37
  [ -z "$project" ] && exit 0
38
38
 
39
39
  p_esc=$(eagle_sql_escape "$project")
package/hooks/stop.sh CHANGED
@@ -33,7 +33,7 @@ agent=$(eagle_agent_source_from_json "$input")
33
33
  agent_type=$(echo "$input" | jq -r '.agent_type // empty')
34
34
  [ -n "$agent_type" ] && [ "$agent_type" != "main" ] && exit 0
35
35
 
36
- project=$(eagle_project_from_cwd "$cwd")
36
+ project=$(eagle_project_from_hook_input "$input")
37
37
  [ -z "$project" ] && exit 0
38
38
 
39
39
  eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcript_path agent=$agent"
@@ -25,7 +25,7 @@ agent=$(eagle_agent_source_from_json "$input")
25
25
 
26
26
  [ -z "$user_prompt" ] && exit 0
27
27
 
28
- project=$(eagle_project_from_cwd "$cwd")
28
+ project=$(eagle_project_from_hook_input "$input")
29
29
  codex_compact=0
30
30
  [ "$agent" = "codex" ] && codex_compact=1
31
31
 
package/lib/common.sh CHANGED
@@ -8,10 +8,10 @@ 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
10
  EAGLE_SETTINGS="${EAGLE_SETTINGS:-$HOME/.claude/settings.json}"
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"
11
+ EAGLE_SKILLS_DIR="${EAGLE_SKILLS_DIR:-$HOME/.claude/skills}"
12
+ EAGLE_CLAUDE_PROJECTS_DIR="${EAGLE_CLAUDE_PROJECTS_DIR:-$HOME/.claude/projects}"
13
+ EAGLE_CLAUDE_PLANS_DIR="${EAGLE_CLAUDE_PLANS_DIR:-$HOME/.claude/plans}"
14
+ EAGLE_CLAUDE_TASKS_DIR="${EAGLE_CLAUDE_TASKS_DIR:-$HOME/.claude/tasks}"
15
15
  EAGLE_CODEX_DIR="${EAGLE_CODEX_DIR:-$HOME/.codex}"
16
16
  EAGLE_CODEX_CONFIG="${EAGLE_CODEX_CONFIG:-$EAGLE_CODEX_DIR/config.toml}"
17
17
  EAGLE_CODEX_HOOKS="${EAGLE_CODEX_HOOKS:-$EAGLE_CODEX_DIR/hooks.json}"
@@ -71,30 +71,52 @@ eagle_log() {
71
71
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
72
72
  }
73
73
 
74
- eagle_project_from_cwd() {
75
- if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
76
- echo "$EAGLE_MEM_PROJECT"
77
- return
78
- fi
74
+ eagle_normalize_project_path() {
75
+ local path="${1:-$(pwd)}"
79
76
 
80
- local cwd="${1:-$(pwd)}"
81
- local resolved="$cwd"
77
+ # Normalize macOS /private prefixes.
78
+ case "$path" in /private/tmp*) path="/tmp${path#/private/tmp}" ;; esac
79
+ case "$path" in /private/var/*) path="/var${path#/private/var}" ;; esac
80
+
81
+ printf '%s\n' "$path"
82
+ }
82
83
 
83
- # Normalize macOS /private prefixes
84
- case "$resolved" in /private/tmp*) resolved="/tmp${resolved#/private/tmp}" ;; esac
85
- case "$resolved" in /private/var/*) resolved="/var${resolved#/private/var}" ;; esac
84
+ eagle_is_ephemeral_project_path() {
85
+ local path="${1:-}"
86
86
 
87
- # Skip ephemeral directories — return empty so hooks early-exit
88
- case "$resolved" in
89
- /tmp|/tmp/*|/var/tmp|/var/tmp/*) echo ""; return ;;
90
- /var/folders|/var/folders/*) echo ""; return ;;
91
- "$HOME/Downloads"|"$HOME/Downloads/"*) echo ""; return ;;
92
- "$HOME/Desktop"|"$HOME/Desktop/"*) echo ""; return ;;
87
+ case "$path" in
88
+ /tmp|/tmp/*|/var/tmp|/var/tmp/*) return 0 ;;
89
+ /var/folders|/var/folders/*) return 0 ;;
90
+ "$HOME/Downloads"|"$HOME/Downloads/"*) return 0 ;;
91
+ "$HOME/Desktop"|"$HOME/Desktop/"*) return 0 ;;
93
92
  esac
94
93
 
95
- # Eagle Mem worker lanes run in sibling git worktrees under
96
- # <parent>/.eagle-worktrees/<repo>/<lane>. Keep their observations attached
97
- # to the real project, not to the disposable worktree path.
94
+ return 1
95
+ }
96
+
97
+ eagle_project_key_from_target_dir() {
98
+ local target_dir="${1:-}"
99
+ [ -z "$target_dir" ] && return 1
100
+
101
+ local name
102
+ name=$(basename "$target_dir")
103
+ if [ ${#name} -le 1 ]; then
104
+ echo ""
105
+ return 0
106
+ fi
107
+
108
+ if [[ "$target_dir" == "$HOME/"* ]]; then
109
+ echo "${target_dir#$HOME/}"
110
+ elif [ "$target_dir" = "$HOME" ]; then
111
+ echo "$name"
112
+ else
113
+ echo "${target_dir#/}"
114
+ fi
115
+ }
116
+
117
+ eagle_project_key_for_worktree_path() {
118
+ local resolved="${1:-}"
119
+
98
120
  case "$resolved" in
99
121
  "$HOME"/*/.eagle-worktrees/*)
100
122
  local worktree_parent worktree_tail worktree_repo worktree_project
@@ -103,34 +125,159 @@ eagle_project_from_cwd() {
103
125
  worktree_repo="${worktree_tail%%/*}"
104
126
  worktree_project="$worktree_parent/$worktree_repo"
105
127
  if [ -n "$worktree_repo" ] && [ -d "$worktree_project" ]; then
106
- echo "${worktree_project#$HOME/}"
107
- return
128
+ eagle_project_key_from_target_dir "$worktree_project"
129
+ return 0
108
130
  fi
109
131
  ;;
110
132
  esac
111
133
 
134
+ return 1
135
+ }
136
+
137
+ eagle_project_from_path_no_git() {
138
+ if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
139
+ echo "$EAGLE_MEM_PROJECT"
140
+ return
141
+ fi
142
+
143
+ local path="${1:-$(pwd)}"
144
+ local resolved
145
+ resolved=$(eagle_normalize_project_path "$path")
146
+
147
+ if eagle_is_ephemeral_project_path "$resolved"; then
148
+ echo ""
149
+ return
150
+ fi
151
+
152
+ local worktree_project
153
+ if worktree_project=$(eagle_project_key_for_worktree_path "$resolved"); then
154
+ echo "$worktree_project"
155
+ return
156
+ fi
157
+
158
+ eagle_project_key_from_target_dir "$resolved"
159
+ }
160
+
161
+ eagle_project_from_cwd() {
162
+ if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
163
+ echo "$EAGLE_MEM_PROJECT"
164
+ return
165
+ fi
166
+
167
+ local cwd="${1:-$(pwd)}"
168
+ local resolved
169
+ resolved=$(eagle_normalize_project_path "$cwd")
170
+
171
+ # Skip ephemeral directories — return empty so hooks early-exit.
172
+ if eagle_is_ephemeral_project_path "$resolved"; then
173
+ echo ""
174
+ return
175
+ fi
176
+
177
+ # Eagle Mem worker lanes run in sibling git worktrees under
178
+ # <parent>/.eagle-worktrees/<repo>/<lane>. Keep their observations attached
179
+ # to the real project, not to the disposable worktree path.
180
+ local worktree_project
181
+ if worktree_project=$(eagle_project_key_for_worktree_path "$resolved"); then
182
+ echo "$worktree_project"
183
+ return
184
+ fi
185
+
112
186
  local target_dir
113
187
  local git_root
114
- git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
188
+ git_root=$(git -C "$resolved" rev-parse --show-toplevel 2>/dev/null)
115
189
  if [ -n "$git_root" ]; then
116
190
  target_dir="$git_root"
117
191
  else
118
- target_dir="$cwd"
192
+ target_dir="$resolved"
119
193
  fi
120
194
 
121
- local name
122
- name=$(basename "$target_dir")
123
- if [ ${#name} -le 1 ]; then
124
- echo ""; return
195
+ eagle_project_key_from_target_dir "$target_dir"
196
+ }
197
+
198
+ eagle_path_is_same_or_child() {
199
+ local parent child
200
+ parent=$(eagle_normalize_project_path "${1:-}")
201
+ child=$(eagle_normalize_project_path "${2:-}")
202
+
203
+ [ -z "$parent" ] || [ -z "$child" ] && return 1
204
+ [ "$child" = "$parent" ] && return 0
205
+ case "$child" in "$parent"/*) return 0 ;; esac
206
+ return 1
207
+ }
208
+
209
+ eagle_transcript_first_cwd() {
210
+ local transcript_path="${1:-}"
211
+ [ -f "$transcript_path" ] || return 1
212
+
213
+ local sample
214
+ sample=$(dd if="$transcript_path" bs=65536 count=4 2>/dev/null)
215
+ [ -n "$sample" ] || return 1
216
+
217
+ printf '%s\n' "$sample" | sed -nE '/"cwd"[[:space:]]*:/ {
218
+ s/.*"cwd"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/
219
+ p
220
+ q
221
+ }'
222
+ }
223
+
224
+ eagle_project_from_claude_project_dir() {
225
+ local project_dir="${1:-}"
226
+ project_dir="${project_dir%/}"
227
+ [ -d "$project_dir" ] || return 1
228
+
229
+ local jsonl cwd project
230
+ for jsonl in "$project_dir"/*.jsonl; do
231
+ [ -f "$jsonl" ] || continue
232
+ cwd=$(eagle_transcript_first_cwd "$jsonl")
233
+ [ -z "$cwd" ] && continue
234
+ project=$(eagle_project_from_path_no_git "$cwd")
235
+ [ -n "$project" ] && { printf '%s\n' "$project"; return 0; }
236
+ done
237
+
238
+ return 1
239
+ }
240
+
241
+ eagle_project_from_claude_transcript() {
242
+ local transcript_path="${1:-}"
243
+ local cwd="${2:-}"
244
+
245
+ case "$transcript_path" in
246
+ "$EAGLE_CLAUDE_PROJECTS_DIR"/*/*.jsonl) ;;
247
+ *) return 1 ;;
248
+ esac
249
+
250
+ local transcript_cwd project
251
+ transcript_cwd=$(eagle_transcript_first_cwd "$transcript_path")
252
+ [ -n "$transcript_cwd" ] || return 1
253
+
254
+ if [ -n "$cwd" ] && ! eagle_path_is_same_or_child "$transcript_cwd" "$cwd"; then
255
+ return 1
125
256
  fi
126
257
 
127
- if [[ "$target_dir" == "$HOME/"* ]]; then
128
- echo "${target_dir#$HOME/}"
129
- elif [ "$target_dir" = "$HOME" ]; then
130
- echo "$name"
131
- else
132
- echo "${target_dir#/}"
258
+ project=$(eagle_project_from_path_no_git "$transcript_cwd")
259
+ [ -n "$project" ] || return 1
260
+ printf '%s\n' "$project"
261
+ }
262
+
263
+ eagle_project_from_hook_input() {
264
+ local input="${1:-}"
265
+
266
+ if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
267
+ echo "$EAGLE_MEM_PROJECT"
268
+ return
133
269
  fi
270
+
271
+ local cwd transcript_path project
272
+ cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
273
+ transcript_path=$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null)
274
+
275
+ if project=$(eagle_project_from_claude_transcript "$transcript_path" "$cwd"); then
276
+ printf '%s\n' "$project"
277
+ return
278
+ fi
279
+
280
+ eagle_project_from_cwd "$cwd"
134
281
  }
135
282
 
136
283
  eagle_project_file_path() {
@@ -761,6 +908,7 @@ Eagle Mem hooks are active for Codex in this project. SessionStart and UserPromp
761
908
  - Attribute recalled context as "Eagle Mem recalls:" when it is injected
762
909
  - Use the Eagle Mem skills when relevant: `eagle-mem-search`, `eagle-mem-overview`, `eagle-mem-memories`, `eagle-mem-tasks`, and `eagle-mem-orchestrate`
763
910
  - For broad multi-agent work, YOU run `eagle-mem orchestrate`; do not ask the user to run these commands
911
+ - Codex does not currently expose a persistent custom statusline like Claude Code; if the user asks for Eagle Mem status, run `eagle-mem statusline`
764
912
  - For important decisions, preferences, gotchas, or durable project facts, include them briefly in normal prose. Eagle Mem will extract them from the transcript.
765
913
  - Do not revert Eagle Mem-surfaced decisions without asking the user
766
914
  - If Eagle Mem reports pending feature verification, verify or waive it before push/PR/publish
@@ -779,7 +927,8 @@ eagle_patch_codex_agents_md() {
779
927
  || grep -qF 'emit an <eagle-summary> block' "$agents_md" 2>/dev/null \
780
928
  || grep -qF 'explicitly include them in the `<eagle-summary>` block' "$agents_md" 2>/dev/null \
781
929
  || grep -qF 'explicitly include them in the <eagle-summary> block' "$agents_md" 2>/dev/null \
782
- || ! grep -qF 'Keep Codex final answers clean' "$agents_md" 2>/dev/null; then
930
+ || ! grep -qF 'Keep Codex final answers clean' "$agents_md" 2>/dev/null \
931
+ || ! grep -qF 'eagle-mem statusline' "$agents_md" 2>/dev/null; then
783
932
  local tmp_md
784
933
  tmp_md=$(mktemp)
785
934
  awk -v marker="$marker" '
@@ -6,22 +6,41 @@
6
6
  _EAGLE_DB_BACKFILL_LOADED=1
7
7
 
8
8
  eagle_build_session_project_map() {
9
+ local session_filter_file="${1:-}"
9
10
  local claude_projects_dir="$EAGLE_CLAUDE_PROJECTS_DIR"
10
11
  [ ! -d "$claude_projects_dir" ] && return 0
11
12
 
13
+ if [ -n "$session_filter_file" ] && [ -s "$session_filter_file" ]; then
14
+ local transcript_index project_cache
15
+ transcript_index=$(mktemp)
16
+ project_cache=$(mktemp)
17
+ find "$claude_projects_dir" -mindepth 2 -maxdepth 2 -name "*.jsonl" -print > "$transcript_index" 2>/dev/null || true
18
+
19
+ local sid jsonl proj_dir project cached
20
+ while IFS= read -r sid; do
21
+ [ -z "$sid" ] && continue
22
+ jsonl=$(grep -m 1 "/$sid.jsonl\$" "$transcript_index" 2>/dev/null || true)
23
+ [ -f "$jsonl" ] || continue
24
+
25
+ proj_dir=$(dirname "$jsonl")
26
+ cached=$(grep -m 1 "^$proj_dir|" "$project_cache" 2>/dev/null || true)
27
+ if [ -n "$cached" ]; then
28
+ project="${cached#*|}"
29
+ else
30
+ project=$(eagle_project_from_claude_project_dir "$proj_dir" 2>/dev/null || true)
31
+ printf '%s|%s\n' "$proj_dir" "$project" >> "$project_cache"
32
+ fi
33
+ [ -n "$project" ] && echo "$sid|$project"
34
+ done < "$session_filter_file"
35
+ rm -f "$transcript_index" "$project_cache"
36
+ return 0
37
+ fi
38
+
12
39
  for proj_dir in "$claude_projects_dir"/*/; do
13
40
  [ ! -d "$proj_dir" ] && continue
14
41
 
15
42
  local project=""
16
- local sample_jsonl
17
- sample_jsonl=$(ls "$proj_dir"*.jsonl 2>/dev/null | head -1)
18
- if [ -n "$sample_jsonl" ] && [ -f "$sample_jsonl" ]; then
19
- local cwd
20
- cwd=$(head -10 "$sample_jsonl" | jq -r 'select(.cwd != null) | .cwd' 2>/dev/null | head -1)
21
- if [ -n "$cwd" ]; then
22
- project=$(eagle_project_from_cwd "$cwd")
23
- fi
24
- fi
43
+ project=$(eagle_project_from_claude_project_dir "$proj_dir" 2>/dev/null || true)
25
44
  [ -z "$project" ] && continue
26
45
 
27
46
  for jsonl in "$proj_dir"*.jsonl; do
@@ -33,36 +52,55 @@ eagle_build_session_project_map() {
33
52
  done
34
53
  }
35
54
 
55
+ eagle_build_claude_project_dir_map() {
56
+ local claude_projects_dir="$EAGLE_CLAUDE_PROJECTS_DIR"
57
+ [ ! -d "$claude_projects_dir" ] && return 0
58
+
59
+ for proj_dir in "$claude_projects_dir"/*/; do
60
+ [ ! -d "$proj_dir" ] && continue
61
+
62
+ local project
63
+ project=$(eagle_project_from_claude_project_dir "$proj_dir" 2>/dev/null || true)
64
+ [ -z "$project" ] && continue
65
+
66
+ printf '%s|%s\n' "${proj_dir%/}" "$project"
67
+ done
68
+ }
69
+
36
70
  eagle_backfill_projects() {
37
71
  local updated=0
38
- local map
39
- map=$(eagle_build_session_project_map)
40
- [ -z "$map" ] && echo "0" && return 0
72
+ local session_filter_file map
73
+ session_filter_file=$(mktemp)
74
+ eagle_db "SELECT id FROM sessions;" > "$session_filter_file" 2>/dev/null || true
75
+ map=$(eagle_build_session_project_map "$session_filter_file")
41
76
 
42
77
  # Phase 1: Build old→new project mapping BEFORE mutating any rows.
43
78
  # Collect from sessions table so non-session tables can be migrated.
44
79
  local rename_map_file
45
80
  rename_map_file=$(mktemp)
46
- while IFS='|' read -r sid project; do
47
- [ -z "$sid" ] || [ -z "$project" ] && continue
48
- local sid_sql
49
- sid_sql=$(eagle_sql_escape "$sid")
50
- local old_project
51
- old_project=$(eagle_db "SELECT project FROM sessions WHERE id = '$sid_sql';")
52
- if [ -n "$old_project" ] && [ "$old_project" != "$project" ]; then
53
- echo "$old_project|$project" >> "$rename_map_file"
54
- fi
55
- done <<< "$map"
81
+ if [ -n "$map" ]; then
82
+ while IFS='|' read -r sid project; do
83
+ [ -z "$sid" ] || [ -z "$project" ] && continue
84
+ local sid_sql
85
+ sid_sql=$(eagle_sql_escape "$sid")
86
+ local old_project
87
+ old_project=$(eagle_db "SELECT project FROM sessions WHERE id = '$sid_sql';")
88
+ if [ -n "$old_project" ] && [ "$old_project" != "$project" ]; then
89
+ echo "$old_project|$project" >> "$rename_map_file"
90
+ fi
91
+ done <<< "$map"
92
+ fi
56
93
 
57
94
  # Phase 2: Update session-linked tables
58
- while IFS='|' read -r sid project; do
59
- [ -z "$sid" ] || [ -z "$project" ] && continue
60
- local sid_sql proj_sql
61
- sid_sql=$(eagle_sql_escape "$sid")
62
- proj_sql=$(eagle_sql_escape "$project")
63
-
64
- local ch
65
- ch=$(eagle_db_pipe <<SQL
95
+ if [ -n "$map" ]; then
96
+ while IFS='|' read -r sid project; do
97
+ [ -z "$sid" ] || [ -z "$project" ] && continue
98
+ local sid_sql proj_sql
99
+ sid_sql=$(eagle_sql_escape "$sid")
100
+ proj_sql=$(eagle_sql_escape "$project")
101
+
102
+ local ch
103
+ ch=$(eagle_db_pipe <<SQL
66
104
  BEGIN;
67
105
  UPDATE sessions SET project = '$proj_sql' WHERE id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
68
106
  UPDATE agent_tasks SET project = '$proj_sql' WHERE source_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
@@ -73,11 +111,38 @@ UPDATE observations SET project = '$proj_sql' WHERE session_id = '$sid_sql' AND
73
111
  SELECT total_changes();
74
112
  COMMIT;
75
113
  SQL
114
+ )
115
+ [ "${ch:-0}" -gt 0 ] && updated=$((updated + ch))
116
+ done <<< "$map"
117
+ fi
118
+
119
+ # Phase 3: Repair Claude memory rows that have no session id but live under
120
+ # a Claude project directory. These rows used to keep stale project keys
121
+ # when their file content did not change.
122
+ local dir_map
123
+ dir_map=$(eagle_build_claude_project_dir_map)
124
+ while IFS='|' read -r proj_dir project; do
125
+ [ -z "$proj_dir" ] || [ -z "$project" ] && continue
126
+ local proj_sql prefix_sql
127
+ proj_sql=$(eagle_sql_escape "$project")
128
+ prefix_sql=$(eagle_sql_escape "$proj_dir/memory/")
129
+
130
+ local ch
131
+ ch=$(eagle_db_pipe <<SQL
132
+ BEGIN;
133
+ UPDATE agent_memories
134
+ SET project = '$proj_sql'
135
+ WHERE file_path >= '$prefix_sql'
136
+ AND file_path < ('$prefix_sql' || char(0x10ffff))
137
+ AND (project = '' OR project != '$proj_sql');
138
+ SELECT total_changes();
139
+ COMMIT;
140
+ SQL
76
141
  )
77
142
  [ "${ch:-0}" -gt 0 ] && updated=$((updated + ch))
78
- done <<< "$map"
143
+ done <<< "$dir_map"
79
144
 
80
- # Phase 3: Update non-session tables using the old→new mapping.
145
+ # Phase 4: Update non-session tables using the old→new mapping.
81
146
  # Skip ambiguous mappings (one old name → multiple new names).
82
147
  if [ -s "$rename_map_file" ]; then
83
148
  local uniq_map
@@ -121,7 +186,7 @@ COMMIT;
121
186
  SQL
122
187
  done <<< "$uniq_map"
123
188
  fi
124
- rm -f "$rename_map_file"
189
+ rm -f "$rename_map_file" "$session_filter_file"
125
190
 
126
191
  echo "$updated"
127
192
  }
package/lib/db-mirrors.sh CHANGED
@@ -71,10 +71,14 @@ ON CONFLICT(file_path) DO UPDATE SET
71
71
  memory_type = excluded.memory_type,
72
72
  content = excluded.content,
73
73
  content_hash = excluded.content_hash,
74
- origin_session_id = excluded.origin_session_id,
75
- origin_agent = excluded.origin_agent,
74
+ origin_session_id = COALESCE(NULLIF(excluded.origin_session_id, ''), agent_memories.origin_session_id),
75
+ origin_agent = COALESCE(NULLIF(excluded.origin_agent, ''), agent_memories.origin_agent),
76
+ project = CASE WHEN excluded.project != '' THEN excluded.project ELSE agent_memories.project END,
76
77
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
77
- WHERE agent_memories.content_hash != excluded.content_hash;
78
+ WHERE agent_memories.content_hash != excluded.content_hash
79
+ OR (excluded.project != '' AND agent_memories.project != excluded.project)
80
+ OR (excluded.origin_session_id != '' AND agent_memories.origin_session_id != excluded.origin_session_id)
81
+ OR (excluded.origin_agent != '' AND agent_memories.origin_agent != excluded.origin_agent);
78
82
  SQL
79
83
  }
80
84
 
@@ -157,7 +161,10 @@ ON CONFLICT(file_path) DO UPDATE SET
157
161
  origin_agent = COALESCE(NULLIF(excluded.origin_agent, ''), agent_plans.origin_agent),
158
162
  project = CASE WHEN excluded.project != '' THEN excluded.project ELSE agent_plans.project END,
159
163
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
160
- WHERE agent_plans.content_hash != excluded.content_hash;
164
+ WHERE agent_plans.content_hash != excluded.content_hash
165
+ OR (excluded.project != '' AND agent_plans.project != excluded.project)
166
+ OR (excluded.origin_session_id != '' AND agent_plans.origin_session_id != excluded.origin_session_id)
167
+ OR (excluded.origin_agent != '' AND agent_plans.origin_agent != excluded.origin_agent);
161
168
  SQL
162
169
  }
163
170
 
@@ -258,7 +265,9 @@ ON CONFLICT(file_path) DO UPDATE SET
258
265
  origin_agent = excluded.origin_agent,
259
266
  project = CASE WHEN excluded.project != '' THEN excluded.project ELSE agent_tasks.project END,
260
267
  updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
261
- WHERE agent_tasks.content_hash != excluded.content_hash;
268
+ WHERE agent_tasks.content_hash != excluded.content_hash
269
+ OR (excluded.project != '' AND agent_tasks.project != excluded.project)
270
+ OR (excluded.origin_agent != '' AND agent_tasks.origin_agent != excluded.origin_agent);
262
271
  SQL
263
272
  }
264
273
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.9.1",
3
+ "version": "4.9.2",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code and Codex",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/help.sh CHANGED
@@ -32,6 +32,7 @@ echo -e " ${BOLD}Safety and token controls:${RESET}"
32
32
  echo -e " ${CYAN}feature${RESET} Track, verify, and unblock feature changes"
33
33
  echo -e " ${CYAN}guard${RESET} Manage regression guardrails for files"
34
34
  echo -e " ${CYAN}config${RESET} View/change providers, RTK, and token guard settings"
35
+ echo -e " ${CYAN}statusline${RESET} Print compact Eagle Mem statusline"
35
36
  echo ""
36
37
  echo -e " ${BOLD}Automation and coordination:${RESET}"
37
38
  echo -e " ${CYAN}curate${RESET} Run curator (co-edits, hot files, guardrails)"
@@ -52,6 +52,26 @@ install_package() {
52
52
  esac
53
53
  }
54
54
 
55
+ ensure_rtk() {
56
+ if command -v rtk &>/dev/null; then
57
+ rtk_version=$(rtk --version 2>/dev/null | head -1)
58
+ eagle_ok "RTK ${DIM}(${rtk_version:-installed})${RESET}"
59
+ return 0
60
+ fi
61
+
62
+ eagle_warn "RTK not found ${DIM}(token guard will be advisory until installed)${RESET}"
63
+ if command -v cargo &>/dev/null; then
64
+ eagle_info "Installing RTK with Cargo: cargo install rtk"
65
+ if cargo install rtk; then
66
+ eagle_ok "RTK installed"
67
+ else
68
+ eagle_warn "RTK install failed ${DIM}(continuing; run 'cargo install rtk' later)${RESET}"
69
+ fi
70
+ else
71
+ eagle_dim "Install Rust/Cargo, then run: cargo install rtk"
72
+ fi
73
+ }
74
+
55
75
  # ─── Check prerequisites ───────────────────────────────────
56
76
 
57
77
  echo -e " ${BOLD}Checking prerequisites...${RESET}"
@@ -118,6 +138,10 @@ else
118
138
  fi
119
139
  fi
120
140
 
141
+ # RTK is optional, but Eagle Mem can use it for both Claude Code and Codex
142
+ # token-guard behavior when it is available on PATH.
143
+ ensure_rtk
144
+
121
145
  # Claude Code / Codex
122
146
  claude_found=false
123
147
  codex_found=false
@@ -283,8 +307,9 @@ if [ "$claude_found" = true ]; then
283
307
  #!/usr/bin/env bash
284
308
  input=$(cat)
285
309
  project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""' 2>/dev/null)
310
+ session_id=$(echo "$input" | jq -r '.session_id // .session.id // ""' 2>/dev/null)
286
311
  source "$HOME/.eagle-mem/scripts/statusline-em.sh"
287
- eagle_mem_statusline "$project_dir"
312
+ eagle_mem_statusline "$project_dir" "$session_id"
288
313
  WRAPPER
289
314
  chmod +x "$wrapper"
290
315
  tmp=$(mktemp)
@@ -303,7 +328,7 @@ WRAPPER
303
328
  eagle_dim " em_section=\"\""
304
329
  eagle_dim " if [ -f \"\$HOME/.eagle-mem/scripts/statusline-em.sh\" ]; then"
305
330
  eagle_dim " source \"\$HOME/.eagle-mem/scripts/statusline-em.sh\""
306
- eagle_dim " em_section=\$(eagle_mem_statusline \"\$project_dir\")"
331
+ eagle_dim " em_section=\$(eagle_mem_statusline \"\$project_dir\" \"\$session_id\")"
307
332
  eagle_dim " fi"
308
333
  echo ""
309
334
  eagle_ok "Statusline ${DIM}(manual patch needed — instructions above)${RESET}"
@@ -455,17 +455,20 @@ memories_sync() {
455
455
  base=$(basename "$memfile")
456
456
  [ "$base" = "MEMORY.md" ] && continue
457
457
 
458
- local existing_hash
459
- existing_hash=$(eagle_db "SELECT content_hash FROM agent_memories WHERE file_path = '$(eagle_sql_escape "$memfile")';")
458
+ local mem_project mem_project_dir existing_row existing_hash existing_project
459
+ mem_project_dir="${memfile%/memory/*}"
460
+ mem_project=$(eagle_project_from_claude_project_dir "$mem_project_dir" 2>/dev/null || true)
461
+ existing_row=$(eagle_db "SELECT content_hash, project FROM agent_memories WHERE file_path = '$(eagle_sql_escape "$memfile")';")
462
+ IFS='|' read -r existing_hash existing_project <<< "$existing_row"
460
463
  local new_hash
461
464
  new_hash=$(shasum -a 256 "$memfile" | awk '{print $1}')
462
465
 
463
- if [ "$existing_hash" = "$new_hash" ]; then
466
+ if [ "$existing_hash" = "$new_hash" ] && { [ -z "$mem_project" ] || [ "$existing_project" = "$mem_project" ]; }; then
464
467
  mem_skipped=$((mem_skipped + 1))
465
468
  continue
466
469
  fi
467
470
 
468
- eagle_capture_agent_memory "$memfile" "" ""
471
+ eagle_capture_agent_memory "$memfile" "" "$mem_project" "claude-code"
469
472
  mem_synced=$((mem_synced + 1))
470
473
  eagle_ok "Memory: $base"
471
474
  done < <(find "$claude_mem_root" -path "*/memory/*.md" -print0 2>/dev/null)
@@ -478,12 +481,13 @@ memories_sync() {
478
481
  for memfile in "$codex_mem_root/MEMORY.md" "$codex_mem_root/memory_summary.md"; do
479
482
  [ ! -f "$memfile" ] && continue
480
483
 
481
- local existing_hash
482
- existing_hash=$(eagle_db "SELECT content_hash FROM agent_memories WHERE file_path = '$(eagle_sql_escape "$memfile")';")
484
+ local existing_row existing_hash existing_project
485
+ existing_row=$(eagle_db "SELECT content_hash, project FROM agent_memories WHERE file_path = '$(eagle_sql_escape "$memfile")';")
486
+ IFS='|' read -r existing_hash existing_project <<< "$existing_row"
483
487
  local new_hash
484
488
  new_hash=$(shasum -a 256 "$memfile" | awk '{print $1}')
485
489
 
486
- if [ "$existing_hash" = "$new_hash" ]; then
490
+ if [ "$existing_hash" = "$new_hash" ] && [ "$existing_project" = "$codex_project" ]; then
487
491
  mem_skipped=$((mem_skipped + 1))
488
492
  continue
489
493
  fi
@@ -5,6 +5,7 @@
5
5
 
6
6
  eagle_mem_statusline() {
7
7
  local project_dir="${1:-}"
8
+ local session_id="${2:-}"
8
9
  local em_db="$HOME/.eagle-mem/memory.db"
9
10
  [ -f "$em_db" ] || return
10
11
 
@@ -12,25 +13,49 @@ eagle_mem_statusline() {
12
13
  . "$SCRIPT_DIR/../lib/common.sh"
13
14
 
14
15
  local proj
16
+ [ -z "$project_dir" ] && project_dir="$(pwd)"
15
17
  proj=$(eagle_project_from_cwd "$project_dir")
16
18
  [ -z "$proj" ] && return
17
19
 
18
20
  proj=$(eagle_sql_escape "$proj")
19
21
 
20
- local cnt mem
22
+ local version cnt mem turns
23
+ version=$(tr -d '[:space:]' < "$HOME/.eagle-mem/.version" 2>/dev/null)
24
+ [ -z "$version" ] && version="?"
21
25
  cnt=$(echo ".headers off
22
26
  SELECT COUNT(*) FROM sessions WHERE project = '${proj}';" | sqlite3 "$em_db" 2>/dev/null | tr -d '[:space:]')
23
27
  mem=$(echo ".headers off
24
28
  SELECT COUNT(*) FROM agent_memories WHERE project = '${proj}';" | sqlite3 "$em_db" 2>/dev/null | tr -d '[:space:]')
29
+ if [ -n "$session_id" ] && [ -f "$HOME/.eagle-mem/.turn-counter.${session_id}" ]; then
30
+ turns=$(tr -d '[:space:]' < "$HOME/.eagle-mem/.turn-counter.${session_id}" 2>/dev/null)
31
+ else
32
+ turns=$(find "$HOME/.eagle-mem" -name '.turn-counter.*' -type f -mtime -1 -print 2>/dev/null \
33
+ | while IFS= read -r f; do
34
+ tr -d '[:space:]' < "$f" 2>/dev/null
35
+ echo ""
36
+ done \
37
+ | awk '($1+0)>max{max=$1+0} END{print max+0}')
38
+ fi
25
39
  cnt=${cnt:-0}; mem=${mem:-0}
40
+ turns=${turns:-0}
26
41
 
27
42
  local R='\033[0m' CYAN='\033[96m' WHT='\033[97m' DIM='\033[2m'
28
- printf "%bEagle Mem%b %b%s%b ses %b%s%b mem" "$CYAN" "$R" "$WHT" "$cnt" "$DIM" "$WHT" "$mem" "$R"
43
+ printf "%bEM%b %bv%s%b ses %b%s%b mem %b%s%b turns %b%s%b" \
44
+ "$CYAN" "$R" \
45
+ "$WHT" "$version" "$DIM" \
46
+ "$WHT" "$cnt" "$DIM" \
47
+ "$WHT" "$mem" "$DIM" \
48
+ "$WHT" "$turns" "$R"
29
49
  }
30
50
 
31
51
  # When run directly, read project_dir from stdin JSON (statusline format)
32
52
  if [ "${BASH_SOURCE[0]}" = "$0" ]; then
33
- input=$(cat)
34
- project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""' 2>/dev/null)
35
- eagle_mem_statusline "$project_dir"
53
+ if [ -t 0 ]; then
54
+ input=""
55
+ else
56
+ input=$(cat)
57
+ fi
58
+ project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // empty' 2>/dev/null)
59
+ session_id=$(echo "$input" | jq -r '.session_id // .session.id // empty' 2>/dev/null)
60
+ eagle_mem_statusline "${project_dir:-$(pwd)}" "$session_id"
36
61
  fi
package/scripts/update.sh CHANGED
@@ -143,6 +143,22 @@ if [ "$codex_found" = true ] && [ -d "$PACKAGE_DIR/skills" ]; then
143
143
  eagle_ok "Codex skills updated"
144
144
  fi
145
145
 
146
+ # ─── Refresh generated Claude statusline wrapper ───────────
147
+
148
+ statusline_wrapper="$EAGLE_MEM_DIR/scripts/statusline-wrapper.sh"
149
+ if [ -f "$statusline_wrapper" ]; then
150
+ cat > "$statusline_wrapper" << 'WRAPPER'
151
+ #!/usr/bin/env bash
152
+ input=$(cat)
153
+ project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""' 2>/dev/null)
154
+ session_id=$(echo "$input" | jq -r '.session_id // .session.id // ""' 2>/dev/null)
155
+ source "$HOME/.eagle-mem/scripts/statusline-em.sh"
156
+ eagle_mem_statusline "$project_dir" "$session_id"
157
+ WRAPPER
158
+ chmod +x "$statusline_wrapper"
159
+ eagle_ok "Statusline wrapper updated"
160
+ fi
161
+
146
162
  # ─── Backfill project names ───────────────────────────────
147
163
 
148
164
  backfilled=$(eagle_backfill_projects 2>/dev/null)