eagle-mem 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/eagle-mem CHANGED
@@ -22,6 +22,15 @@ case "$command" in
22
22
  search) bash "$SCRIPTS_DIR/search.sh" "$@" ;;
23
23
  health) bash "$SCRIPTS_DIR/health.sh" "$@" ;;
24
24
  config) bash "$SCRIPTS_DIR/config.sh" "$@" ;;
25
+ guard) bash "$SCRIPTS_DIR/guard.sh" "$@" ;;
26
+ overview) bash "$SCRIPTS_DIR/overview.sh" "$@" ;;
27
+ memories) bash "$SCRIPTS_DIR/memories.sh" "$@" ;;
28
+ tasks) bash "$SCRIPTS_DIR/tasks.sh" "$@" ;;
29
+ curate) bash "$SCRIPTS_DIR/curate.sh" "$@" ;;
30
+ feature) bash "$SCRIPTS_DIR/feature.sh" "$@" ;;
31
+ prune) bash "$SCRIPTS_DIR/prune.sh" "$@" ;;
32
+ scan) bash "$SCRIPTS_DIR/scan.sh" "$@" ;;
33
+ index) bash "$SCRIPTS_DIR/index.sh" "$@" ;;
25
34
  help|--help|-h)
26
35
  bash "$SCRIPTS_DIR/help.sh" ;;
27
36
  version|--version|-v|-V)
@@ -0,0 +1,12 @@
1
+ -- Clear fossil placeholder data that the COALESCE upsert preserves forever.
2
+ -- These rows block real data from being written on future Stop hook fires.
3
+ PRAGMA trusted_schema=ON;
4
+
5
+ UPDATE summaries SET completed = ''
6
+ WHERE completed LIKE '%(auto-captured%';
7
+
8
+ UPDATE summaries SET request = ''
9
+ WHERE request LIKE '%<local-command-caveat>%';
10
+
11
+ UPDATE summaries SET request = ''
12
+ WHERE request LIKE '%<system-reminder>%';
@@ -0,0 +1,18 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Migration 023: Guardrails table
3
+ -- Persistent per-project rules surfaced at Edit/Write time
4
+ -- ═══════════════════════════════════════════════════════════
5
+
6
+ CREATE TABLE IF NOT EXISTS guardrails (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ project TEXT NOT NULL,
9
+ file_pattern TEXT NOT NULL DEFAULT '',
10
+ rule TEXT NOT NULL,
11
+ source TEXT NOT NULL DEFAULT 'manual',
12
+ active INTEGER NOT NULL DEFAULT 1,
13
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
14
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
15
+ UNIQUE(project, source, file_pattern, rule)
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_guardrails_project ON guardrails(project, active);
@@ -20,7 +20,23 @@ session_id=$(echo "$input" | jq -r '.session_id // empty')
20
20
  cwd=$(echo "$input" | jq -r '.cwd // empty')
21
21
  tool_name=$(echo "$input" | jq -r '.tool_name // empty')
22
22
 
23
- if [ -z "$session_id" ] || [ -z "$tool_name" ]; then exit 0; fi
23
+ hook_event=$(echo "$input" | jq -r '.hook_event_name // empty')
24
+
25
+ if [ -z "$session_id" ]; then exit 0; fi
26
+
27
+ # TaskCreated/TaskCompleted dedicated events — mirror tasks and exit
28
+ case "$hook_event" in
29
+ TaskCreated|TaskCompleted)
30
+ [ ! -f "$EAGLE_MEM_DB" ] && exit 0
31
+ project=$(eagle_project_from_cwd "$cwd")
32
+ [ -z "$project" ] && exit 0
33
+ eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
34
+ eagle_posttool_mirror_tasks "TaskCreate" "$session_id" "$project" "$input"
35
+ exit 0
36
+ ;;
37
+ esac
38
+
39
+ [ -z "$tool_name" ] && exit 0
24
40
 
25
41
  # Only track relevant tools
26
42
  case "$tool_name" in
