eagle-mem 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — PostToolUse extracted responsibilities
4
+ # Source from hooks/post-tool-use.sh after common.sh + db.sh
5
+ # ═══════════════════════════════════════════════════════════
6
+ [ -n "${_EAGLE_HOOKS_POSTTOOL_LOADED:-}" ] && return 0
7
+ _EAGLE_HOOKS_POSTTOOL_LOADED=1
8
+
9
+ eagle_posttool_mirror_writes() {
10
+ local tool_name="$1" fp="$2" session_id="$3" project="$4"
11
+
12
+ case "$tool_name" in
13
+ Write|Edit)
14
+ if [ -n "$fp" ]; then
15
+ case "$fp" in
16
+ *..*) ;; # path traversal — skip
17
+ "$EAGLE_CLAUDE_PROJECTS_DIR"/*/memory/*.md)
18
+ local mem_base
19
+ mem_base=$(basename "$fp")
20
+ if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
21
+ eagle_capture_claude_memory "$fp" "$session_id" "$project"
22
+ fi
23
+ ;;
24
+ "$EAGLE_CLAUDE_PLANS_DIR/"*.md)
25
+ if [ -f "$fp" ]; then
26
+ eagle_capture_claude_plan "$fp" "$session_id" "$project"
27
+ fi
28
+ ;;
29
+ esac
30
+ fi
31
+ ;;
32
+ esac
33
+ }
34
+
35
+ eagle_posttool_mirror_tasks() {
36
+ local tool_name="$1" session_id="$2" project="$3" input="$4"
37
+
38
+ case "$tool_name" in
39
+ TaskCreate|TaskUpdate)
40
+ if eagle_validate_session_id "$session_id"; then
41
+ local task_dir="$EAGLE_CLAUDE_TASKS_DIR/$session_id"
42
+ if [ -d "$task_dir" ]; then
43
+ local task_id
44
+ task_id=$(echo "$input" | jq -r '.tool_input.id // empty')
45
+ if [ -z "$task_id" ]; then
46
+ local newest
47
+ newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
48
+ [ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
49
+ elif eagle_validate_session_id "$task_id"; then
50
+ local task_json="$task_dir/$task_id.json"
51
+ [ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
52
+ fi
53
+ fi
54
+ fi
55
+ ;;
56
+ esac
57
+ }
58
+
59
+ eagle_posttool_stale_hint() {
60
+ local tool_name="$1" fp="$2" project="$3"
61
+
62
+ case "$tool_name" in
63
+ Write|Edit)
64
+ if [ -n "$fp" ]; then
65
+ local fname fname_stem
66
+ fname=$(basename "$fp")
67
+ fname_stem="${fname%.*}"
68
+ case "$fp" in
69
+ "$HOME/.claude/"*) ;; # skip Claude config files
70
+ *)
71
+ if [ ${#fname_stem} -ge 3 ]; then
72
+ local fts_query
73
+ fts_query=$(eagle_fts_sanitize "$fname_stem")
74
+ if [ -n "$fts_query" ]; then
75
+ local stale_hit
76
+ stale_hit=$(eagle_search_stale_memories "$project" "$fts_query")
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."
79
+ jq -nc --arg ctx "$stale_msg" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
80
+ fi
81
+ fi
82
+ fi
83
+ ;;
84
+ esac
85
+ fi
86
+ ;;
87
+ esac
88
+ }
89
+
90
+ eagle_posttool_decision_surface() {
91
+ local tool_name="$1" fp="$2" project="$3"
92
+
93
+ case "$tool_name" in
94
+ Read)
95
+ if [ -n "$fp" ]; then
96
+ local fname fname_stem read_context=""
97
+ fname=$(basename "$fp")
98
+ fname_stem="${fname%.*}"
99
+ case "$fp" in
100
+ "$HOME/.claude/"*) ;; # skip Claude config files
101
+ *)
102
+ if [ ${#fname_stem} -ge 3 ]; then
103
+ local fts_query
104
+ fts_query=$(eagle_fts_sanitize "$fname_stem")
105
+ if [ -n "$fts_query" ]; then
106
+ local decision_hit
107
+ decision_hit=$(eagle_search_decisions_for_file "$project" "$fts_query")
108
+ if [ -n "$decision_hit" ]; then
109
+ read_context+="Eagle Mem decision history for '${fname}': ${decision_hit} — Do not revert without explicit user request. "
110
+ fi
111
+ fi
112
+ fi
113
+
114
+ local feature_hit
115
+ feature_hit=$(eagle_find_features_for_file "$project" "$fp")
116
+ if [ -n "$feature_hit" ]; then
117
+ while IFS='|' read -r feat_name feat_desc feat_verified _role feat_deps feat_other_files feat_smoke; do
118
+ [ -z "$feat_name" ] && continue
119
+ read_context+="Eagle Mem: '${fname}' is part of feature '${feat_name}'"
120
+ [ -n "$feat_desc" ] && read_context+=" ($feat_desc)"
121
+ read_context+="."
122
+ [ -n "$feat_verified" ] && read_context+=" Last verified: ${feat_verified}."
123
+ [ -n "$feat_deps" ] && read_context+=" Dependencies: ${feat_deps}."
124
+ [ -n "$feat_other_files" ] && read_context+=" Other files in pipeline: ${feat_other_files}."
125
+ [ -n "$feat_smoke" ] && read_context+=" Smoke tests: ${feat_smoke}."
126
+ read_context+=" Changes require re-testing after deploy. "
127
+ done <<< "$feature_hit"
128
+ fi
129
+
130
+ if [ -n "$read_context" ]; then
131
+ jq -nc --arg ctx "$read_context" '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}'
132
+ fi
133
+ ;;
134
+ esac
135
+ fi
136
+ ;;
137
+ esac
138
+ }
package/lib/provider.sh CHANGED
@@ -50,19 +50,30 @@ eagle_config_set() {
50
50
  local key="$2"
51
51
  local value="$3"
52
52
 
53
+ # Validate section/key are alphanumeric+underscore (safe for grep/sed patterns)
54
+ if [[ ! "$section" =~ ^[A-Za-z0-9_-]+$ ]] || [[ ! "$key" =~ ^[A-Za-z0-9_-]+$ ]]; then
55
+ eagle_log "ERROR" "config_set: invalid section/key: [$section] $key"
56
+ return 1
57
+ fi
58
+
53
59
  if [ ! -f "$EAGLE_CONFIG_FILE" ]; then
54
60
  eagle_config_init
55
61
  fi
56
62
 
63
+ # Escape sed metacharacters in value to prevent injection via |, &, \, /
64
+ local safe_value
65
+ safe_value=$(printf '%s' "$value" | sed 's/[|&/\]/\\&/g')
66
+
57
67
  if grep -q "^\[${section}\]" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
58
68
  if grep -q "^[[:space:]]*${key}[[:space:]]*=" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
59
- sed -i '' "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${value}\"|" "$EAGLE_CONFIG_FILE"
69
+ sed -i '' "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${safe_value}\"|" "$EAGLE_CONFIG_FILE"
60
70
  else
61
71
  sed -i '' "/^\[${section}\]/a\\
62
- ${key} = \"${value}\"
72
+ ${key} = \"${safe_value}\"
63
73
  " "$EAGLE_CONFIG_FILE"
64
74
  fi
65
75
  else
76
+ # printf is safe — no sed interpolation needed for append
66
77
  printf '\n[%s]\n%s = "%s"\n' "$section" "$key" "$value" >> "$EAGLE_CONFIG_FILE"
67
78
  fi
68
79
  }
@@ -115,7 +126,10 @@ eagle_config_init() {
115
126
  model="gpt-4o-mini"
116
127
  fi
117
128
 
118
- cat > "$EAGLE_CONFIG_FILE" << TOML
129
+ # Create config with restrictive permissions from the start (no TOCTOU window)
130
+ (
131
+ umask 077
132
+ cat > "$EAGLE_CONFIG_FILE" << TOML
119
133
  # Eagle Mem configuration
120
134
  # Docs: https://github.com/eagleisbatman/eagle-mem
121
135
 
@@ -144,7 +158,7 @@ schedule = "manual"
144
158
  # Additional secret patterns (regex) beyond built-in defaults
145
159
  # extra_patterns = ["MY_CUSTOM_SECRET_.*"]
146
160
  TOML
147
-
161
+ )
148
162
  eagle_log "INFO" "Config initialized: provider=$provider model=$model"
149
163
  }
150
164
 
@@ -236,11 +250,12 @@ _eagle_call_anthropic() {
236
250
  messages: [{role: "user", content: $prompt}]
237
251
  }')
238
252
 
253
+ # Pass API key via config stdin to avoid exposing it in process list (ps aux)
239
254
  local response
240
255
  response=$(curl -sf "https://api.anthropic.com/v1/messages" \
241
256
  --connect-timeout 5 \
242
257
  --max-time 120 \
243
- -H "x-api-key: ${api_key}" \
258
+ -K <(printf 'header = "x-api-key: %s"' "$api_key") \
244
259
  -H "anthropic-version: 2023-06-01" \
245
260
  -H "content-type: application/json" \
246
261
  -d "$body" 2>/dev/null)
@@ -280,11 +295,12 @@ _eagle_call_openai() {
280
295
  ]
281
296
  }')
282
297
 
298
+ # Pass API key via config stdin to avoid exposing it in process list (ps aux)
283
299
  local response
284
300
  response=$(curl -sf "https://api.openai.com/v1/chat/completions" \
285
301
  --connect-timeout 5 \
286
302
  --max-time 120 \
287
- -H "Authorization: Bearer ${api_key}" \
303
+ -K <(printf 'header = "Authorization: Bearer %s"' "$api_key") \
288
304
  -H "content-type: application/json" \
289
305
  -d "$body" 2>/dev/null)
290
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
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
@@ -213,6 +213,16 @@ If no rules needed, output: NONE"
213
213
  max_lines=$(echo "$max_lines" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
214
214
  reason=$(echo "$reason" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
215
215
 
216
+ # Guard: skip malformed lines missing required fields
217
+ if [ -z "$pattern" ] || [ -z "$strategy" ]; then
218
+ eagle_log "WARN" "Curator: skipping malformed RULE line: $line"
219
+ continue
220
+ fi
221
+ case "$strategy" in summary|truncate) ;; *)
222
+ eagle_log "WARN" "Curator: skipping RULE with invalid strategy '$strategy'"
223
+ continue
224
+ ;; esac
225
+
216
226
  [ "$max_lines" = "-" ] && max_lines=""
217
227
 
218
228
  if [ "$DRY_RUN" -eq 1 ]; then
@@ -292,6 +302,12 @@ Rules:
292
302
  fdesc=$(echo "$fdesc" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
293
303
  ffiles=$(echo "$ffiles" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
294
304
 
305
+ # Guard: skip malformed lines missing required name
306
+ if [ -z "$fname" ]; then
307
+ eagle_log "WARN" "Curator: skipping malformed FEATURE line: $line"
308
+ continue
309
+ fi
310
+
295
311
  if [ "$DRY_RUN" -eq 1 ]; then
296
312
  eagle_info " Feature: $fname — $fdesc"
297
313
  eagle_info " Files: $ffiles"
@@ -152,7 +152,10 @@ eagle_ok "Files copied to $EAGLE_MEM_DIR"
152
152
 
153
153
  # ─── Run migrations ────────────────────────────────────────
154
154
 
155
- "$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null | grep -v -E '^(wal|5000|Eagle Mem database)$' > /dev/null
155
+ if ! "$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null; then
156
+ eagle_err "Database migration failed"
157
+ exit 1
158
+ fi
156
159
  eagle_ok "Database ready"
157
160
 
158
161
  # ─── Patch settings.json ───────────────────────────────────
@@ -242,7 +245,7 @@ else
242
245
  echo ""
243
246
  eagle_ok "Statusline ${DIM}(manual patch needed — instructions above)${RESET}"
244
247
  else
245
- eagle_ok "Statusline ${DIM}(already has Eagle Mem)${RESET}"
248
+ eagle_ok "Statusline ${DIM}(existing cannot auto-patch; add Eagle Mem manually)${RESET}"
246
249
  fi
247
250
  fi
248
251
 
@@ -438,7 +438,7 @@ memories_sync() {
438
438
  eagle_info "Scanning for Claude Code auto-memory files..."
439
439
  echo ""
440
440
 
441
- local claude_mem_root="$HOME/.claude/projects"
441
+ local claude_mem_root="$EAGLE_CLAUDE_PROJECTS_DIR"
442
442
  local mem_synced=0
443
443
  local mem_skipped=0
444
444
 
@@ -471,7 +471,7 @@ memories_sync() {
471
471
  eagle_info "Scanning for Claude Code plan files..."
472
472
  echo ""
473
473
 
474
- local plans_dir="$HOME/.claude/plans"
474
+ local plans_dir="$EAGLE_CLAUDE_PLANS_DIR"
475
475
  local plan_synced=0
476
476
  local plan_skipped=0
477
477
 
@@ -504,7 +504,7 @@ memories_sync() {
504
504
  eagle_info "Scanning for Claude Code task files..."
505
505
  echo ""
506
506
 
507
- local tasks_dir="$HOME/.claude/tasks"
507
+ local tasks_dir="$EAGLE_CLAUDE_TASKS_DIR"
508
508
  local task_synced=0
509
509
  local task_skipped=0
510
510
 
@@ -18,7 +18,7 @@ eagle_header "Uninstall"
18
18
  # ─── Remove hooks from settings.json ──────────────────────
19
19
 
20
20
  if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
21
- for event in SessionStart Stop PostToolUse SessionEnd UserPromptSubmit; do
21
+ for event in SessionStart Stop PostToolUse PreToolUse SessionEnd UserPromptSubmit; do
22
22
  if jq -e ".hooks.${event}" "$SETTINGS" &>/dev/null; then
23
23
  tmp=$(mktemp)
24
24
  jq ".hooks.${event} = [.hooks.${event}[]? | select(any(.hooks[]?; .command | contains(\"eagle-mem\")) | not)]" "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
package/scripts/update.sh CHANGED
@@ -46,7 +46,11 @@ eagle_ok "Files updated"
46
46
 
47
47
  # ─── Run pending migrations ────────────────────────────────
48
48
 
49
- migration_output=$("$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null | grep -v -E '^(wal|5000)$')
49
+ migration_output=$("$EAGLE_MEM_DIR/db/migrate.sh" 2>&1) || {
50
+ eagle_err "Database migration failed"
51
+ eagle_err "$migration_output"
52
+ exit 1
53
+ }
50
54
  if echo "$migration_output" | grep -q "applied:"; then
51
55
  echo "$migration_output" | grep "applied:" | while read -r line; do
52
56
  eagle_ok "Migration: ${line#*applied: }"