eagle-mem 3.1.0 → 3.2.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.
@@ -0,0 +1,15 @@
1
+ -- Fix: UNIQUE(key, project) fails with NULL project (each NULL is unique in SQL).
2
+ -- Change project to NOT NULL DEFAULT '' so ON CONFLICT works for global keys.
3
+ CREATE TABLE IF NOT EXISTS eagle_meta_new (
4
+ key TEXT NOT NULL,
5
+ project TEXT NOT NULL DEFAULT '',
6
+ value TEXT NOT NULL,
7
+ updated_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
8
+ UNIQUE(key, project)
9
+ );
10
+
11
+ INSERT OR REPLACE INTO eagle_meta_new (key, project, value, updated_at)
12
+ SELECT key, COALESCE(project, ''), value, updated_at FROM eagle_meta;
13
+
14
+ DROP TABLE eagle_meta;
15
+ ALTER TABLE eagle_meta_new RENAME TO eagle_meta;
@@ -0,0 +1,23 @@
1
+ -- Fix: command_rules.project has same NULL uniqueness flaw as eagle_meta had.
2
+ -- Change to NOT NULL DEFAULT '' for consistent UNIQUE constraint behavior.
3
+ CREATE TABLE IF NOT EXISTS command_rules_new (
4
+ id INTEGER PRIMARY KEY,
5
+ project TEXT NOT NULL DEFAULT '',
6
+ pattern TEXT NOT NULL,
7
+ strategy TEXT NOT NULL DEFAULT 'summary' CHECK (strategy IN ('summary', 'truncate')),
8
+ max_lines INTEGER,
9
+ reason TEXT,
10
+ times_seen INTEGER DEFAULT 0,
11
+ avg_output_bytes INTEGER DEFAULT 0,
12
+ enabled INTEGER DEFAULT 1,
13
+ created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
14
+ updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
15
+ UNIQUE(project, pattern)
16
+ );
17
+
18
+ INSERT OR REPLACE INTO command_rules_new (id, project, pattern, strategy, max_lines, reason, times_seen, avg_output_bytes, enabled, created_at, updated_at)
19
+ SELECT id, COALESCE(project, ''), pattern, strategy, max_lines, reason, times_seen, avg_output_bytes, enabled, created_at, updated_at FROM command_rules;
20
+
21
+ DROP TABLE command_rules;
22
+ ALTER TABLE command_rules_new RENAME TO command_rules;
23
+ CREATE INDEX IF NOT EXISTS idx_command_rules_pattern ON command_rules(pattern);
@@ -2,17 +2,15 @@
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — SessionEnd hook
4
4
  # Fires when the Claude Code session ends
5
- # Marks the session as completed + triggers auto-curate
5
+ # Marks the session as completed
6
6
  # ═══════════════════════════════════════════════════════════
7
7
  set +e
8
8
 
9
9
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
10
  LIB_DIR="$SCRIPT_DIR/../lib"
11
- SCRIPTS_DIR="$SCRIPT_DIR/../scripts"
12
11
 
13
12
  . "$LIB_DIR/common.sh"
14
13
  . "$LIB_DIR/db.sh"
15
- . "$LIB_DIR/provider.sh"
16
14
 
17
15
  input=$(eagle_read_stdin)
18
16
  [ -z "$input" ] && exit 0
@@ -44,23 +42,4 @@ eagle_log "INFO" "SessionEnd: session=$session_id marked completed"
44
42
  # Prune observations older than 90 days (keeps DB size bounded)
45
43
  eagle_prune_observations 90 "$project"
46
44
 
47
- # ─── Auto-curate trigger ─────────────────────────────────
48
- curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
49
- if [ "$curator_schedule" = "auto" ]; then
50
- provider=$(eagle_config_get "provider" "type" "none")
51
- if [ "$provider" != "none" ]; then
52
- min_sessions=$(eagle_config_get "curator" "min_sessions" "5")
53
- min_sessions=$(eagle_sql_int "$min_sessions")
54
-
55
- last_curated=$(eagle_meta_get "last_curated_at" "$project")
56
- since="${last_curated:-1970-01-01T00:00:00Z}"
57
-
58
- sessions_since=$(eagle_count_sessions_since "$project" "$since")
59
- if [ "${sessions_since:-0}" -ge "$min_sessions" ]; then
60
- eagle_log "INFO" "SessionEnd: auto-curate triggered (${sessions_since} sessions since last curate)"
61
- nohup bash "$SCRIPTS_DIR/curate.sh" -p "$project" >> "$EAGLE_MEM_LOG" 2>&1 &
62
- fi
63
- fi
64
- fi
65
-
66
45
  exit 0
@@ -8,9 +8,11 @@ set +e
8
8
 
9
9
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
10
  LIB_DIR="$SCRIPT_DIR/../lib"
11
+ SCRIPTS_DIR="$SCRIPT_DIR/../scripts"
11
12
 
12
13
  . "$LIB_DIR/common.sh"
13
14
  . "$LIB_DIR/db.sh"
15
+ . "$LIB_DIR/provider.sh"
14
16
 
15
17
  eagle_ensure_db
16
18
 
@@ -29,6 +31,8 @@ project=$(eagle_project_from_cwd "$cwd")
29
31
  # Skip ephemeral directories (tmp, Downloads, etc.) — no tracking
30
32
  [ -z "$project" ] && exit 0
31
33
 
34
+ p_esc=$(eagle_sql_escape "$project")
35
+
32
36
  eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$source_type"
33
37
 
34
38
  eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
@@ -38,6 +42,25 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
38
42
  # so long-lived sessions with regular compactions aren't falsely abandoned
39
43
  eagle_abandon_stale_sessions "$session_id"
40
44
 