@@ -121,6 +121,42 @@ ${context}"
121
121
  Edit|Write)
122
122
  fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
123
123
  if [ -n "$fp" ]; then
124
+ # ─── Guardrail + decision/gotcha surfacing ────────
125
+ fname=$(basename "$fp")
126
+ fname_stem="${fname%.*}"
127
+ case "$fp" in
128
+ "$HOME/.claude/"*) ;;
129
+ *)
130
+ # Guardrails use GLOB on full filename — no stem length minimum needed.
131
+ # FTS decision/gotcha lookups need a meaningful stem (>= 3 chars).
132
+ if [ ${#fname_stem} -ge 3 ]; then
133
+ fts_query=$(eagle_fts_sanitize "$fname_stem")
134
+ fts_query=${fts_query:-"$fname_stem"}
135
+ edit_ctx=$(eagle_get_edit_context "$project" "$fname" "$fts_query" 2>/dev/null)
136
+ else
137
+ # Short stem (e.g. db.sh) — only fetch guardrails, skip FTS queries
138
+ edit_ctx=$(eagle_get_guardrails_for_file "$project" "$fname" 2>/dev/null)
139
+ if [ -n "$edit_ctx" ]; then
140
+ # Prefix with GR: to match batched output format; strip empty lines
141
+ edit_ctx=$(echo "$edit_ctx" | grep -v '^$' | sed 's/^/GR:/')
142
+ fi
143
+ fi
144
+ if [ -n "$edit_ctx" ]; then
145
+ gr_block=""
146
+ while IFS= read -r ctx_line; do
147
+ case "$ctx_line" in
148
+ GR:*) gr_block+=" - ${ctx_line#GR:}"$'\n' ;;
149
+ DEC:*) context+="Eagle Mem decisions for '${fname}': ${ctx_line#DEC:} — Do not revert without asking. " ;;
150
+ GOT:*) context+="Eagle Mem gotchas for '${fname}': ${ctx_line#GOT:} " ;;
151
+ esac
152
+ done <<< "$edit_ctx"
153
+ if [ -n "$gr_block" ]; then
154
+ context+="Eagle Mem guardrails for '${fname}':"$'\n'"${gr_block}"
155
+ fi
156
+ fi
157
+ ;;
158
+ esac
159
+
124
160
  # ─── Stuck loop detection ─────────────────────────
125
161
  if [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
126
162
  edit_tracker="$EAGLE_MEM_DIR/edit-tracker/${session_id}"
package/hooks/stop.sh CHANGED
@@ -42,7 +42,13 @@ eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
42
42
 
43
43
  # ─── Primary: heuristic extraction from transcript ───────────
44
44
 
45
- 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)
45
+ 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 \
46
+ | grep -v '<local-command-caveat>' \
47
+ | grep -v '<system-reminder>' \
48
+ | grep -v '<command-name>' \
49
+ | grep -v '<command-message>' \
50
+ | grep -v '^\[{' \
51
+ | head -1 | cut -c1-500)
46
52
 
47
53
  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)
48
54
  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)
@@ -124,59 +130,132 @@ if [ -n "$summary_block" ]; then
124
130
  eagle_log "INFO" "Stop: eagle-summary block merged over heuristic data"
125
131
  fi
126
132
 
127
- # ─── LLM enrichment: fill in decisions/gotchas/key_files ─────
128
- # Check both local vars (from eagle-summary block) AND existing DB enrichment.
129
- # Skip LLM call if either source already has enrichment data.
130
- # Exception: under context pressure, force re-enrichment for richest summary.
133
+ # ─── LLM enrichment: extract structured data when eagle-summary absent ──
134
+ # Runs when: (a) no eagle-summary block, OR (b) heuristic data is thin
135
+ # Skips when: eagle-summary already provided rich data, OR no text to analyze
131
136
 
132
137
  context_pressure=0
133
138
  if [ -f "$EAGLE_MEM_DIR/.context-pressure" ]; then
134
139
  context_pressure=1
135
140
  fi
136
141
 
