eagle-mem 4.2.1 → 4.6.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.
@@ -81,7 +81,7 @@ fts_query=$(echo "$user_prompt" | tr -cs '[:alnum:]' ' ' | tr '[:upper:]' '[:low
81
81
  results=$(eagle_search_summaries "$fts_query" "$project" 3)
82
82
 
83
83
  if [ -n "$results" ]; then
84
- context+="=== EAGLE MEM — Relevant Memory ===
84
+ context+="=== Eagle Mem — Relevant Memory ===
85
85
  "
86
86
  while IFS='|' read -r req completed learned _next_steps created_at _proj decisions gotchas key_files; do
87
87
  [ -z "$req" ] && [ -z "$completed" ] && continue
@@ -106,7 +106,7 @@ if [ "${has_chunks:-0}" -gt 0 ]; then
106
106
  code_results=$(eagle_search_code_chunks "$fts_query" "$project" 5)
107
107
 
108
108
  if [ -n "$code_results" ]; then
109
- context+="=== EAGLE MEM — Relevant Code ===
109
+ context+="=== Eagle Mem — Relevant Code ===
110
110
  "
111
111
  while IFS='|' read -r fpath sline eline lang; do
112
112
  [ -z "$fpath" ] && continue
@@ -123,7 +123,7 @@ fi
123
123
  context+="
124
124
  IMPORTANT: When Eagle Mem finds relevant memories or code for the user's prompt, briefly mention it at the start of your response: \"Eagle Mem recalled N relevant sessions\" or \"Eagle Mem found related code in [files]\". One line max — then proceed with the answer.
125
125
 
126
- Eagle Mem (persistent memory across sessions)
126
+ === Eagle Mem (persistent memory across sessions) ===
127
127
  "
128
128
 
129
129
  echo "$context"
package/lib/common.sh CHANGED
@@ -39,18 +39,27 @@ eagle_project_from_cwd() {
39
39
  "$HOME/Desktop"|"$HOME/Desktop/"*) echo ""; return ;;
40
40
  esac
41
41
 
42
+ local target_dir
42
43
  local git_root
43
44
  git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
44
45
  if [ -n "$git_root" ]; then
45
- basename "$git_root"
46
+ target_dir="$git_root"
46
47
  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
53
- basename "$cwd"
48
+ target_dir="$cwd"
49
+ fi
50
+
51
+ local name
52
+ name=$(basename "$target_dir")
53
+ if [ ${#name} -le 1 ]; then
54
+ echo ""; return
55
+ fi
56
+
57
+ if [[ "$target_dir" == "$HOME/"* ]]; then
58
+ echo "${target_dir#$HOME/}"
59
+ elif [ "$target_dir" = "$HOME" ]; then
60
+ echo "$name"
61
+ else
62
+ echo "${target_dir#/}"
54
63
  fi
55
64
  }
56
65
 
@@ -66,7 +75,7 @@ eagle_sql_int() {
66
75
  }
67
76
 
68
77
  eagle_fts_sanitize() {
69
- printf '%s' "$1" | sed 's/[*"(){}^~:]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
78
+ printf '%s' "$1" | sed 's/[^A-Za-z0-9_]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
70
79
  }
71
80
 
72
81
  # Escape SQL LIKE wildcards (% and _) so literal filenames match exactly.
@@ -158,17 +167,8 @@ eagle_collect_files() {
158
167
  fi
159
168
  }
160
169
 
161
- eagle_patch_claude_md() {
162
- local claude_md="$HOME/.claude/CLAUDE.md"
163
- local marker="## Eagle Mem — Persistent Memory"
164
-
165
- if [ -f "$claude_md" ] && grep -qF "$marker" "$claude_md" 2>/dev/null; then
166
- return 1
167
- fi
168
-
169
- mkdir -p "$HOME/.claude"
170
-
171
- cat >> "$claude_md" << 'EAGLE_MD'
170
+ _eagle_claude_md_section() {
171
+ cat << 'EAGLE_MD'
172
172
 
173
173
  ---
174
174
 
@@ -180,9 +180,15 @@ Eagle Mem hooks are active in every project. SessionStart injects context (overv
180
180
 
181
181
  ```
182
182
  <eagle-summary>
183
- request: [what user asked] | completed: [what shipped] | learned: [non-obvious discoveries]
184
- next_steps: [concrete actions] | decisions: [choice — why] | gotchas: [what surprised]
185
- key_files: [path — role] | files_read: [...] | files_modified: [...]
183
+ request: [what user asked]
184
+ completed: [what shipped]
185
+ learned: [non-obvious discoveries]
186
+ decisions: [choice — why]
187
+ gotchas: [what surprised]
188
+ next_steps: [concrete actions]
189
+ key_files: [path — role]
190
+ files_read: [path, ...]
191
+ files_modified: [path, ...]
186
192
  </eagle-summary>
187
193
  ```
188
194
 
@@ -196,3 +202,34 @@ key_files: [path — role] | files_read: [...] | files_modified: [...]
196
202
  - If you contradict a loaded memory, update the memory file
197
203
  EAGLE_MD
198
204
  }
205
+
206
+ eagle_patch_claude_md() {
207
+ local claude_md="$HOME/.claude/CLAUDE.md"
208
+ local marker="## Eagle Mem — Persistent Memory"
209
+
210
+ mkdir -p "$HOME/.claude"
211
+
212
+ if [ -f "$claude_md" ] && grep -qF "$marker" "$claude_md" 2>/dev/null; then
213
+ # Check if section has outdated pipe-separated format
214
+ if grep -qF 'request: \[what user asked\] | completed:' "$claude_md" 2>/dev/null; then
215
+ # Replace the outdated section: remove old, append new
216
+ local tmp_md
217
+ tmp_md=$(mktemp)
218
+ awk -v marker="$marker" '
219
+ $0 ~ marker { skip=1; next }
220
+ skip && /^---$/ && !seen_end { seen_end=1; next }
221
+ skip && /^## / { skip=0 }
222
+ !skip { print }
223
+ ' "$claude_md" > "$tmp_md"
224
+ # Remove trailing blank lines left by section removal
225
+ sed -e :a -e '/^[[:space:]]*$/{ $d; N; ba; }' "$tmp_md" > "${tmp_md}.clean"
226
+ mv "${tmp_md}.clean" "$claude_md"
227
+ rm -f "$tmp_md"
228
+ _eagle_claude_md_section >> "$claude_md"
229
+ return 0
230
+ fi
231
+ return 1
232
+ fi
233
+
234
+ _eagle_claude_md_section >> "$claude_md"
235
+ }
@@ -39,16 +39,28 @@ eagle_backfill_projects() {
39
39
  map=$(eagle_build_session_project_map)
40
40
  [ -z "$map" ] && echo "0" && return 0
41
41
 
42
+ # Phase 1: Build old→new project mapping BEFORE mutating any rows.
43
+ # Collect from sessions table so non-session tables can be migrated.
44
+ local rename_map_file
45
+ 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"
56
+
57
+ # Phase 2: Update session-linked tables
42
58
  while IFS='|' read -r sid project; do
43
59
  [ -z "$sid" ] || [ -z "$project" ] && continue
44
60
  local sid_sql proj_sql
45
61
  sid_sql=$(eagle_sql_escape "$sid")
46
62
  proj_sql=$(eagle_sql_escape "$project")
47
63
 
48
- # All six tables updated atomically per session to prevent
49
- # partial backfill if the process is interrupted.
50
- # Note: total_changes() includes FTS trigger changes, so the
51
- # reported count may be higher than actual rows updated.
52
64
  local ch
53
65
  ch=$(eagle_db_pipe <<SQL
54
66
  BEGIN;
@@ -65,6 +77,52 @@ SQL
65
77
  [ "${ch:-0}" -gt 0 ] && updated=$((updated + ch))
66
78
  done <<< "$map"
67
79
 
80
+ # Phase 3: Update non-session tables using the old→new mapping.
81
+ # Skip ambiguous mappings (one old name → multiple new names).
82
+ if [ -s "$rename_map_file" ]; then
83
+ local uniq_map
84
+ uniq_map=$(sort -u "$rename_map_file")
85
+ local prev_old=""
86
+ local ambiguous=""
87
+ while IFS='|' read -r old_proj new_proj; do
88
+ [ -z "$old_proj" ] && continue
89
+ if [ "$old_proj" = "$prev_old" ]; then
90
+ ambiguous+="$old_proj|"
91
+ fi
92
+ prev_old="$old_proj"
93
+ done <<< "$(echo "$uniq_map" | sort -t'|' -k1,1)"
94
+
95
+ while IFS='|' read -r old_proj new_proj; do
96
+ [ -z "$old_proj" ] || [ -z "$new_proj" ] && continue
97
+ case "$ambiguous" in *"$old_proj|"*) continue ;; esac
98
+
99
+ local old_sql new_sql
100
+ old_sql=$(eagle_sql_escape "$old_proj")
101
+ new_sql=$(eagle_sql_escape "$new_proj")
102
+
103
+ eagle_db_pipe <<SQL 2>/dev/null
104
+ BEGIN;
105
+ UPDATE OR IGNORE overviews SET project = '$new_sql' WHERE project = '$old_sql';
106
+ DELETE FROM overviews WHERE project = '$old_sql';
107
+ DELETE FROM code_chunks WHERE project = '$old_sql'
108
+ AND EXISTS (SELECT 1 FROM code_chunks WHERE project = '$new_sql' LIMIT 1);
109
+ UPDATE code_chunks SET project = '$new_sql' WHERE project = '$old_sql';
110
+ UPDATE OR IGNORE features SET project = '$new_sql' WHERE project = '$old_sql';
111
+ DELETE FROM features WHERE project = '$old_sql';
112
+ UPDATE OR IGNORE command_rules SET project = '$new_sql' WHERE project = '$old_sql';
113
+ DELETE FROM command_rules WHERE project = '$old_sql';
114
+ UPDATE OR IGNORE eagle_meta SET project = '$new_sql' WHERE project = '$old_sql';
115
+ DELETE FROM eagle_meta WHERE project = '$old_sql';
116
+ UPDATE OR IGNORE file_hints SET project = '$new_sql' WHERE project = '$old_sql';
117
+ DELETE FROM file_hints WHERE project = '$old_sql';
118
+ UPDATE OR IGNORE guardrails SET project = '$new_sql' WHERE project = '$old_sql';
119
+ DELETE FROM guardrails WHERE project = '$old_sql';
120
+ COMMIT;
121
+ SQL
122
+ done <<< "$uniq_map"
123
+ fi
124
+ rm -f "$rename_map_file"
125
+
68
126
  echo "$updated"
69
127
  }
70
128
 
package/lib/db-core.sh CHANGED
@@ -7,8 +7,9 @@ _EAGLE_DB_CORE_LOADED=1
7
7
 
8
8
  EAGLE_DB_SETUP=".headers off
9
9
  .output /dev/null
10
+ PRAGMA busy_timeout=10000;
11
+ PRAGMA journal_mode=WAL;
10
12
  PRAGMA synchronous=NORMAL;
11
- PRAGMA busy_timeout=5000;
12
13
  PRAGMA foreign_keys=ON;
13
14
  PRAGMA trusted_schema=ON;
14
15
  .output stdout"
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Guardrails helpers
4
+ # Persistent per-project rules surfaced at Edit/Write time
5
+ # ═══════════════════════════════════════════════════════════
6
+ [ -n "${_EAGLE_DB_GUARDRAILS_LOADED:-}" ] && return 0
7
+ _EAGLE_DB_GUARDRAILS_LOADED=1
8
+
9
+ eagle_add_guardrail() {
10
+ local raw_rule="$2"
11
+ # Cap rule length to 2048 chars to prevent unbounded storage
12
+ if [ ${#raw_rule} -gt 2048 ]; then
13
+ raw_rule="${raw_rule:0:2048}"
14
+ fi
15
+
16
+ local project; project=$(eagle_sql_escape "$1")
17
+ local rule; rule=$(eagle_sql_escape "$raw_rule")
18
+ local file_pattern="${3:-}"
19
+ local source; source=$(eagle_sql_escape "${4:-manual}")
20
+
21
+ file_pattern=$(eagle_sql_escape "$file_pattern")
22
+ eagle_db "INSERT INTO guardrails (project, file_pattern, rule, source)
23
+ VALUES ('$project', '$file_pattern', '$rule', '$source')
24
+ ON CONFLICT(project, source, file_pattern, rule) DO UPDATE SET
25
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
26
+ }
27
+
28
+ eagle_get_guardrails_for_file() {
29
+ local project; project=$(eagle_sql_escape "$1")
30
+ local filename; filename=$(eagle_sql_escape "$2")
31
+
32
+ eagle_db "SELECT rule FROM guardrails
33
+ WHERE project = '$project'
34
+ AND active = 1
35
+ AND (
36
+ file_pattern = ''
37
+ OR '$filename' GLOB file_pattern
38
+ OR file_pattern = '$filename'
39
+ )
40
+ ORDER BY file_pattern = '', created_at DESC
41
+ LIMIT 3;"
42
+ }
43
+
44
+ eagle_get_edit_context() {
45
+ local project; project=$(eagle_sql_escape "$1")
46
+ local filename; filename=$(eagle_sql_escape "$2")
47
+ local fts_query; fts_query=$(eagle_sql_escape "$3")
48
+
49
+ # Batched query: guardrails + decisions + gotchas in one sqlite3 call.
50
+ # Results tagged with TYPE: prefix for caller to parse.
51
+ eagle_db_pipe <<SQL
52
+ SELECT 'GR:' || rule FROM guardrails
53
+ WHERE project = '$project'
54
+ AND active = 1
55
+ AND (
56
+ file_pattern = ''
57
+ OR '$filename' GLOB file_pattern
58
+ OR file_pattern = '$filename'
59
+ )
60
+ ORDER BY file_pattern = '', created_at DESC
61
+ LIMIT 3;
62
+ SELECT 'DEC:' || s.decisions
63
+ FROM summaries s
64
+ JOIN summaries_fts f ON f.rowid = s.id
65
+ WHERE summaries_fts MATCH '$fts_query'
66
+ AND s.project = '$project'
67
+ AND s.decisions IS NOT NULL AND s.decisions != ''
68
+ ORDER BY s.created_at DESC LIMIT 1;
69
+ SELECT 'GOT:' || s.gotchas
70
+ FROM summaries s
71
+ JOIN summaries_fts f ON f.rowid = s.id
72
+ WHERE summaries_fts MATCH '$fts_query'
73
+ AND s.project = '$project'
74
+ AND s.gotchas IS NOT NULL AND s.gotchas != ''
75
+ ORDER BY s.created_at DESC LIMIT 2;
76
+ SQL
77
+ }
78
+
79
+ eagle_list_guardrails() {
80
+ local project; project=$(eagle_sql_escape "$1")
81
+
82
+ eagle_db "SELECT id, file_pattern, rule, source, active, created_at
83
+ FROM guardrails
84
+ WHERE project = '$project'
85
+ ORDER BY active DESC, created_at DESC;"
86
+ }
87
+
88
+ eagle_remove_guardrail() {
89
+ local id; id=$(eagle_sql_int "$1")
90
+
91
+ eagle_db "DELETE FROM guardrails WHERE id = $id;"
92
+ }
93
+
94
+ eagle_has_any_guardrails() {
95
+ local project; project=$(eagle_sql_escape "$1")
96
+ eagle_db "SELECT 1 FROM guardrails
97
+ WHERE project = '$project' AND active = 1
98
+ LIMIT 1;"
99
+ }
100
+
101
+ eagle_deactivate_guardrail() {
102
+ local id; id=$(eagle_sql_int "$1")
103
+
104
+ eagle_db "UPDATE guardrails SET active = 0,
105
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
106
+ WHERE id = $id;"
107
+ }
package/lib/db-mirrors.sh CHANGED
@@ -55,6 +55,10 @@ SQL
55
55
 
56
56
  eagle_search_claude_memories() {
57
57
  local query; query=$(eagle_fts_sanitize "$1")
58
+ if [ -z "$query" ]; then
59
+ echo "Search query is empty after sanitization. Try a different search term." >&2
60
+ return 1
61
+ fi
58
62
  query=$(eagle_sql_escape "$query")
59
63
  local project="${2:-}"
60
64
  local limit; limit=$(eagle_sql_int "${3:-10}")
@@ -131,6 +135,10 @@ SQL
131
135
 
132
136
  eagle_search_claude_plans() {
133
137
  local query; query=$(eagle_fts_sanitize "$1")
138
+ if [ -z "$query" ]; then
139
+ echo "Search query is empty after sanitization. Try a different search term." >&2
140
+ return 1
141
+ fi
134
142
  query=$(eagle_sql_escape "$query")
135
143
  local project="${2:-}"
136
144
  local limit; limit=$(eagle_sql_int "${3:-10}")
@@ -242,6 +250,10 @@ eagle_list_claude_tasks() {
242
250
 
243
251
  eagle_search_claude_tasks() {
244
252
  local query; query=$(eagle_fts_sanitize "$1")
253
+ if [ -z "$query" ]; then
254
+ echo "Search query is empty after sanitization. Try a different search term." >&2
255
+ return 1
256
+ fi
245
257
  query=$(eagle_sql_escape "$query")
246
258
  local project="${2:-}"
247
259
  local limit; limit=$(eagle_sql_int "${3:-10}")
@@ -46,12 +46,13 @@ eagle_prune_observations() {
46
46
 
47
47
  eagle_get_command_rule() {
48
48
  local project; project=$(eagle_sql_escape "$1")
49
- local cmd; cmd=$(eagle_sql_escape "$2")
49
+ local base_cmd; base_cmd=$(eagle_sql_escape "$2")
50
+ local full_cmd; full_cmd=$(eagle_sql_escape "${3:-$2}")
50
51
  eagle_db "SELECT strategy, max_lines, reason
51
52
  FROM command_rules
52
53
  WHERE enabled = 1
53
54
  AND (project = '$project' OR project = '')
54
- AND ('$cmd' LIKE pattern OR '$cmd' = pattern)
55
+ AND ('$base_cmd' = pattern OR '$full_cmd' = pattern OR '$full_cmd' LIKE pattern || ' %')
55
56
  ORDER BY CASE WHEN project != '' THEN 0 ELSE 1 END,
56
57
  LENGTH(pattern) DESC
57
58
  LIMIT 1;"
@@ -81,6 +81,7 @@ eagle_search_summaries() {
81
81
  FROM summaries s
82
82
  JOIN summaries_fts f ON f.rowid = s.id
83
83
  WHERE summaries_fts MATCH '$query'
84
+ AND s.request NOT LIKE '%<local-command-caveat>%'
84
85
  $where_clause
85
86
  ORDER BY rank
86
87
  LIMIT $limit;"
@@ -141,6 +142,20 @@ eagle_last_session_enriched() {
141
142
  ORDER BY created_at DESC LIMIT 1;"
142
143
  }
143
144
 
145
+ eagle_search_gotchas_for_file() {
146
+ local project; project=$(eagle_sql_escape "$1")
147
+ local fts_query; fts_query=$(eagle_sql_escape "$2")
148
+ eagle_db "SELECT s.gotchas
149
+ FROM summaries s
150
+ JOIN summaries_fts f ON f.rowid = s.id
151
+ WHERE summaries_fts MATCH '$fts_query'
152
+ AND s.project = '$project'
153
+ AND s.gotchas IS NOT NULL
154
+ AND s.gotchas != ''
155
+ ORDER BY s.created_at DESC
156
+ LIMIT 2;"
157
+ }
158
+
144
159
  eagle_search_stale_memories() {
145
160
  local project; project=$(eagle_sql_escape "$1")
146
161
  local fts_query; fts_query=$(eagle_sql_escape "$2")
package/lib/db.sh CHANGED
@@ -16,3 +16,4 @@ _eagle_db_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  . "$_eagle_db_dir/db-features.sh"
17
17
  . "$_eagle_db_dir/db-hints.sh"
18
18
  . "$_eagle_db_dir/db-backfill.sh"
19
+ . "$_eagle_db_dir/db-guardrails.sh"
@@ -75,7 +75,9 @@ eagle_posttool_stale_hint() {
75
75
  local stale_hit
76
76
  stale_hit=$(eagle_search_stale_memories "$project" "$fts_query")
77
77
  if [ -n "$stale_hit" ]; then
78
- local stale_msg="Eagle Mem: Memory '${stale_hit}' may reference '${fname}'. If your edit contradicts it, update the memory."
78
+ local stale_msg="=== Eagle Mem ===
79
+ Memory '${stale_hit}' may reference '${fname}'. If your edit contradicts it, update the memory.
80
+ ================"
79
81
  jq -nc --arg ctx "$stale_msg" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
80
82
  fi
81
83
  fi
@@ -106,7 +108,10 @@ eagle_posttool_decision_surface() {
106
108
  local decision_hit
107
109
  decision_hit=$(eagle_search_decisions_for_file "$project" "$fts_query")
108
110
  if [ -n "$decision_hit" ]; then
109
- read_context+="Eagle Mem decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request. "
111
+ read_context+="=== Eagle Mem ===
112
+ Decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request.
113
+ ================
114
+ "
110
115
  fi
111
116
  fi
112
117
  fi
@@ -116,14 +121,17 @@ eagle_posttool_decision_surface() {
116
121
  if [ -n "$feature_hit" ]; then
117
122
  while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
118
123
  [ -z "$feat_name" ] && continue
119
- read_context+="Eagle Mem: '${fname}' is part of feature '${feat_name}'"
124
+ read_context+="=== Eagle Mem ===
125
+ '${fname}' is part of feature '${feat_name}'"
120
126
  [ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
121
127
  read_context+="."
122
128
  [ -n "$feat_verified" ] && read_context+=" Last verified: ${feat_verified}."
123
129
  [ -n "$feat_deps" ] && read_context+=" Dependencies: ${feat_deps}."
124
130
  [ -n "$feat_other_files" ] && read_context+=" Other files in pipeline: ${feat_other_files}."
125
131
  [ -n "$feat_smoke" ] && read_context+=" Smoke tests: ${feat_smoke}."
126
- read_context+=" Changes require re-testing after deploy. "
132
+ read_context+=" Changes require re-testing after deploy.
133
+ ================
134
+ "
127
135
  done <<< "$feature_hit"
128
136
  fi
129
137
 
@@ -8,16 +8,22 @@ _EAGLE_HOOKS_SESSIONSTART_LOADED=1
8
8
 
9
9
  _state_dir="$EAGLE_MEM_DIR/state"
10
10
 
11
+ _eagle_state_slug() {
12
+ printf '%s' "$1" | shasum | cut -c1-12
13
+ }
14
+
11
15
  _eagle_state_fresh() {
12
16
  local key="$1" project="$2" max_age_days="${3:-1}"
13
- local state_file="$_state_dir/${key}-${project}"
17
+ local safe_project; safe_project=$(_eagle_state_slug "$project")
18
+ local state_file="$_state_dir/${key}-${safe_project}"
14
19
  [ -f "$state_file" ] && [ -z "$(find "$state_file" -mtime +${max_age_days} 2>/dev/null)" ]
15
20
  }
16
21
 
17
22
  _eagle_state_touch() {
18
23
  local key="$1" project="$2"
24
+ local safe_project; safe_project=$(_eagle_state_slug "$project")
19
25
  mkdir -p "$_state_dir" 2>/dev/null
20
- touch "$_state_dir/${key}-${project}"
26
+ touch "$_state_dir/${key}-${safe_project}"
21
27
  }
22
28
 
23
29
  eagle_sessionstart_auto_provision() {
package/lib/provider.sh CHANGED
@@ -65,13 +65,22 @@ eagle_config_set() {
65
65
  safe_value=$(printf '%s' "$value" | sed 's/[|&/\]/\\&/g')
66
66
 
67
67
  if grep -q "^\[${section}\]" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
68
- if grep -q "^[[:space:]]*${key}[[:space:]]*=" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
69
- sed -i '' "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${safe_value}\"|" "$EAGLE_CONFIG_FILE"
70
- else
71
- sed -i '' "/^\[${section}\]/a\\
72
- ${key} = \"${safe_value}\"
73
- " "$EAGLE_CONFIG_FILE"
74
- fi
68
+ local tmp_cfg="${EAGLE_CONFIG_FILE}.tmp.$$"
69
+ awk -v sect="[${section}]" -v k="$key" -v v="$safe_value" '
70
+ BEGIN { in_sect=0; replaced=0 }
71
+ /^\[/ {
72
+ if (in_sect && !replaced) {
73
+ print k" = \""v"\""
74
+ replaced=1
75
+ }
76
+ in_sect=($0 == sect)
77
+ }
78
+ in_sect && !replaced && $0 ~ "^[[:space:]]*"k"[[:space:]]*=" {
79
+ print k" = \""v"\""; replaced=1; next
80
+ }
81
+ { print }
82
+ END { if (in_sect && !replaced) print k" = \""v"\"" }
83
+ ' "$EAGLE_CONFIG_FILE" > "$tmp_cfg" && mv "$tmp_cfg" "$EAGLE_CONFIG_FILE"
75
84
  else
76
85
  # printf is safe — no sed interpolation needed for append
77
86
  printf '\n[%s]\n%s = "%s"\n' "$section" "$key" "$value" >> "$EAGLE_CONFIG_FILE"
@@ -95,7 +104,7 @@ eagle_ollama_best_model() {
95
104
  models=$(eagle_ollama_models "$1")
96
105
  [ -z "$models" ] && return 1
97
106
 
98
- local preferred="mistral qwen3-coder gemma4 llama3 phi3 deepseek-coder"
107
+ local preferred="gemma4 gemma3 gemma2 mistral llama3 phi3 deepseek-coder"
99
108
  for pref in $preferred; do
100
109
  if echo "$models" | grep -qi "$pref"; then
101
110
  echo "$models" | grep -i "$pref" | head -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.2.1",
3
+ "version": "4.6.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/curate.sh CHANGED
@@ -89,7 +89,8 @@ If none qualify, output: NONE"
89
89
  if [ "$DRY_RUN" -eq 1 ]; then
90
90
  eagle_info " Would promote: $gotcha_text"
91
91
  else
92
- eagle_log "INFO" "Curator: promoting gotcha: $gotcha_text"
92
+ eagle_add_guardrail "$project" "$gotcha_text" "" "promoted"
93
+ eagle_log "INFO" "Curator: promoted gotcha to guardrail: $gotcha_text"
93
94
  fi
94
95
  promoted=$((promoted + 1))
95
96
  ;;