45
+ # ─── Auto-curate trigger (background, non-blocking) ──────────
46
+ # Moved here from SessionEnd because SessionEnd rarely fires in long-lived sessions.
47
+ # SessionStart fires on every new session, resume, and compaction — reliable trigger.
48
+ curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
49
+ if [ "$curator_schedule" = "auto" ]; then
50
+ _curator_provider=$(eagle_config_get "provider" "type" "none")
51
+ if [ "$_curator_provider" != "none" ]; then
52
+ _min_sessions=$(eagle_config_get "curator" "min_sessions" "5")
53
+ _min_sessions=$(eagle_sql_int "$_min_sessions")
54
+ _last_curated=$(eagle_meta_get "last_curated_at" "$project")
55
+ _since="${_last_curated:-1970-01-01T00:00:00Z}"
56
+ _sessions_since=$(eagle_count_sessions_since "$project" "$_since")
57
+ if [ "${_sessions_since:-0}" -ge "$_min_sessions" ]; then
58
+ eagle_log "INFO" "SessionStart: auto-curate triggered (${_sessions_since} sessions since last curate)"
59
+ nohup bash "$SCRIPTS_DIR/curate.sh" -p "$project" >> "$EAGLE_MEM_LOG" 2>&1 &
60
+ fi
61
+ fi
62
+ fi
63
+
41
64
  # ─── Version check (non-blocking) ────────────────────────────
42
65
 
43
66
  update_notice=""
@@ -125,38 +148,25 @@ eagle_banner="======================================
125
148
  ======================================"
126
149
 
127
150
  context="$eagle_banner
128
-
129
- === EAGLE MEM — Active (trigger: $source_type) ===
130
- 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.
131
-
132
151
  "
133
152
 
134
153
  if [ -n "$update_notice" ]; then
135
- context+="=== EAGLE MEM — $update_notice ===
136
-
137
- "
138
- fi
139
-
140
- # Nudge if last session lacked enrichment
141
- last_enriched=$(eagle_last_session_enriched "$project")
142
- if [ "${last_enriched:-1}" = "0" ] && [ "$stat_with_summaries" -gt 0 ]; then
143
- context+="=== EAGLE MEM — Enrichment Reminder ===
144
- The previous session's summary did NOT include decisions, gotchas, or key_files. These fields power Eagle Mem's self-learning (feature discovery, anti-regression, command intelligence). Please emit an <eagle-summary> block at the end of this session with these fields populated.
145
-
154
+ context+="
155
+ === $update_notice ===
146
156
  "
147
157
  fi
148
158
 
149
159
  # Project overview
150
160
  overview=$(eagle_get_overview "$project")
151
161
  if [ -n "$overview" ]; then
152
- context+="=== EAGLE MEM — Project Overview ===
162
+ context+="
163
+ === Project Overview ===
153
164
  $overview
154
-
155
165
  "
156
166
  else
157
- context+="=== EAGLE MEM — Action Required ===
158
- No overview exists for '$project'. On the user's first prompt, run /eagle-mem-overview to build a structured project briefing. The skill has full instructions for what to read and how to write a rich overview.
159
-
167
+ context+="
168
+ === Action Required ===
169
+ No overview exists for '$project'. Run /eagle-mem-overview to build one.
160
170
  "
161
171
  fi
162
172
 
@@ -164,8 +174,8 @@ fi
164
174
  recent=$(eagle_get_recent_summaries "$project" 5)
165
175
 
166
176
  if [ -n "$recent" ]; then
167
- context+="=== EAGLE MEM ===
168
- Recent sessions for project '$project':
177
+ context+="
178
+ === Recent Sessions ===
169
179
  "
170
180
  while IFS='|' read -r request completed learned next_steps created_at decisions gotchas key_files; do
171
181
  [ -z "$request" ] && [ -z "$completed" ] && continue