137
- existing_enrichment=""
138
- if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ]; then
139
- s_esc=$(eagle_sql_escape "$session_id")
140
- existing_enrichment=$(eagle_db "SELECT decisions||gotchas||key_files FROM summaries WHERE session_id='$s_esc';")
142
+ has_rich_data=0
143
+ if [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ]; then
144
+ has_rich_data=1
141
145
  fi
142
146
 
143
- if [ -z "$decisions" ] && [ -z "$gotchas" ] && [ -z "$key_files" ] && { [ -z "$existing_enrichment" ] || [ "$context_pressure" -eq 1 ]; }; then
147
+ request_is_polluted=0
148
+ if echo "$request" | grep -qE '<(local-command-caveat|system-reminder|command-name)>' 2>/dev/null; then
149
+ request_is_polluted=1
150
+ fi
151
+
152
+ needs_enrichment=0
153
+ if [ "$has_rich_data" -eq 0 ]; then
154
+ needs_enrichment=1
155
+ elif [ "$context_pressure" -eq 1 ]; then
156
+ needs_enrichment=1
157
+ elif [ -z "$completed" ] && [ -z "$learned" ]; then
158
+ needs_enrichment=1
159
+ elif [ "$request_is_polluted" -eq 1 ]; then
160
+ needs_enrichment=1
161
+ fi
162
+
163
+ if [ "$needs_enrichment" -eq 1 ]; then
144
164
  provider=$(eagle_config_get "provider" "type" "none" 2>/dev/null)
145
165
  if [ "$provider" != "none" ] && [ -n "$text_content" ]; then
146
- excerpt=$(echo "$text_content" | tail -c 2000)
166
+ excerpt=$(echo "$text_content" | tail -c 3000)
147
167
 
148
- enrich_prompt="Extract from this Claude Code session excerpt:
149
- 1. DECISIONS: architectural or design choices made (with WHY). One per line.
150
- 2. GOTCHAS: non-obvious pitfalls, bugs found, things that surprised. One per line.
151
- 3. KEY_FILES: important files that were central to the work. One per line.
168
+ enrich_prompt="Extract facts from this Claude Code session. Only include items with clear evidence in the session text. Do NOT invent or repeat example content.
152
169
 
153
- SESSION EXCERPT:
154
- $excerpt
170
+ Respond with EXACTLY these sections (omit sections with no evidence):
171
+
172
+ REQUEST:
173
+ One-line summary of what the user asked for. No system tags or XML.
174
+
175
+ COMPLETED:
176
+ What was actually accomplished. Be specific about changes made.
177
+
178
+ LEARNED:
179
+ Non-obvious discoveries or insights from the session.
155
180
 
156
- Output EXACTLY this format (omit sections with nothing to report):
157
181
  DECISIONS:
158
- - <decision> — why: <reason>
182
+ Each as: <what was decided> — why: <reason>
183
+
159
184
  GOTCHAS:
160
- - <gotcha>
185
+ Each as: <surprising finding or pitfall>
186
+
161
187
  KEY_FILES:
162
- - <filepath>"
188
+ Each as: <filepath>
189
+
190
+ SESSION TEXT:
191
+ $excerpt"
163
192
 
164
- 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
193
+ enrich_system="You extract structured facts from development sessions. Output format for decisions: '- Did X — why: Y'. Output format for gotchas: '- Gotcha description'. Be concise. Only include items with clear evidence in the session text. Never fabricate content."
194
+ enrich_result=$(eagle_llm_call "$enrich_prompt" "$enrich_system" 768 2>/dev/null)
195
+ llm_rc=$?
165
196
 
166
- if [ -n "$enrich_result" ]; then
197
+ if [ $llm_rc -ne 0 ] || [ -z "$enrich_result" ]; then
198
+ eagle_log "WARN" "Stop: LLM enrichment failed (rc=$llm_rc) for session=$session_id provider=$provider"
199
+ else
167
200
  extract_section() {
168
201
  local result="$1" header="$2"
169
202
  echo "$result" | awk -v h="$header:" '
170
203
  $0 == h || $0 ~ "^"h { found=1; next }
171
204
  found && /^[A-Z_]+:/ { exit }
172
- found && /^- / { sub(/^- /, ""); lines[++n] = $0 }
205
+ found && /^[[:space:]]*$/ { next }
206
+ found && /^- / { sub(/^- /, ""); lines[++n] = $0; next }
207
+ found { lines[++n] = $0 }
173
208
  END { for (i=1; i<=n; i++) { printf "%s", lines[i]; if (i<n) printf "; " } }
174
209
  '
175
210
  }
176
- decisions=$(extract_section "$enrich_result" "DECISIONS")
177
- gotchas=$(extract_section "$enrich_result" "GOTCHAS")
178
- key_files=$(extract_section "$enrich_result" "KEY_FILES")
179
- [ -n "$decisions" ] || [ -n "$gotchas" ] || [ -n "$key_files" ] && eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id"
211
+ _req=$(extract_section "$enrich_result" "REQUEST")
212
+ _comp=$(extract_section "$enrich_result" "COMPLETED")
213
+ _learn=$(extract_section "$enrich_result" "LEARNED")
214
+ _dec=$(extract_section "$enrich_result" "DECISIONS")
215
+ _got=$(extract_section "$enrich_result" "GOTCHAS")
216
+ _kf=$(extract_section "$enrich_result" "KEY_FILES")
217
+
218
+ [ -z "$request" ] || [ "$request_is_polluted" -eq 1 ] && [ -n "$_req" ] && request="$_req"
219
+ [ -z "$completed" ] && [ -n "$_comp" ] && completed="$_comp"
220
+ [ -z "$learned" ] && [ -n "$_learn" ] && learned="$_learn"
221
+ [ -z "$decisions" ] && [ -n "$_dec" ] && decisions="$_dec"
222
+ [ -z "$gotchas" ] && [ -n "$_got" ] && gotchas="$_got"
223
+ [ -z "$key_files" ] && [ -n "$_kf" ] && key_files="$_kf"
224
+
225
+ eagle_log "INFO" "Stop: LLM enrichment extracted for session=$session_id (req=${#_req} comp=${#_comp} dec=${#_dec})"
226
+ fi
227
+ else
228
+ eagle_log "INFO" "Stop: LLM enrichment skipped — provider=$provider text_len=${#text_content}"
229
+ fi
230
+ else
231
+ eagle_log "INFO" "Stop: LLM enrichment skipped — rich data already present"
232
+ fi
233
+
234
+ # ─── Test reminder for guardrailed files ─────────────────
235
+
236
+ if [ -n "$files_modified" ] && [ "$files_modified" != "[]" ]; then
237
+ # Short-circuit: skip per-file loop if project has no guardrails at all
238
+ has_gr=$(eagle_has_any_guardrails "$project" 2>/dev/null)
239
+ if [ -n "$has_gr" ]; then
240
+ guardrailed_files=""
241
+ while IFS= read -r mod_file; do
242
+ [ -z "$mod_file" ] && continue
243
+ mod_basename=$(basename "$mod_file")
244
+ gr_check=$(eagle_get_guardrails_for_file "$project" "$mod_basename" 2>/dev/null)
245
+ if [ -n "$gr_check" ]; then
246
+ guardrailed_files+="${mod_basename}, "
247
+ fi
248
+ done < <(echo "$files_modified" | jq -r '.[]?' 2>/dev/null)
249
+
250
+ if [ -n "$guardrailed_files" ]; then
251
+ guardrailed_files=${guardrailed_files%, }
252
+ test_reminder="Run affected tests for guardrailed files: ${guardrailed_files}"
253
+ if [ -n "$next_steps" ]; then
254
+ next_steps="${next_steps}; ${test_reminder}"
255
+ else
256
+ next_steps="$test_reminder"
257
+ fi
258
+ eagle_log "INFO" "Stop: added test reminder for guardrailed files: $guardrailed_files"
180
259
  fi