@@ -200,8 +210,7 @@ memories=$(eagle_db "SELECT memory_name, memory_type, description, file_path, up
200
210
  LIMIT 5;")
201
211
  if [ -n "$memories" ]; then
202
212
  context+="
203
- === EAGLE MEM — Memories ===
204
- Recent memories for '$project':
213
+ === Memories ===
205
214
  "
206
215
  while IFS='|' read -r mname mtype mdesc _fpath _updated days_ago; do
207
216
  [ -z "$mname" ] && continue
@@ -225,8 +234,7 @@ fi
225
234
  plans=$(eagle_list_claude_plans "$project" 3)
226
235
  if [ -n "$plans" ]; then
227
236
  context+="
228
- === EAGLE MEM — Plans ===
229
- Recent plans for '$project':
237
+ === Plans ===
230
238
  "
231
239
  while IFS='|' read -r ptitle _pproj _fpath _updated; do
232
240
  [ -z "$ptitle" ] && continue
@@ -247,8 +255,7 @@ synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
247
255
  LIMIT 10;")
248
256
  if [ -n "$synced_tasks" ]; then
249
257
  context+="
250
- === EAGLE MEM — Tasks ===
251
- Tasks for '$project':
258
+ === Tasks ===
252
259
  "
253
260
  while IFS='|' read -r tsubject tstatus tblocked; do
254
261
  [ -z "$tsubject" ] && continue
@@ -261,72 +268,37 @@ Tasks for '$project':
261
268
  done <<< "$synced_tasks"
262
269
  fi
263
270
 
264
- # Emit the eagle-summary instruction
265
- context+="
266
- === EAGLE MEM INSTRUCTIONS ===
267
- 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.
268
-
269
- 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:
271
+ # ─── Instructions (full on startup, minimal on compact) ──
270
272
 
271
- \`\`\`
272
- $eagle_banner
273
- \`\`\`
274
-
275
- This gives the user visibility into the full context Eagle Mem loaded for this session.
276
-
277
- 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.
278
-
279
- 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.
280
-
281
- 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.
282
-
283
- === EAGLE MEM — SESSION SUMMARY (MANDATORY) ===
284
- 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.
285
-
286
- FORMAT — emit this block exactly. Every field is REQUIRED. Do not skip fields, do not leave them empty, do not write \"N/A\".
287
-
288
- <eagle-summary>
289
- request: [One sentence: what did the user ask for?]
290
- investigated: [Comma-separated file paths you read or explored]
291
- learned: [Non-obvious technical discoveries — things a future session could not guess from reading the code]
292
- completed: [What was accomplished — be specific about what shipped, not what was \"worked on\"]
293
- next_steps: [Concrete actions for the next session, not vague aspirations]
294
- decisions:
295
- - [Choice made] Why: [the reason — what constraint or tradeoff drove this choice]
296
- - [Choice made] Why: [reason]
297
- gotchas:
298
- - [What failed, surprised, or does not work the obvious way. Be specific — \"X does not work because Y\" not just \"X was tricky\"]
299
- key_files:
300
- - [path/to/file.ext] — [one-line role: what this file does in the context of this work]
301
- - [path/to/other.ext] — [role]
302
- files_read: [file1, file2, ...]
303
- files_modified: [file1, file2, ...]
304
- </eagle-summary>
273
+ if [ "$source_type" = "compact" ] || [ "$source_type" = "clear" ]; then
274
+ context+="
275
+ === Eagle Mem (compact reload) ===
276
+ Persistent memory active. Attribute recalled context to Eagle Mem. Do not revert past decisions surfaced by PostToolUse without asking the user. Emit <eagle-summary> before your final response.
277
+ "
278
+ else
279
+ context+="
280
+ === Eagle Mem ===
281
+ Persistent memory active for '$project'. Attribute recalled context: \"Eagle Mem recalls:\" When PostToolUse surfaces past decisions about a file, do not revert without explicit user request. Never include raw secrets in eagle-summary fields. If you change something that contradicts a loaded memory, update that memory file.
305
282
 
306
- EXAMPLE this is what a well-written summary looks like:
283
+ Emit an <eagle-summary> block before your FINAL response:
307
284
 
308
285
  <eagle-summary>
309
- request: Add JWT authentication middleware to the API
310
- investigated: src/middleware/auth.ts, src/routes/users.ts, package.json, src/config/env.ts
311
- learned: express-jwt v8 changed its API — req.auth replaces req.user. The error handler must check err.name === 'UnauthorizedError', not err.status === 401.
312
- 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.
313
- next_steps: Add refresh token rotation; rate-limit the /auth/token endpoint
286
+ request: [what the user asked for]
287
+ investigated: [file paths read or explored]
288
+ learned: [non-obvious discoveries a future session couldn't guess from code]
289
+ completed: [what shipped be specific]
290
+ next_steps: [concrete actions for next session]
314
291
  decisions:
315
- - Chose RS256 over HS256 for JWT signing. Why: allows key rotation via JWKS without redeploying; HS256 requires shared secret on every service.
316
- - 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.
292
+ - [choice] Why: [reason]
317
293
  gotchas:
318
- - express-jwt v8 is ESM-onlyrequire() fails silently and returns undefined. Must use dynamic import().
319
- - 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.
294
+ - [what failed or surprised\"X doesn't work because Y\"]
320
295
  key_files:
321
- - src/middleware/auth.tsJWT validation + role guard middleware
322
- - src/config/env.ts — JWKS_URI and JWT_ISSUER environment config
323
- - src/routes/users.ts — first route to use the new auth guard (reference implementation)
324
- files_read: [src/middleware/auth.ts, src/routes/users.ts, package.json, src/config/env.ts]
325
- files_modified: [src/middleware/auth.ts, src/config/env.ts, src/routes/users.ts, package.json]
296
+ - [path][role in this work]
297
+ files_read: [file1, file2]
298
+ files_modified: [file1, file2]
326
299
  </eagle-summary>
327
-
328
- 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.
329
300
  "
301
+ fi
330
302
 
331
303
  # Output context (plain text stdout = additionalContext for SessionStart)
332
304
  if [ -n "$context" ]; then
package/hooks/stop.sh CHANGED
@@ -2,8 +2,9 @@
2
2
  # ═══════════════════════════════════════════════════════════
3
3
  # Eagle Mem — Stop hook
4
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
5
+ # Primary: extracts summary from transcript heuristically
6
+ # Bonus: eagle-summary block overrides where present
7
+ # LLM enrichment fills in decisions/gotchas/key_files
7
8
  # ═══════════════════════════════════════════════════════════
8
9
  set +e
9
10
 
@@ -37,122 +38,109 @@ eagle_log "INFO" "Stop: session=$session_id project=$project transcript=$transcr
37
38
  # Ensure session exists (may not if SessionStart didn't fire)
38
39
  eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
39
40
 
40
- # ─── Try to parse <eagle-summary> from transcript ──────────
41
+ # ─── Guard: skip if summary already exists for this session ──
42
+ # Stop fires every assistant turn. Only process once per session.
41
43
 
42
- summary_block=""
43
- if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
44
- # Extract all text from the last assistant message using jq on JSONL
45
- # Real transcript format: top-level .type == "assistant", content in .message.content[]
46
- text_content=$(jq -rs '
47
- [.[] | select(.type == "assistant")] | last |
48
- if . then
49
- [.message.content[]? | select(.type == "text") | .text] | join("\n")
50
- else "" end
51
- ' "$transcript_path" 2>/dev/null)
52
-
53
- # Strip <private>...</private> blocks before processing (case-insensitive, tolerates attributes/whitespace)
54
- text_content=$(echo "$text_content" | sed -E '/<[Pp][Rr][Ii][Vv][Aa][Tt][Ee][^>]*>/,/<\/[Pp][Rr][Ii][Vv][Aa][Tt][Ee][[:space:]]*>/d')
55
-
56
- # Parse <eagle-summary> block
57
- if [ -n "$text_content" ] && echo "$text_content" | grep -q '<eagle-summary>' 2>/dev/null; then
58
- summary_block=$(echo "$text_content" | sed -n '/<eagle-summary>/,/<\/eagle-summary>/p' | sed '1d;$d')
59
- fi
44
+ existing_count=$(eagle_count_session_summaries "$session_id")
45
+ if [ "${existing_count:-0}" -gt 0 ]; then
46
+ eagle_log "INFO" "Stop: summary already exists for session=$session_id skipping"
47
+ exit 0
48
+ fi
49
+
50
+ [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ] && exit 0
51
+
52
+ # ─── Primary: heuristic extraction from transcript ───────────
53
+
54
+ 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)
55
+
56
+ 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)
57
+ 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)
58
+
59
+ files_read="[]"
60
+ files_modified="[]"
61
+ if [ -n "$heuristic_reads" ]; then
62
+ files_read=$(echo "$heuristic_reads" | jq -Rsc 'split("\n") | map(select(. != ""))')
63
+ fi
64
+ if [ -n "$heuristic_writes" ]; then
65
+ files_modified=$(echo "$heuristic_writes" | jq -Rsc 'split("\n") | map(select(. != ""))')
60
66
  fi
61
67
 
62
- # ─── Extract fields from summary block ─────────────────────
63
-
64
- parse_field() {
65
- local block="$1"
66
- local field="$2"
67
- echo "$block" | awk -v f="$field" '
68
- BEGIN { IGNORECASE=1; found=0 }
69
- $0 ~ "^"f":" {
70
- sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
71
- }
72
- found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files):/ { exit }
73
- found { val = val " " $0 }
74
- END { if (found) print val }
75
- '
76
- }
77
-
78
- request=""
79
68
  investigated=""
80
69
  learned=""
81
- completed=""
70
+ completed="(auto-captured)"
82
71
  next_steps=""
83
- files_read="[]"
84
- files_modified="[]"
85
72
  notes=""
86
73
  decisions=""
87
74
  gotchas=""
88
75
  key_files=""
89
76
 
90
- if [ -n "$summary_block" ]; then
91
- request=$(parse_field "$summary_block" "request")
92
- investigated=$(parse_field "$summary_block" "investigated")
93
- learned=$(parse_field "$summary_block" "learned")
94
- completed=$(parse_field "$summary_block" "completed")
95
- next_steps=$(parse_field "$summary_block" "next_steps")
96
- decisions=$(parse_field "$summary_block" "decisions")
97
- gotchas=$(parse_field "$summary_block" "gotchas")
98
- key_files=$(parse_field "$summary_block" "key_files")
77
+ eagle_log "INFO" "Stop: heuristic extraction complete"
99
78
 
100
- raw_fr=$(parse_field "$summary_block" "files_read")
101
- raw_fm=$(parse_field "$summary_block" "files_modified")
79
+ # ─── Bonus: eagle-summary block overrides where present ──────
80
+
81
+ text_content=$(jq -rs '
82
+ [.[] | select(.type == "assistant")] | last |
83
+ if . then
84
+ [.message.content[]? | select(.type == "text") | .text] | join("\n")
85
+ else "" end
86
+ ' "$transcript_path" 2>/dev/null)
87
+
88
+ # Strip <private>...</private> blocks
89
+ text_content=$(echo "$text_content" | sed -E '/<[Pp][Rr][Ii][Vv][Aa][Tt][Ee][^>]*>/,/<\/[Pp][Rr][Ii][Vv][Aa][Tt][Ee][[:space:]]*>/d')
90
+
91
+ summary_block=""
92
+ if [ -n "$text_content" ] && echo "$text_content" | grep -q '<eagle-summary>' 2>/dev/null; then
93
+ summary_block=$(echo "$text_content" | sed -n '/<eagle-summary>/,/<\/eagle-summary>/p' | sed '1d;$d')
94
+ fi
102
95
 
103
- # Convert bracket-list to JSON array (handles special chars safely)
96
+ if [ -n "$summary_block" ]; then
97
+ parse_field() {
98
+ local block="$1"
99
+ local field="$2"
100
+ echo "$block" | awk -v f="$field" '
101
+ BEGIN { IGNORECASE=1; found=0 }
102
+ $0 ~ "^"f":" {
103
+ sub("^"f":[[:space:]]*", ""); found=1; val=$0; next
104
+ }
105
+ found && /^(request|investigated|learned|completed|next_steps|files_read|files_modified|notes|decisions|gotchas|key_files):/ { exit }
106
+ found { val = val " " $0 }
107
+ END { if (found) print val }
108
+ '
109
+ }
110
+
111
+ # Override heuristic fields with eagle-summary where non-empty
112
+ _val=$(parse_field "$summary_block" "request"); [ -n "$_val" ] && request="$_val"
113
+ _val=$(parse_field "$summary_block" "investigated"); [ -n "$_val" ] && investigated="$_val"
114
+ _val=$(parse_field "$summary_block" "learned"); [ -n "$_val" ] && learned="$_val"
115
+ _val=$(parse_field "$summary_block" "completed"); [ -n "$_val" ] && completed="$_val"
116
+ _val=$(parse_field "$summary_block" "next_steps"); [ -n "$_val" ] && next_steps="$_val"
117
+ _val=$(parse_field "$summary_block" "decisions"); [ -n "$_val" ] && decisions="$_val"
118
+ _val=$(parse_field "$summary_block" "gotchas"); [ -n "$_val" ] && gotchas="$_val"
119
+ _val=$(parse_field "$summary_block" "key_files"); [ -n "$_val" ] && key_files="$_val"
120
+
121
+ # Convert bracket-list to JSON array
104
122
  to_json_array() {
105
123
  local raw="$1"
106
124
  raw=$(echo "$raw" | sed 's/^\[//;s/\]$//')
107
125
  echo "$raw" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | jq -Rsc 'split("\n") | map(select(. != ""))'
108
126
  }
109
127
 
128
+ raw_fr=$(parse_field "$summary_block" "files_read")
129
+ raw_fm=$(parse_field "$summary_block" "files_modified")
110
130
  [ -n "$raw_fr" ] && files_read=$(to_json_array "$raw_fr")
111
131
  [ -n "$raw_fm" ] && files_modified=$(to_json_array "$raw_fm")
112
132
 
113
- eagle_log "INFO" "Stop: parsed eagle-summary block"
133
+ eagle_log "INFO" "Stop: eagle-summary block merged over heuristic data"
114
134
  fi
115
135
 
116
- # ─── Guard: skip fallback work if summary already exists ──
117
- # Stop fires every assistant turn. Without this, the heuristic and LLM
118
- # enrichment blocks fire on turn 2+ — wasting tokens and producing
119
- # empty inserts that get rejected.
120
-
121
- existing_count=0
122
- if [ -z "$summary_block" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
123
- existing_count=$(eagle_count_session_summaries "$session_id")
124
- fi
125
-
126
- if [ -z "$summary_block" ] && [ "${existing_count:-0}" -eq 0 ]; then
127
-
128
- # ─── Heuristic fallback: extract from tool calls ───────────
136
+ # ─── LLM enrichment: fill in decisions/gotchas/key_files ─────
129
137
 
130
- if [ -z "$request" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
131
- eagle_log "INFO" "Stop: no eagle-summary found, using heuristic fallback"
138
+ if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ]; then
139
+ provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
140
+ if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
141
+ excerpt=$(echo "$text_content" | tail -c 2000)
132
142
 
133
- 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)
134
-
135
- 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)
136
- 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)
137
-
138
- if [ -n "$heuristic_reads" ]; then
139
- files_read=$(echo "$heuristic_reads" | jq -Rsc 'split("\n") | map(select(. != ""))')
140
- fi
141
- if [ -n "$heuristic_writes" ]; then
142
- files_modified=$(echo "$heuristic_writes" | jq -Rsc 'split("\n") | map(select(. != ""))')
143
- fi
144
-
145
- completed="(auto-captured from tool usage)"
146
- fi
147
-
148
- # ─── LLM enrichment: extract decisions/gotchas/key_files ──
149
-
150
- if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ]; then
151
- provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
152
- if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
153
- excerpt=$(echo "$text_content" | tail -c 2000)
154
-
155
- enrich_prompt="Extract from this Claude Code session excerpt:
143
+ enrich_prompt="Extract from this Claude Code session excerpt:
156
144
  1. DECISIONS: architectural or design choices made (with WHY). One per line.
157
145
  2. GOTCHAS: non-obvious pitfalls, bugs found, things that surprised. One per line.
158
146
  3. KEY_FILES: important files that were central to the work. One per line.
@@ -168,28 +156,24 @@ GOTCHAS:
168
156
  KEY_FILES:
169
157
  - <filepath>"
170
158
 
171
- enrich_result=$(eagle_llm_call "$enrich_prompt" "Extract structured facts from development sessions. Be concise. Only include items with clear evidence." 512 2>/dev/null) || true
172
-
173
- if [ -n "$enrich_result" ]; then
174
- extract_section() {
175
- local result="$1" header="$2"
176
- echo "$result" | awk -v h="$header:" '
177
- $0 == h || $0 ~ "^"h { found=1; next }
178
- found && /^[A-Z_]+:/ { exit }
179
- found && /^- / { sub(/^- /, ""); lines[++n] = $0 }
180
- END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
181
- '
182
- }
183
- decisions=$(extract_section "$enrich_result" "DECISIONS")
184
- gotchas=$(extract_section "$enrich_result" "GOTCHAS")
185
- key_files=$(extract_section "$enrich_result" "KEY_FILES")
186
- [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ] && eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id"
187
- fi
159
+ enrich_result=$(eagle_llm_call "$enrich_prompt" "Extract structured facts from development sessions. Be concise. Only include items with clear evidence." 512 2>/dev/null) || true
160
+
161
+ if [ -n "$enrich_result" ]; then
162
+ extract_section() {
163
+ local result="$1" header="$2"
164
+ echo "$result" | awk -v h="$header:" '
165
+ $0 == h || $0 ~ "^"h { found=1; next }
166
+ found && /^[A-Z_]+:/ { exit }
167
+ found && /^- / { sub(/^- /, ""); lines[++n] = $0 }
168
+ END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
169
+ '
170
+ }
171
+ decisions=$(extract_section "$enrich_result" "DECISIONS")
172
+ gotchas=$(extract_section "$enrich_result" "GOTCHAS")
173
+ key_files=$(extract_section "$enrich_result" "KEY_FILES")
174
+ [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ] && eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id"
188
175
  fi
189
176
  fi
190
-
191
- elif [ -z "$summary_block" ] && [ "${existing_count:-0}" -gt 0 ]; then
192
- eagle_log "INFO" "Stop: skipping fallback — summary already exists for session=$session_id (count=$existing_count)"
193
177
  fi
194
178
 
195
179
  # ─── Redact secrets from all text fields before storage ────
package/lib/common.sh CHANGED
@@ -27,12 +27,14 @@ eagle_project_from_cwd() {
27
27
  local cwd="${1:-$(pwd)}"
28
28
  local resolved="$cwd"
29
29
 
30
- # Resolve /private/tmp → /tmp on macOS
30
+ # Normalize macOS /private prefixes
31
31
  case "$resolved" in /private/tmp*) resolved="/tmp${resolved#/private/tmp}" ;; esac
32
+ case "$resolved" in /private/var/*) resolved="/var${resolved#/private/var}" ;; esac
32
33
 
33
34
  # Skip ephemeral directories — return empty so hooks early-exit
34
35
  case "$resolved" in
35
36
  /tmp|/tmp/*|/var/tmp|/var/tmp/*) echo ""; return ;;
37
+ /var/folders|/var/folders/*) echo ""; return ;;
36
38
  "$HOME/Downloads"|"$HOME/Downloads/"*) echo ""; return ;;
37
39
  "$HOME/Desktop"|"$HOME/Desktop/"*) echo ""; return ;;
38
40
  esac
@@ -42,6 +44,12 @@ eagle_project_from_cwd() {
42
44
  if [ -n "$git_root" ]; then
43
45
  basename "$git_root"
44
46
  else
47
+ local name
48
+ name=$(basename "$cwd")
49
+ # Reject single-character project names (likely temp dir fragments)
50
+ if [ ${#name} -le 1 ]; then
51
+ echo ""; return
52
+ fi
45
53
  basename "$cwd"
46
54
  fi
47
55
  }
@@ -50,9 +50,9 @@ eagle_get_command_rule() {
50
50
  eagle_db "SELECT strategy, max_lines, reason
51
51
  FROM command_rules
52
52
  WHERE enabled = 1
53
- AND (project = '$project' OR project IS NULL)
53
+ AND (project = '$project' OR project = '')
54
54
  AND ('$cmd' LIKE pattern OR '$cmd' = pattern)
55
- ORDER BY CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END
55
+ ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END
56
56
  LIMIT 1;"
57
57
  }
58
58
 
@@ -69,27 +69,16 @@ eagle_count_session_summaries() {
69
69
 
70
70
  eagle_meta_get() {
71
71
  local key; key=$(eagle_sql_escape "$1")
72
- local project="${2:-}"
73
- if [ -n "$project" ]; then
74
- local p_esc; p_esc=$(eagle_sql_escape "$project")
75
- eagle_db "SELECT value FROM eagle_meta WHERE key = '$key' AND project = '$p_esc' LIMIT 1;"
76
- else
77
- eagle_db "SELECT value FROM eagle_meta WHERE key = '$key' AND project IS NULL LIMIT 1;"
78
- fi
72
+ local p_esc; p_esc=$(eagle_sql_escape "${2:-}")
73
+ eagle_db "SELECT value FROM eagle_meta WHERE key = '$key' AND project = '$p_esc' LIMIT 1;"
79
74
  }
80
75
 
81
76
  eagle_meta_set() {
82
77
  local key; key=$(eagle_sql_escape "$1")
83
78
  local value; value=$(eagle_sql_escape "$2")
84
- local project="${3:-}"
85
- if [ -n "$project" ]; then
86
- local p_esc; p_esc=$(eagle_sql_escape "$project")
87
- eagle_db "INSERT INTO eagle_meta (key, project, value) VALUES ('$key', '$p_esc', '$value')
88
- ON CONFLICT(key, project) DO UPDATE SET value = excluded.value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
89
- else
90
- eagle_db "INSERT INTO eagle_meta (key, project, value) VALUES ('$key', NULL, '$value')
91
- ON CONFLICT(key, project) DO UPDATE SET value = excluded.value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
92
- fi
79
+ local p_esc; p_esc=$(eagle_sql_escape "${3:-}")
80
+ eagle_db "INSERT INTO eagle_meta (key, project, value) VALUES ('$key', '$p_esc', '$value')
81
+ ON CONFLICT(key, project) DO UPDATE SET value = excluded.value, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
93
82
  }
94
83
 
95
84
  eagle_count_sessions_since() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Persistent memory for Claude Code — SQLite + FTS5, no daemon, no bloat",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/health.sh CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
- # ═══════════════════════════════════════════════════════════
2
+ # ════════��══════════��═══════════════════════════════════════
3
3
  # Eagle Mem — Health Check
4
4
  # Diagnoses how well the self-learning pipeline is working
5
- # ═══════════════════════════════════════════════════════════
5
+ # ════════���══════════════════════════════════════════════════
6
6
  set -euo pipefail
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -13,8 +13,6 @@ LIB_DIR="$SCRIPT_DIR/../lib"
13
13
  . "$LIB_DIR/db.sh"
14
14
  . "$LIB_DIR/provider.sh"
15
15
 
16
- eagle_header "Health Check"
17
-
18
16
  project=""
19
17
  JSON_OUT=0
20
18
 
@@ -26,6 +24,12 @@ while [ $# -gt 0 ]; do
26
24
  esac
27
25
  done
28
26
 
27
+ if [ "$JSON_OUT" -eq 1 ]; then
28
+ exec 3>&1 1>&2
29
+ fi
30
+
31
+ eagle_header "Health Check"
32
+
29
33
  if [ -z "$project" ]; then
30
34
  project=$(eagle_project_from_cwd "$(pwd)")
31
35
  fi
@@ -44,135 +48,148 @@ score=0
44
48
  max_score=0
45
49
  issues=()
46
50
 
47
- # ─── 1. Summary enrichment rate ──────────────────────────
51
+ # ─��─ 1. Summary capture rate (25 pts) ───────────────────
48
52
 
49
- max_score=$((max_score + 30))
53
+ max_score=$((max_score + 25))
50
54
 
55
+ total_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project = '$p_esc';")
51
56
  total_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc';")
57
+ heuristic_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc' AND completed = '(auto-captured)';")
52
58
  enriched_summaries=$(eagle_db "SELECT COUNT(*) FROM summaries WHERE project = '$p_esc' AND (decisions IS NOT NULL AND decisions != '' OR gotchas IS NOT NULL AND gotchas != '' OR key_files IS NOT NULL AND key_files != '');")
53
59
 
54
- if [ "${total_summaries:-0}" -eq 0 ]; then
55
- enrich_pct=0
60
+ if [ "${total_sessions:-0}" -eq 0 ]; then
61
+ capture_pct=0
56
62
  else
57
- enrich_pct=$((enriched_summaries * 100 / total_summaries))
63
+ capture_pct=$((total_summaries * 100 / total_sessions))
58
64
  fi
59
65
 
60
- if [ "$enrich_pct" -ge 50 ]; then
61
- eagle_ok "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%)"
62
- score=$((score + 30))
63
- elif [ "$enrich_pct" -ge 20 ]; then
64
- eagle_warn "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — aim for 50%+"
65
- score=$((score + 15))
66
- issues+=("Low enrichment rate (${enrich_pct}%). Eagle-summary blocks aren't being emitted reliably.")
67
- elif [ "${total_summaries:-0}" -gt 0 ]; then
68
- eagle_fail "Enriched summaries: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) — self-learning not working"
69
- issues+=("Critical: ${enrich_pct}% enrichment. Decisions/gotchas/key_files are not being captured.")
66
+ model_summaries=$((total_summaries - heuristic_summaries))
67
+
68
+ if [ "${total_summaries:-0}" -gt 0 ]; then
69
+ if [ "$capture_pct" -ge 50 ]; then
70
+ eagle_ok "Capture: ${total_summaries}/${total_sessions} sessions (${capture_pct}%) — ${model_summaries} from model, ${heuristic_summaries} heuristic"
71
+ score=$((score + 25))
72
+ elif [ "$capture_pct" -ge 20 ]; then
73
+ eagle_warn "Capture: ${total_summaries}/${total_sessions} sessions (${capture_pct}%) ${heuristic_summaries} heuristic"
74
+ score=$((score + 15))
75
+ else
76
+ eagle_fail "Capture: ${total_summaries}/${total_sessions} sessions (${capture_pct}%)"
77
+ score=$((score + 5))
78
+ fi
70
79
  else
71
80
  eagle_dim " No summaries yet"
72
81
  fi
73
82
 
74
- # ─── 2. Feature discovery ────────────────────────────────
83
+ # ─── 2. Enrichment rate (25 pts) ────────────────────────
75
84
 
76
- max_score=$((max_score + 20))
85
+ max_score=$((max_score + 25))
77
86
 
78
- feature_count=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
79
- feature_file_count=$(eagle_db "SELECT COUNT(*) FROM feature_files ff JOIN features f ON ff.feature_id = f.id WHERE f.project = '$p_esc' AND f.status = 'active';")
87
+ if [ "${total_summaries:-0}" -eq 0 ]; then
88
+ enrich_pct=0
89
+ else
90
+ enrich_pct=$((enriched_summaries * 100 / total_summaries))
91
+ fi
80
92
 
81
- if [ "${feature_count:-0}" -ge 3 ]; then
82
- eagle_ok "Features tracked: ${feature_count} (${feature_file_count} files mapped)"
83
- score=$((score + 20))
84
- elif [ "${feature_count:-0}" -ge 1 ]; then
85
- eagle_warn "Features tracked: ${feature_count} curator needs more sessions"
86
- score=$((score + 10))
87
- issues+=("Only ${feature_count} features discovered. Run curator more often.")
93
+ if [ "${total_summaries:-0}" -gt 0 ]; then
94
+ if [ "$enrich_pct" -ge 50 ]; then
95
+ eagle_ok "Enriched: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) have decisions/gotchas/key_files"
96
+ score=$((score + 25))
97
+ elif [ "$enrich_pct" -ge 20 ]; then
98
+ eagle_warn "Enriched: ${enriched_summaries}/${total_summaries} (${enrich_pct}%) LLM extraction may need tuning"
99
+ score=$((score + 12))
100
+ issues+=("Low enrichment (${enrich_pct}%). Check LLM provider is responsive: eagle-mem config")
101
+ elif [ "${enriched_summaries:-0}" -gt 0 ]; then
102
+ eagle_fail "Enriched: ${enriched_summaries}/${total_summaries} (${enrich_pct}%)"
103
+ score=$((score + 5))
104
+ issues+=("${enrich_pct}% enrichment. Decisions/gotchas/key_files mostly missing.")
105
+ else
106
+ eagle_fail "Enriched: 0/${total_summaries} — no summaries have decisions/gotchas/key_files"
107
+ issues+=("Zero enrichment. Check provider config: eagle-mem config")
108
+ fi
88
109
  else
89
- eagle_fail "Features tracked: 0 — feature graph is empty"
90
- issues+=("No features discovered. Run: eagle-mem curate")
110
+ eagle_dim " No summaries to enrich"
91
111
  fi
92
112
 
93
- # ─── 3. Command intelligence ─────────────────────────────
113
+ # ─── 3. Feature discovery (15 pts) ─────────────────────
94
114
 
95
115
  max_score=$((max_score + 15))
96
116
 
97
- rule_count=$(eagle_db "SELECT COUNT(*) FROM command_rules WHERE (project = '$p_esc' OR project IS NULL) AND enabled = 1;")
98
- obs_with_metrics=$(eagle_db "SELECT COUNT(*) FROM observations WHERE project = '$p_esc' AND tool_name = 'Bash' AND output_bytes IS NOT NULL AND output_bytes > 0;")
117
+ feature_count=$(eagle_db "SELECT COUNT(*) FROM features WHERE project = '$p_esc' AND status = 'active';")
99
118
 
100
- if [ "${rule_count:-0}" -ge 2 ]; then
101
- eagle_ok "Command rules: ${rule_count} active (${obs_with_metrics} observations with metrics)"
119
+ if [ "${feature_count:-0}" -ge 3 ]; then
120
+ eagle_ok "Features: ${feature_count} tracked"
102
121
  score=$((score + 15))
103
- elif [ "${rule_count:-0}" -ge 1 ]; then
104
- eagle_warn "Command rules: ${rule_count} — learning in progress"
122
+ elif [ "${feature_count:-0}" -ge 1 ]; then
123
+ eagle_warn "Features: ${feature_count} — curator needs more sessions"
105
124
  score=$((score + 8))
106
- elif [ "${obs_with_metrics:-0}" -gt 20 ]; then
107
- eagle_fail "Command rules: 0 (but ${obs_with_metrics} observations available — run curator)"
108
- issues+=("Command metrics collected but no rules generated yet.")
109
125
  else
110
- eagle_dim " Command metrics: ${obs_with_metrics} observations (need more data)"
111
- score=$((score + 5))
126
+ eagle_fail "Features: 0 run: eagle-mem curate"
127
+ issues+=("No features discovered. Run: eagle-mem curate")
112
128
  fi
113
129
 
114
- # ─── 4. Provider configured ──────────────────────────────
130
+ # ─── 4. Provider configured (15 pts) ───────────────────
115
131
 
116
132
  max_score=$((max_score + 15))
117
133
 
118
134
  provider=$(eagle_config_get "provider" "type" "none")
119
135
  if [ "$provider" != "none" ]; then
120
136
  model=$(eagle_config_get "$provider" "model" "default")
121
- eagle_ok "LLM provider: ${provider} (${model})"
137
+ eagle_ok "Provider: ${provider} (${model})"
122
138
  score=$((score + 15))
123
139
  else
124
- eagle_fail "No LLM provider — curator and enrichment extraction disabled"
140
+ eagle_fail "No LLM provider — curator and enrichment disabled"
125
141
  issues+=("Configure a provider: eagle-mem config init")
126
142
  fi
127
143
 
128
- # ─── 5. Project data quality ─────────────────────────────
144
+ # ─── 5. Data quality (10 pts) ──────────────────────────
129
145
 
130
146
  max_score=$((max_score + 10))
131
147
 
132
- tmp_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project IN ('tmp', 'private', '');")
133
- total_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions;")
148
+ # Count noise: ephemeral projects + single-char project names
149
+ noise_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions WHERE project IN ('tmp', 'private', '', 'T') OR LENGTH(project) <= 1;")
150
+ all_sessions=$(eagle_db "SELECT COUNT(*) FROM sessions;")
134
151
 
135
- if [ "${total_sessions:-0}" -eq 0 ]; then
152
+ if [ "${all_sessions:-0}" -eq 0 ]; then
136
153
  noise_pct=0
137
154
  else
138
- noise_pct=$((tmp_sessions * 100 / total_sessions))
155
+ noise_pct=$((noise_sessions * 100 / all_sessions))
139
156
  fi
140
157
 
141
158
  if [ "$noise_pct" -le 5 ]; then
142
- eagle_ok "Data quality: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
159
+ eagle_ok "Data quality: ${noise_pct}% noise (${noise_sessions} ephemeral sessions)"
143
160
  score=$((score + 10))
144
161
  elif [ "$noise_pct" -le 20 ]; then
145
- eagle_warn "Data quality: ${tmp_sessions} ephemeral sessions (${noise_pct}% noise)"
162
+ eagle_warn "Data quality: ${noise_pct}% noise (${noise_sessions}/${all_sessions})"
146
163
  score=$((score + 5))
147
- issues+=("${noise_pct}% of sessions from ephemeral dirs. Skiplist should prevent new ones.")
164
+ issues+=("${noise_pct}% noise. Skiplist prevents new pollution; prune old: eagle-mem prune")
148
165
  else
149
- eagle_fail "Data quality: ${noise_pct}% noise ${tmp_sessions}/${total_sessions} sessions from ephemeral dirs"
150
- issues+=("Heavy ephemeral pollution. Update Eagle Mem to get skiplist protection.")
166
+ eagle_fail "Data quality: ${noise_pct}% noise (${noise_sessions}/${all_sessions})"
167
+ issues+=("Heavy noise. Run: eagle-mem prune to clean ephemeral data.")
151
168
  fi
152
169
 
153
- # ─── 6. Curator activity ─────────────────────────────────
170
+ # ─── 6. Curator activity (10 pts) ──────────────────────
154
171
 
155
172
  max_score=$((max_score + 10))
156
173
 
157
174
  curator_schedule=$(eagle_config_get "curator" "schedule" "manual")
158
- last_curated=$(eagle_db "SELECT value FROM eagle_meta WHERE key = 'last_curated_at' AND (project = '$p_esc' OR project IS NULL) ORDER BY CASE WHEN project IS NOT NULL THEN 0 ELSE 1 END LIMIT 1;" 2>/dev/null || echo "")
175
+ last_curated=$(eagle_db "SELECT value FROM eagle_meta WHERE key = 'last_curated_at' AND project = '$p_esc' LIMIT 1;" 2>/dev/null || echo "")
159
176
 
160
177
  if [ -n "$last_curated" ]; then
161
178
  eagle_ok "Curator: last run ${last_curated} (schedule: ${curator_schedule})"
162
179
  score=$((score + 10))
163
180
  elif [ "$curator_schedule" = "auto" ]; then
164
- eagle_warn "Curator: auto-scheduled but hasn't run yet"
181
+ eagle_warn "Curator: auto-scheduled, hasn't run yet (triggers on session start)"
165
182
  score=$((score + 5))
166
- issues+=("Auto-curate is configured but hasn't run. It triggers at session end.")
183
+ issues+=("Auto-curate configured but hasn't fired. Needs ${_min_sessions:-5}+ sessions since last run.")
167
184
  else
168
185
  eagle_fail "Curator: never run (schedule: ${curator_schedule})"
169
- issues+=("Curator has never run. Try: eagle-mem curate --dry-run")
186
+ issues+=("Run: eagle-mem curate --dry-run")
170
187
  fi
171
188
 
172
- # ─── Score ────────────────────────────────────────────────
189
+ # ─── Score ───────���────────────────────────────────────────
173
190
 
174
191
  echo ""
175
- echo -e " ${DIM}─────────────────────────────────────${RESET}"
192
+ echo -e " ${DIM}────���────────────────────────────────${RESET}"
176
193
 
177
194
  pct=$((score * 100 / max_score))
178
195
  if [ "$pct" -ge 80 ]; then
@@ -205,14 +222,17 @@ if [ "$JSON_OUT" -eq 1 ]; then
205
222
  --argjson max_score "$max_score" \
206
223
  --argjson pct "$pct" \
207
224
  --arg grade "$grade" \
225
+ --argjson total_sessions "${total_sessions:-0}" \
208
226
  --argjson total_summaries "${total_summaries:-0}" \
227
+ --argjson heuristic_summaries "${heuristic_summaries:-0}" \
209
228
  --argjson enriched_summaries "${enriched_summaries:-0}" \
210
229
  --argjson features "${feature_count:-0}" \
211
- --argjson command_rules "${rule_count:-0}" \
212
230
  --arg provider "$provider" \
213
231
  --argjson noise_pct "$noise_pct" \
232
+ --arg last_curated "${last_curated:-never}" \
214
233
  '{project:$project, score:$score, max:$max_score, pct:$pct, grade:$grade,
215
- enrichment:{total:$total_summaries, enriched:$enriched_summaries},
216
- features:$features, command_rules:$command_rules,
217
- provider:$provider, noise_pct:$noise_pct}'
234
+ capture:{sessions:$total_sessions, summaries:$total_summaries, heuristic:$heuristic_summaries},
235
+ enrichment:$enriched_summaries,
236
+ features:$features, provider:$provider,
237
+ noise_pct:$noise_pct, last_curated:$last_curated}' >&3
218
238
  fi
@@ -142,7 +142,7 @@ cp "$PACKAGE_DIR"/hooks/*.sh "$EAGLE_MEM_DIR/hooks/"
142
142
  cp "$PACKAGE_DIR"/lib/*.sh "$EAGLE_MEM_DIR/lib/"
143
143
  cp "$PACKAGE_DIR"/db/*.sh "$EAGLE_MEM_DIR/db/"
144
144
  cp "$PACKAGE_DIR"/db/*.sql "$EAGLE_MEM_DIR/db/"
145
- cp "$PACKAGE_DIR"/scripts/statusline-em.sh "$EAGLE_MEM_DIR/scripts/" 2>/dev/null
145
+ cp "$PACKAGE_DIR"/scripts/*.sh "$EAGLE_MEM_DIR/scripts/" 2>/dev/null
146
146
 
147
147
  chmod +x "$EAGLE_MEM_DIR"/hooks/*.sh
148
148
  chmod +x "$EAGLE_MEM_DIR"/db/migrate.sh
package/scripts/update.sh CHANGED
@@ -32,15 +32,17 @@ fi
32
32
 
33
33
  # ─── Update files ──────────────────────────────────────────
34
34
 
35
- mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db}
35
+ mkdir -p "$EAGLE_MEM_DIR"/{hooks,lib,db,scripts}
36
36
 
37
37
  cp "$PACKAGE_DIR"/hooks/*.sh "$EAGLE_MEM_DIR/hooks/"
38
38
  cp "$PACKAGE_DIR"/lib/*.sh "$EAGLE_MEM_DIR/lib/"
39
39
  cp "$PACKAGE_DIR"/db/*.sh "$EAGLE_MEM_DIR/db/"
40
40
  cp "$PACKAGE_DIR"/db/*.sql "$EAGLE_MEM_DIR/db/"
41
+ cp "$PACKAGE_DIR"/scripts/*.sh "$EAGLE_MEM_DIR/scripts/" 2>/dev/null
41
42
 
42
43
  chmod +x "$EAGLE_MEM_DIR"/hooks/*.sh
43
44
  chmod +x "$EAGLE_MEM_DIR"/db/migrate.sh
45
+ chmod +x "$EAGLE_MEM_DIR"/scripts/*.sh 2>/dev/null
44
46
 
45
47
  eagle_ok "Files updated"
46
48