181
260
  fi
182
261
  fi
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
+ }
@@ -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"
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.4.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
  ;;
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Guardrails management
4
+ # eagle-mem guard [add|list|remove]
5
+ # ═══════════════════════════════════════════════════════════
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ LIB_DIR="$SCRIPT_DIR/../lib"
10
+
11
+ . "$LIB_DIR/common.sh"
12
+ . "$LIB_DIR/db.sh"
13
+ . "$SCRIPT_DIR/style.sh"
14
+
15
+ eagle_ensure_db
16
+
17
+ project=$(eagle_project_from_cwd "$(pwd)")
18
+ if [ -z "$project" ]; then
19
+ eagle_err "Not in a recognized project directory"
20
+ exit 1
21
+ fi
22
+
23
+ eagle_header "Guardrails"
24
+
25
+ subcommand="${1:-list}"
26
+ shift 2>/dev/null || true
27
+
28
+ case "$subcommand" in
29
+ add)
30
+ rule="${1:-}"
31
+ if [ -z "$rule" ]; then
32
+ eagle_err "Usage: eagle-mem guard add \"rule text\" [--file pattern]"
33
+ eagle_info "Examples:"
34
+ eagle_info " eagle-mem guard add \"PRAGMA busy_timeout must precede synchronous\" --file \"db-core.sh\""
35
+ eagle_info " eagle-mem guard add \"Never manually copy files to ~/.eagle-mem\""
36
+ eagle_info " eagle-mem guard add \"Always validate session IDs\" --file \"*.sh\""
37
+ exit 1
38
+ fi
39
+ shift
40
+
41
+ file_pattern=""
42
+ while [ $# -gt 0 ]; do
43
+ case "$1" in
44
+ --file|-f)
45
+ if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
46
+ eagle_err "--file requires a pattern argument"
47
+ exit 1
48
+ fi
49
+ file_pattern="$2"
50
+ shift 2
51
+ ;;
52
+ *)
53
+ shift
54
+ ;;
55
+ esac
56
+ done
57
+
58
+ eagle_add_guardrail "$project" "$rule" "$file_pattern" "manual"
59
+ eagle_ok "Guardrail added for project: $project"
60
+ if [ -n "$file_pattern" ]; then
61
+ eagle_info "File pattern: $file_pattern"
62
+ else
63
+ eagle_info "Scope: project-wide"
64
+ fi
65
+ eagle_dim "Rule: $rule"
66
+ ;;
67
+
68
+ list|ls)
69
+ results=$(eagle_list_guardrails "$project")
70
+ if [ -z "$results" ]; then
71
+ eagle_info "No guardrails for project: $project"
72
+ eagle_dim "Add one: eagle-mem guard add \"rule\" [--file pattern]"
73
+ exit 0
74
+ fi
75
+
76
+ echo -e " ${BOLD}ID File Pattern Rule Source Active${RESET}"
77
+ echo -e " ${DIM}──── ──────────────────── ──────────────────────────────────────── ──────── ──────${RESET}"
78
+
79
+ while IFS='|' read -r id pat rule source active _created; do
80
+ [ -z "$id" ] && continue
81
+ pat="${pat:-(all)}"
82
+ rule_display="${rule:0:40}"
83
+ [ ${#rule} -gt 40 ] && rule_display="${rule_display}..."
84
+ if [ "$active" = "1" ]; then
85
+ active_display="${GREEN}yes${RESET}"
86
+ else
87
+ active_display="${DIM}no${RESET}"
88
+ fi
89
+ printf " %-4s %-20s %-40s %-8s %b\n" "$id" "$pat" "$rule_display" "$source" "$active_display"
90
+ done <<< "$results"
91
+ echo ""
92
+ ;;
93
+
94
+ remove|rm|delete)
95
+ id="${1:-}"
96
+ if [ -z "$id" ]; then
97
+ eagle_err "Usage: eagle-mem guard remove <id>"
98
+ exit 1
99
+ fi
100
+ case "$id" in
101
+ *[!0-9]*)
102
+ eagle_err "Invalid ID: '$id' (must be numeric)"
103
+ exit 1
104
+ ;;
105
+ esac
106
+ eagle_remove_guardrail "$id"
107
+ eagle_ok "Guardrail #$id removed"
108
+ ;;
109
+
110
+ sync)
111
+ eagle_info "Syncing rules from CLAUDE.md files..."
112
+
113
+ # Collect rules first, then delete+insert in a single transaction
114
+ # to prevent data loss if interrupted mid-sync
115
+ sync_sql=""
116
+ synced=0
117
+ p_esc=$(eagle_sql_escape "$project")
118
+
119
+ for claude_md in "$HOME/.claude/CLAUDE.md" ".claude/CLAUDE.md" "CLAUDE.md"; do
120
+ [ ! -f "$claude_md" ] && continue
121
+ eagle_dim " Reading: $claude_md"
122
+
123
+ # Extract lines that look like imperative rules
124
+ while IFS= read -r line; do
125
+ # Strip markdown formatting
126
+ clean=$(printf '%s\n' "$line" | sed 's/^[[:space:]]*[-*>]*[[:space:]]*//' | sed 's/\*\*//g' | sed 's/`//g')
127
+ [ -z "$clean" ] && continue
128
+ # Skip headings, short lines, and non-rule content
129
+ [ ${#clean} -lt 15 ] && continue
130
+ case "$clean" in \#*) continue ;; esac
131
+
132
+ # Cap rule length
133
+ [ ${#clean} -gt 2048 ] && clean="${clean:0:2048}"
134
+ rule_esc=$(eagle_sql_escape "$clean")
135
+ sync_sql+="INSERT OR IGNORE INTO guardrails (project, file_pattern, rule, source) VALUES ('$p_esc', '', '$rule_esc', 'claude-md');"$'\n'
136
+ synced=$((synced + 1))
137
+ done < <(grep -iE '^[[:space:]]*[-*>]*[[:space:]]*(never |always |do not |don'\''t |must |rule:|important:)' "$claude_md" 2>/dev/null)
138
+ done
139
+
140
+ # Atomic: always delete stale + insert new in one transaction
141
+ # Even when synced=0, DELETE must run to clear removed rules
142
+ eagle_db_pipe <<SQL
143
+ BEGIN;
144
+ DELETE FROM guardrails WHERE project = '$p_esc' AND source = 'claude-md';
145
+ $sync_sql
146
+ COMMIT;
147
+ SQL
148
+ if [ "$synced" -gt 0 ]; then
149
+ eagle_ok "$synced rules synced from CLAUDE.md"
150
+ else
151
+ eagle_info "No imperative rules found in CLAUDE.md files (stale rules cleared)"
152
+ fi
153
+ ;;
154
+
155
+ *)
156
+ eagle_err "Unknown guard command: $subcommand"
157
+ eagle_info "Usage: eagle-mem guard [add|list|remove|sync]"
158
+ exit 1
159
+ ;;
160
+ esac
package/scripts/help.sh CHANGED
@@ -22,6 +22,13 @@ echo -e " ${CYAN}uninstall${RESET} Remove hooks and optionally delete data"
22
22
  echo -e " ${CYAN}search${RESET} Search past sessions, memories, and code"
23
23
  echo -e " ${CYAN}health${RESET} Diagnose pipeline health and background automation"
24
24
  echo -e " ${CYAN}config${RESET} View or change LLM provider settings"
25
+ echo -e " ${CYAN}guard${RESET} Manage regression guardrails for files"
26
+ echo -e " ${CYAN}overview${RESET} Build or view project overview"
27
+ echo -e " ${CYAN}memories${RESET} View/sync Claude Code memories"
28
+ echo -e " ${CYAN}tasks${RESET} View mirrored tasks"
29
+ echo -e " ${CYAN}curate${RESET} Run curator (co-edits, hot files, guardrails)"
30
+ echo -e " ${CYAN}feature${RESET} Track and verify features"
31
+ echo -e " ${CYAN}prune${RESET} Clean old sessions and stale data"
25
32
  echo ""
26
33
  echo -e " ${BOLD}Search modes:${RESET}"
27
34
  echo -e " ${DIM}\$${RESET} eagle-mem search \"auth bug\" ${DIM}# keyword search${RESET}"
@@ -176,6 +176,14 @@ eagle_patch_hook "$SETTINGS" "PostToolUse" "Read|Write|Edit|Bash|TaskCreate|Task
176
176
  "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
177
177
  "PostToolUse hook"
178
178
 
179
+ eagle_patch_hook "$SETTINGS" "TaskCreated" "" \
180
+ "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
181
+ "TaskCreated hook"
182
+
183
+ eagle_patch_hook "$SETTINGS" "TaskCompleted" "" \
184
+ "$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
185
+ "TaskCompleted hook"
186
+
179
187
  eagle_patch_hook "$SETTINGS" "SessionEnd" "" \
180
188
  "$EAGLE_MEM_DIR/hooks/session-end.sh" \
181
189
  "SessionEnd hook"
@@ -16,8 +16,12 @@ eagle_ensure_db
16
16
 
17
17
  # ─── Parse arguments ──────────────────────────────────────
18
18
 
19
- action="${1:-show}"
20
- shift 2>/dev/null || true
19
+ action="show"
20
+ case "${1:-}" in
21
+ -*) ;; # flags parsed below
22
+ "") ;;
23
+ *) action="$1"; shift ;;
24
+ esac
21
25
 
22
26
  project=""
23
27
  json_output=false
@@ -36,7 +40,7 @@ show_help() {
36
40
  echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current dir)"
37
41
  echo -e " ${CYAN}-j, --json${RESET} Output as JSON"
38
42
  echo ""
39
- echo -e " ${BOLD}Tip:${RESET} Use ${CYAN}eagle-mem scan${RESET} to auto-generate an overview from code."
43
+ echo -e " ${BOLD}Tip:${RESET} Overviews are auto-generated during ${CYAN}eagle-mem install${RESET} and background scans."
40
44
  echo ""
41
45
  exit 0
42
46
  }
@@ -66,7 +70,7 @@ overview_show() {
66
70
 
67
71
  if [ -z "$content" ]; then
68
72
  eagle_dim "No overview for project '$project'"
69
- eagle_dim "Run 'eagle-mem scan' or 'eagle-mem overview set <text>' to create one"
73
+ eagle_dim "Use 'eagle-mem overview set <text>' to create one, or run install/update to auto-generate"
70
74
  return
71
75
  fi
72
76
 
package/scripts/search.sh CHANGED
@@ -78,7 +78,13 @@ limit=$(eagle_sql_int "$limit")
78
78
  # ─── Keyword search ──────────────────────────────────────
79
79
 
80
80
  search_keyword() {
81
- local q; q=$(eagle_sql_escape "$(eagle_fts_sanitize "$query")")
81
+ local sanitized_q
82
+ sanitized_q=$(eagle_fts_sanitize "$query")
83
+ if [ -z "$sanitized_q" ]; then
84
+ eagle_err "Search query contains no valid search terms"
85
+ exit 1
86
+ fi
87
+ local q; q=$(eagle_sql_escape "$sanitized_q")
82
88
  local p; p=$(eagle_sql_escape "$project")
83
89
 
84
90
  local where_project=""
@@ -328,7 +334,13 @@ search_memories() {
328
334
  fi
329
335
 
330
336
  if [ -n "$query" ]; then
331
- local q; q=$(eagle_sql_escape "$(eagle_fts_sanitize "$query")")
337
+ local sanitized_mq
338
+ sanitized_mq=$(eagle_fts_sanitize "$query")
339
+ if [ -z "$sanitized_mq" ]; then
340
+ eagle_err "Search query contains no valid search terms"
341
+ exit 1
342
+ fi
343
+ local q; q=$(eagle_sql_escape "$sanitized_mq")
332
344
  local where_match="WHERE claude_memories_fts MATCH '$q'"
333
345
  if [ "$cross_project" = false ]; then
334
346
  where_match="$where_match AND m.project = '$p'"
@@ -409,7 +421,13 @@ search_tasks() {
409
421
  fi
410
422
 
411
423
  if [ -n "$query" ]; then
412
- local q; q=$(eagle_sql_escape "$(eagle_fts_sanitize "$query")")
424
+ local sanitized_tq
425
+ sanitized_tq=$(eagle_fts_sanitize "$query")
426
+ if [ -z "$sanitized_tq" ]; then
427
+ eagle_err "Search query contains no valid search terms"
428
+ exit 1
429
+ fi
430
+ local q; q=$(eagle_sql_escape "$sanitized_tq")
413
431
 
414
432
  if [ "$json_output" = true ]; then
415
433
  eagle_db_json "SELECT t.subject, t.status, t.project, t.updated_at
package/scripts/tasks.sh CHANGED
@@ -17,8 +17,12 @@ eagle_ensure_db
17
17
 
18
18
  # ─── Parse arguments ──────────────────────────────────────
19
19
 
20
- action="${1:-pending}"
21
- shift 2>/dev/null || true
20
+ action="pending"
21
+ case "${1:-}" in
22
+ -*) ;; # flags parsed below
23
+ "") ;;
24
+ *) action="$1"; shift ;;
25
+ esac
22
26
 
23
27
  project=""
24
28
  json_output=false
@@ -123,9 +127,15 @@ tasks_search() {
123
127
  exit 1
124
128
  fi
125
129
 
130
+ local sanitized_query
131
+ sanitized_query=$(eagle_fts_sanitize "$query")
132
+ if [ -z "$sanitized_query" ]; then
133
+ eagle_err "Search query contains no valid search terms"
134
+ exit 1
135
+ fi
136
+
126
137
  if [ "$json_output" = true ]; then
127
- local query_sql; query_sql=$(eagle_fts_sanitize "$query")
128
- query_sql=$(eagle_sql_escape "$query_sql")
138
+ local query_sql; query_sql=$(eagle_sql_escape "$sanitized_query")
129
139
  eagle_db_json "SELECT t.source_task_id, t.subject, t.status, t.description, t.updated_at
130
140
  FROM claude_tasks t
131
141
  JOIN claude_tasks_fts f ON f.rowid = t.id
@@ -137,8 +147,7 @@ tasks_search() {
137
147
  fi
138
148
 
139
149
  local results
140
- local query_sql; query_sql=$(eagle_fts_sanitize "$query")
141
- query_sql=$(eagle_sql_escape "$query_sql")
150
+ local query_sql; query_sql=$(eagle_sql_escape "$sanitized_query")
142
151
  results=$(eagle_db "SELECT t.source_task_id, t.subject, t.status, t.description
143
152
  FROM claude_tasks t
144
153
  JOIN claude_tasks_fts f ON f.rowid = t.id
package/scripts/update.sh CHANGED
@@ -73,6 +73,8 @@ if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
73
73
  eagle_patch_hook "$SETTINGS" "SessionStart" "" "$EAGLE_MEM_DIR/hooks/session-start.sh"
74
74
  eagle_patch_hook "$SETTINGS" "Stop" "" "$EAGLE_MEM_DIR/hooks/stop.sh"
75
75
  eagle_patch_hook "$SETTINGS" "PostToolUse" "Read|Write|Edit|Bash|TaskCreate|TaskUpdate" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
76
+ eagle_patch_hook "$SETTINGS" "TaskCreated" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
77
+ eagle_patch_hook "$SETTINGS" "TaskCompleted" "" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
76
78
  eagle_patch_hook "$SETTINGS" "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh"
77
79
  eagle_patch_hook "$SETTINGS" "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
78
80
  eagle_patch_hook "$SETTINGS" "PreToolUse" "Bash" "$EAGLE_MEM_DIR/hooks/pre-tool-use.sh"