eagle-mem 2.0.7 → 3.0.1
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 +3 -0
- package/db/013_features.sql +69 -0
- package/db/014_command_intelligence.sql +25 -0
- package/db/015_integrity_fixes.sql +156 -0
- package/db/migrate.sh +3 -0
- package/hooks/post-tool-use.sh +103 -18
- package/hooks/pre-tool-use.sh +125 -0
- package/hooks/session-end.sh +3 -0
- package/hooks/session-start.sh +4 -0
- package/hooks/stop.sh +16 -2
- package/lib/common.sh +45 -0
- package/lib/db.sh +178 -5
- package/lib/provider.sh +346 -0
- package/package.json +1 -1
- package/scripts/config.sh +72 -0
- package/scripts/curate.sh +365 -0
- package/scripts/feature.sh +110 -0
- package/scripts/help.sh +3 -0
- package/scripts/index.sh +1 -1
- package/scripts/install.sh +19 -2
- package/scripts/uninstall.sh +1 -1
- package/scripts/update.sh +6 -1
package/lib/db.sh
CHANGED
|
@@ -14,15 +14,45 @@ PRAGMA trusted_schema=ON;
|
|
|
14
14
|
.output stdout"
|
|
15
15
|
|
|
16
16
|
eagle_db() {
|
|
17
|
-
|
|
17
|
+
local _eagle_db_err
|
|
18
|
+
_eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_err.$$")
|
|
19
|
+
local _eagle_db_out
|
|
20
|
+
_eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
|
|
21
|
+
local _eagle_db_rc=$?
|
|
22
|
+
if [ -s "$_eagle_db_err" ]; then
|
|
23
|
+
cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
|
|
24
|
+
fi
|
|
25
|
+
rm -f "$_eagle_db_err" 2>/dev/null
|
|
26
|
+
[ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
|
|
27
|
+
return $_eagle_db_rc
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
eagle_db_pipe() {
|
|
21
|
-
|
|
31
|
+
local _eagle_db_err
|
|
32
|
+
_eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_pipe_err.$$")
|
|
33
|
+
local _eagle_db_out
|
|
34
|
+
_eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
|
|
35
|
+
local _eagle_db_rc=$?
|
|
36
|
+
if [ -s "$_eagle_db_err" ]; then
|
|
37
|
+
cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
|
|
38
|
+
fi
|
|
39
|
+
rm -f "$_eagle_db_err" 2>/dev/null
|
|
40
|
+
[ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
|
|
41
|
+
return $_eagle_db_rc
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
eagle_db_json() {
|
|
25
|
-
|
|
45
|
+
local _eagle_db_err
|
|
46
|
+
_eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_json_err.$$")
|
|
47
|
+
local _eagle_db_out
|
|
48
|
+
_eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
|
|
49
|
+
local _eagle_db_rc=$?
|
|
50
|
+
if [ -s "$_eagle_db_err" ]; then
|
|
51
|
+
cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
|
|
52
|
+
fi
|
|
53
|
+
rm -f "$_eagle_db_err" 2>/dev/null
|
|
54
|
+
[ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
|
|
55
|
+
return $_eagle_db_rc
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
eagle_ensure_db() {
|
|
@@ -62,9 +92,19 @@ eagle_insert_observation() {
|
|
|
62
92
|
local tool_input_summary; tool_input_summary=$(eagle_sql_escape "$4")
|
|
63
93
|
local files_read; files_read=$(eagle_sql_escape "$5")
|
|
64
94
|
local files_modified; files_modified=$(eagle_sql_escape "$6")
|
|
95
|
+
local output_bytes="${7:-}"
|
|
96
|
+
local output_lines="${8:-}"
|
|
97
|
+
local command_category; command_category=$(eagle_sql_escape "${9:-}")
|
|
98
|
+
|
|
99
|
+
local extra_cols=""
|
|
100
|
+
local extra_vals=""
|
|
101
|
+
if [ -n "$output_bytes" ]; then
|
|
102
|
+
extra_cols=", output_bytes, output_lines, command_category"
|
|
103
|
+
extra_vals=", $(eagle_sql_int "$output_bytes"), $(eagle_sql_int "$output_lines"), '$command_category'"
|
|
104
|
+
fi
|
|
65
105
|
|
|
66
|
-
eagle_db "INSERT INTO observations (session_id, project, tool_name, tool_input_summary, files_read, files_modified)
|
|
67
|
-
SELECT '$session_id', '$project', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'
|
|
106
|
+
eagle_db "INSERT INTO observations (session_id, project, tool_name, tool_input_summary, files_read, files_modified${extra_cols})
|
|
107
|
+
SELECT '$session_id', '$project', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'${extra_vals}
|
|
68
108
|
WHERE NOT EXISTS (
|
|
69
109
|
SELECT 1 FROM observations
|
|
70
110
|
WHERE session_id = '$session_id'
|
|
@@ -565,3 +605,136 @@ COMMIT;"
|
|
|
565
605
|
fi
|
|
566
606
|
echo "$removed"
|
|
567
607
|
}
|
|
608
|
+
|
|
609
|
+
# ─── Feature graph helpers ─────��───────────────────────────
|
|
610
|
+
|
|
611
|
+
eagle_upsert_feature() {
|
|
612
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
613
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
614
|
+
local description; description=$(eagle_sql_escape "${3:-}")
|
|
615
|
+
|
|
616
|
+
eagle_db "INSERT INTO features (project, name, description)
|
|
617
|
+
VALUES ('$project', '$name', '$description')
|
|
618
|
+
ON CONFLICT(project, name) DO UPDATE SET
|
|
619
|
+
description = COALESCE(NULLIF('$description', ''), features.description),
|
|
620
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
eagle_add_feature_dependency() {
|
|
624
|
+
local feature_id; feature_id=$(eagle_sql_int "$1")
|
|
625
|
+
local kind; kind=$(eagle_sql_escape "$2")
|
|
626
|
+
local target; target=$(eagle_sql_escape "$3")
|
|
627
|
+
local name; name=$(eagle_sql_escape "$4")
|
|
628
|
+
local notes; notes=$(eagle_sql_escape "${5:-}")
|
|
629
|
+
|
|
630
|
+
eagle_db "INSERT OR IGNORE INTO feature_dependencies (feature_id, kind, target, name, notes)
|
|
631
|
+
VALUES ($feature_id, '$kind', '$target', '$name', '$notes');"
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
eagle_add_feature_file() {
|
|
635
|
+
local feature_id; feature_id=$(eagle_sql_int "$1")
|
|
636
|
+
local file_path; file_path=$(eagle_sql_escape "$2")
|
|
637
|
+
local role; role=$(eagle_sql_escape "${3:-}")
|
|
638
|
+
|
|
639
|
+
eagle_db "INSERT OR IGNORE INTO feature_files (feature_id, file_path, role)
|
|
640
|
+
VALUES ($feature_id, '$file_path', '$role');"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
eagle_add_feature_smoke_test() {
|
|
644
|
+
local feature_id; feature_id=$(eagle_sql_int "$1")
|
|
645
|
+
local command; command=$(eagle_sql_escape "$2")
|
|
646
|
+
local description; description=$(eagle_sql_escape "${3:-}")
|
|
647
|
+
|
|
648
|
+
eagle_db "INSERT OR IGNORE INTO feature_smoke_tests (feature_id, command, description)
|
|
649
|
+
VALUES ($feature_id, '$command', '$description');"
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
eagle_verify_feature() {
|
|
653
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
654
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
655
|
+
local notes; notes=$(eagle_sql_escape "${3:-}")
|
|
656
|
+
|
|
657
|
+
eagle_db "UPDATE features SET
|
|
658
|
+
last_verified_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
|
|
659
|
+
last_verified_notes = '$notes',
|
|
660
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
661
|
+
WHERE project = '$project' AND name = '$name';"
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
eagle_get_feature_id() {
|
|
665
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
666
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
667
|
+
eagle_db "SELECT id FROM features WHERE project = '$project' AND name = '$name';"
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
eagle_list_features() {
|
|
671
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
672
|
+
local limit; limit=$(eagle_sql_int "${2:-20}")
|
|
673
|
+
|
|
674
|
+
eagle_db "SELECT f.name, f.description, f.status, f.last_verified_at,
|
|
675
|
+
(SELECT COUNT(*) FROM feature_dependencies WHERE feature_id = f.id) as dep_count,
|
|
676
|
+
(SELECT COUNT(*) FROM feature_files WHERE feature_id = f.id) as file_count,
|
|
677
|
+
(SELECT COUNT(*) FROM feature_smoke_tests WHERE feature_id = f.id) as test_count
|
|
678
|
+
FROM features f
|
|
679
|
+
WHERE f.project = '$project' AND f.status = 'active'
|
|
680
|
+
ORDER BY f.updated_at DESC
|
|
681
|
+
LIMIT $limit;"
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
eagle_show_feature() {
|
|
685
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
686
|
+
local name; name=$(eagle_sql_escape "$2")
|
|
687
|
+
|
|
688
|
+
local feature_id
|
|
689
|
+
feature_id=$(eagle_get_feature_id "$1" "$2")
|
|
690
|
+
[ -z "$feature_id" ] && return 1
|
|
691
|
+
|
|
692
|
+
echo "=== Feature: $2 ==="
|
|
693
|
+
eagle_db "SELECT name, description, status, last_verified_at, last_verified_notes
|
|
694
|
+
FROM features WHERE id = $feature_id;"
|
|
695
|
+
|
|
696
|
+
local deps
|
|
697
|
+
deps=$(eagle_db "SELECT kind, target, name, notes FROM feature_dependencies WHERE feature_id = $feature_id;")
|
|
698
|
+
if [ -n "$deps" ]; then
|
|
699
|
+
echo "--- Dependencies ---"
|
|
700
|
+
echo "$deps"
|
|
701
|
+
fi
|
|
702
|
+
|
|
703
|
+
local files
|
|
704
|
+
files=$(eagle_db "SELECT file_path, role FROM feature_files WHERE feature_id = $feature_id;")
|
|
705
|
+
if [ -n "$files" ]; then
|
|
706
|
+
echo "--- Files ---"
|
|
707
|
+
echo "$files"
|
|
708
|
+
fi
|
|
709
|
+
|
|
710
|
+
local tests
|
|
711
|
+
tests=$(eagle_db "SELECT command, description FROM feature_smoke_tests WHERE feature_id = $feature_id;")
|
|
712
|
+
if [ -n "$tests" ]; then
|
|
713
|
+
echo "--- Smoke Tests ---"
|
|
714
|
+
echo "$tests"
|
|
715
|
+
fi
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
eagle_find_features_for_file() {
|
|
719
|
+
local project; project=$(eagle_sql_escape "$1")
|
|
720
|
+
local file_path="$2"
|
|
721
|
+
local fname; fname=$(basename "$file_path")
|
|
722
|
+
local fname_esc; fname_esc=$(eagle_sql_escape "$fname")
|
|
723
|
+
local fname_like; fname_like=$(eagle_like_escape "$fname_esc")
|
|
724
|
+
|
|
725
|
+
eagle_db "SELECT f.name, f.description, f.last_verified_at,
|
|
726
|
+
ff.role,
|
|
727
|
+
(SELECT GROUP_CONCAT(fd.target || ':' || fd.name, ', ')
|
|
728
|
+
FROM feature_dependencies fd WHERE fd.feature_id = f.id) as deps,
|
|
729
|
+
(SELECT GROUP_CONCAT(ff2.file_path, ', ')
|
|
730
|
+
FROM feature_files ff2 WHERE ff2.feature_id = f.id AND ff2.file_path != ff.file_path) as other_files,
|
|
731
|
+
(SELECT GROUP_CONCAT(fst.command, ', ')
|
|
732
|
+
FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke_tests
|
|
733
|
+
FROM features f
|
|
734
|
+
JOIN feature_files ff ON ff.feature_id = f.id
|
|
735
|
+
WHERE f.project = '$project'
|
|
736
|
+
AND f.status = 'active'
|
|
737
|
+
AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\')
|
|
738
|
+
ORDER BY f.updated_at DESC
|
|
739
|
+
LIMIT 3;"
|
|
740
|
+
}
|
package/lib/provider.sh
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — LLM Provider Abstraction
|
|
4
|
+
# Config parsing + unified eagle_llm_call for Ollama/Anthropic/OpenAI
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
EAGLE_CONFIG_FILE="${EAGLE_MEM_DIR}/config.toml"
|
|
8
|
+
EAGLE_DEFAULT_OLLAMA_URL="http://localhost:11434"
|
|
9
|
+
|
|
10
|
+
# ─── Config parsing ────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
eagle_config_get() {
|
|
13
|
+
local section="$1"
|
|
14
|
+
local key="$2"
|
|
15
|
+
local default="${3:-}"
|
|
16
|
+
|
|
17
|
+
if [ ! -f "$EAGLE_CONFIG_FILE" ]; then
|
|
18
|
+
echo "$default"
|
|
19
|
+
return
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
local value
|
|
23
|
+
value=$(awk -v section="$section" -v key="$key" '
|
|
24
|
+
/^[[:space:]]*\[/ {
|
|
25
|
+
gsub(/[\[\][:space:]]/, "")
|
|
26
|
+
current = $0
|
|
27
|
+
}
|
|
28
|
+
current == section && /^[[:space:]]*[^#\[]/ {
|
|
29
|
+
split($0, parts, "=")
|
|
30
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", parts[1])
|
|
31
|
+
if (parts[1] == key) {
|
|
32
|
+
val = substr($0, index($0, "=") + 1)
|
|
33
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", val)
|
|
34
|
+
gsub(/^["'"'"']|["'"'"']$/, "", val)
|
|
35
|
+
print val
|
|
36
|
+
exit
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
' "$EAGLE_CONFIG_FILE")
|
|
40
|
+
|
|
41
|
+
if [ -n "$value" ]; then
|
|
42
|
+
echo "$value"
|
|
43
|
+
else
|
|
44
|
+
echo "$default"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
eagle_config_set() {
|
|
49
|
+
local section="$1"
|
|
50
|
+
local key="$2"
|
|
51
|
+
local value="$3"
|
|
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
|
+
|
|
59
|
+
if [ ! -f "$EAGLE_CONFIG_FILE" ]; then
|
|
60
|
+
eagle_config_init
|
|
61
|
+
fi
|
|
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
|
+
|
|
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
|
|
75
|
+
else
|
|
76
|
+
# printf is safe — no sed interpolation needed for append
|
|
77
|
+
printf '\n[%s]\n%s = "%s"\n' "$section" "$key" "$value" >> "$EAGLE_CONFIG_FILE"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ─── Ollama detection ──────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
eagle_detect_ollama() {
|
|
84
|
+
local url="${1:-$EAGLE_DEFAULT_OLLAMA_URL}"
|
|
85
|
+
curl -sf "${url}/api/tags" --connect-timeout 2 --max-time 3 2>/dev/null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
eagle_ollama_models() {
|
|
89
|
+
local url="${1:-$EAGLE_DEFAULT_OLLAMA_URL}"
|
|
90
|
+
eagle_detect_ollama "$url" | jq -r '.models[].name' 2>/dev/null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
eagle_ollama_best_model() {
|
|
94
|
+
local models
|
|
95
|
+
models=$(eagle_ollama_models "$1")
|
|
96
|
+
[ -z "$models" ] && return 1
|
|
97
|
+
|
|
98
|
+
local preferred="mistral qwen3-coder gemma4 llama3 phi3 deepseek-coder"
|
|
99
|
+
for pref in $preferred; do
|
|
100
|
+
if echo "$models" | grep -qi "$pref"; then
|
|
101
|
+
echo "$models" | grep -i "$pref" | head -1
|
|
102
|
+
return 0
|
|
103
|
+
fi
|
|
104
|
+
done
|
|
105
|
+
|
|
106
|
+
echo "$models" | head -1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# ─── Config initialization ─────────────────────────────────
|
|
110
|
+
|
|
111
|
+
eagle_config_init() {
|
|
112
|
+
local ollama_url="$EAGLE_DEFAULT_OLLAMA_URL"
|
|
113
|
+
local provider="none"
|
|
114
|
+
local model=""
|
|
115
|
+
|
|
116
|
+
local ollama_response
|
|
117
|
+
ollama_response=$(eagle_detect_ollama "$ollama_url")
|
|
118
|
+
if [ -n "$ollama_response" ]; then
|
|
119
|
+
provider="ollama"
|
|
120
|
+
model=$(eagle_ollama_best_model "$ollama_url")
|
|
121
|
+
elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
|
122
|
+
provider="anthropic"
|
|
123
|
+
model="claude-haiku-4-5-20251001"
|
|
124
|
+
elif [ -n "${OPENAI_API_KEY:-}" ]; then
|
|
125
|
+
provider="openai"
|
|
126
|
+
model="gpt-4o-mini"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Create config with restrictive permissions from the start (no TOCTOU window)
|
|
130
|
+
(
|
|
131
|
+
umask 077
|
|
132
|
+
cat > "$EAGLE_CONFIG_FILE" << TOML
|
|
133
|
+
# Eagle Mem configuration
|
|
134
|
+
# Docs: https://github.com/eagleisbatman/eagle-mem
|
|
135
|
+
|
|
136
|
+
[provider]
|
|
137
|
+
# Which LLM provider to use for the curator and analysis features
|
|
138
|
+
# Options: "ollama" (free, local), "anthropic", "openai"
|
|
139
|
+
type = "$provider"
|
|
140
|
+
|
|
141
|
+
[ollama]
|
|
142
|
+
url = "$ollama_url"
|
|
143
|
+
model = "${model:-mistral}"
|
|
144
|
+
|
|
145
|
+
[anthropic]
|
|
146
|
+
# Uses ANTHROPIC_API_KEY env var for authentication
|
|
147
|
+
model = "claude-haiku-4-5-20251001"
|
|
148
|
+
|
|
149
|
+
[openai]
|
|
150
|
+
# Uses OPENAI_API_KEY env var for authentication
|
|
151
|
+
model = "gpt-4o-mini"
|
|
152
|
+
|
|
153
|
+
[curator]
|
|
154
|
+
# How often the curator runs: "manual", "daily", "weekly"
|
|
155
|
+
schedule = "manual"
|
|
156
|
+
|
|
157
|
+
[redaction]
|
|
158
|
+
# Additional secret patterns (regex) beyond built-in defaults
|
|
159
|
+
# extra_patterns = ["MY_CUSTOM_SECRET_.*"]
|
|
160
|
+
TOML
|
|
161
|
+
)
|
|
162
|
+
eagle_log "INFO" "Config initialized: provider=$provider model=$model"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ─── Unified LLM call ─────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
eagle_llm_call() {
|
|
168
|
+
local prompt="$1"
|
|
169
|
+
local system_prompt="${2:-You are a helpful assistant that analyzes software development sessions.}"
|
|
170
|
+
local max_tokens="${3:-1024}"
|
|
171
|
+
|
|
172
|
+
local provider
|
|
173
|
+
provider=$(eagle_config_get "provider" "type" "none")
|
|
174
|
+
|
|
175
|
+
case "$provider" in
|
|
176
|
+
ollama) _eagle_call_ollama "$prompt" "$system_prompt" "$max_tokens" ;;
|
|
177
|
+
anthropic) _eagle_call_anthropic "$prompt" "$system_prompt" "$max_tokens" ;;
|
|
178
|
+
openai) _eagle_call_openai "$prompt" "$system_prompt" "$max_tokens" ;;
|
|
179
|
+
none)
|
|
180
|
+
eagle_log "ERROR" "No LLM provider configured. Run: eagle-mem config"
|
|
181
|
+
return 1
|
|
182
|
+
;;
|
|
183
|
+
*)
|
|
184
|
+
eagle_log "ERROR" "Unknown provider: $provider"
|
|
185
|
+
return 1
|
|
186
|
+
;;
|
|
187
|
+
esac
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_eagle_call_ollama() {
|
|
191
|
+
local prompt="$1" system="$2" max_tokens="$3"
|
|
192
|
+
local url model
|
|
193
|
+
|
|
194
|
+
url=$(eagle_config_get "ollama" "url" "$EAGLE_DEFAULT_OLLAMA_URL")
|
|
195
|
+
model=$(eagle_config_get "ollama" "model" "mistral")
|
|
196
|
+
|
|
197
|
+
local body
|
|
198
|
+
body=$(jq -nc \
|
|
199
|
+
--arg model "$model" \
|
|
200
|
+
--arg system "$system" \
|
|
201
|
+
--arg prompt "$prompt" \
|
|
202
|
+
--argjson tokens "$max_tokens" \
|
|
203
|
+
'{
|
|
204
|
+
model: $model,
|
|
205
|
+
messages: [
|
|
206
|
+
{role: "system", content: $system},
|
|
207
|
+
{role: "user", content: $prompt}
|
|
208
|
+
],
|
|
209
|
+
stream: false,
|
|
210
|
+
options: {num_predict: $tokens}
|
|
211
|
+
}')
|
|
212
|
+
|
|
213
|
+
local response
|
|
214
|
+
response=$(curl -sf "${url}/api/chat" \
|
|
215
|
+
--connect-timeout 5 \
|
|
216
|
+
--max-time 120 \
|
|
217
|
+
-H "Content-Type: application/json" \
|
|
218
|
+
-d "$body" 2>/dev/null)
|
|
219
|
+
|
|
220
|
+
if [ $? -ne 0 ] || [ -z "$response" ]; then
|
|
221
|
+
eagle_log "ERROR" "Ollama call failed: model=$model url=$url"
|
|
222
|
+
return 1
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
echo "$response" | jq -r '.message.content // empty'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_eagle_call_anthropic() {
|
|
229
|
+
local prompt="$1" system="$2" max_tokens="$3"
|
|
230
|
+
local model api_key
|
|
231
|
+
|
|
232
|
+
model=$(eagle_config_get "anthropic" "model" "claude-haiku-4-5-20251001")
|
|
233
|
+
api_key="${ANTHROPIC_API_KEY:-}"
|
|
234
|
+
|
|
235
|
+
if [ -z "$api_key" ]; then
|
|
236
|
+
eagle_log "ERROR" "ANTHROPIC_API_KEY not set"
|
|
237
|
+
return 1
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
local body
|
|
241
|
+
body=$(jq -nc \
|
|
242
|
+
--arg model "$model" \
|
|
243
|
+
--arg system "$system" \
|
|
244
|
+
--arg prompt "$prompt" \
|
|
245
|
+
--argjson tokens "$max_tokens" \
|
|
246
|
+
'{
|
|
247
|
+
model: $model,
|
|
248
|
+
max_tokens: $tokens,
|
|
249
|
+
system: $system,
|
|
250
|
+
messages: [{role: "user", content: $prompt}]
|
|
251
|
+
}')
|
|
252
|
+
|
|
253
|
+
# Pass API key via config stdin to avoid exposing it in process list (ps aux)
|
|
254
|
+
local response
|
|
255
|
+
response=$(curl -sf "https://api.anthropic.com/v1/messages" \
|
|
256
|
+
--connect-timeout 5 \
|
|
257
|
+
--max-time 120 \
|
|
258
|
+
-K <(printf 'header = "x-api-key: %s"' "$api_key") \
|
|
259
|
+
-H "anthropic-version: 2023-06-01" \
|
|
260
|
+
-H "content-type: application/json" \
|
|
261
|
+
-d "$body" 2>/dev/null)
|
|
262
|
+
|
|
263
|
+
if [ $? -ne 0 ] || [ -z "$response" ]; then
|
|
264
|
+
eagle_log "ERROR" "Anthropic call failed: model=$model"
|
|
265
|
+
return 1
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
echo "$response" | jq -r '.content[0].text // empty'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_eagle_call_openai() {
|
|
272
|
+
local prompt="$1" system="$2" max_tokens="$3"
|
|
273
|
+
local model api_key
|
|
274
|
+
|
|
275
|
+
model=$(eagle_config_get "openai" "model" "gpt-4o-mini")
|
|
276
|
+
api_key="${OPENAI_API_KEY:-}"
|
|
277
|
+
|
|
278
|
+
if [ -z "$api_key" ]; then
|
|
279
|
+
eagle_log "ERROR" "OPENAI_API_KEY not set"
|
|
280
|
+
return 1
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
local body
|
|
284
|
+
body=$(jq -nc \
|
|
285
|
+
--arg model "$model" \
|
|
286
|
+
--arg system "$system" \
|
|
287
|
+
--arg prompt "$prompt" \
|
|
288
|
+
--argjson tokens "$max_tokens" \
|
|
289
|
+
'{
|
|
290
|
+
model: $model,
|
|
291
|
+
max_tokens: $tokens,
|
|
292
|
+
messages: [
|
|
293
|
+
{role: "system", content: $system},
|
|
294
|
+
{role: "user", content: $prompt}
|
|
295
|
+
]
|
|
296
|
+
}')
|
|
297
|
+
|
|
298
|
+
# Pass API key via config stdin to avoid exposing it in process list (ps aux)
|
|
299
|
+
local response
|
|
300
|
+
response=$(curl -sf "https://api.openai.com/v1/chat/completions" \
|
|
301
|
+
--connect-timeout 5 \
|
|
302
|
+
--max-time 120 \
|
|
303
|
+
-K <(printf 'header = "Authorization: Bearer %s"' "$api_key") \
|
|
304
|
+
-H "content-type: application/json" \
|
|
305
|
+
-d "$body" 2>/dev/null)
|
|
306
|
+
|
|
307
|
+
if [ $? -ne 0 ] || [ -z "$response" ]; then
|
|
308
|
+
eagle_log "ERROR" "OpenAI call failed: model=$model"
|
|
309
|
+
return 1
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
echo "$response" | jq -r '.choices[0].message.content // empty'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# ─── Config CLI helpers ────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
eagle_show_config() {
|
|
318
|
+
if [ ! -f "$EAGLE_CONFIG_FILE" ]; then
|
|
319
|
+
echo "No config file found. Run: eagle-mem config init"
|
|
320
|
+
return 1
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
local provider model
|
|
324
|
+
provider=$(eagle_config_get "provider" "type" "none")
|
|
325
|
+
model=$(eagle_config_get "$provider" "model" "unknown")
|
|
326
|
+
|
|
327
|
+
echo "Provider: $provider"
|
|
328
|
+
echo "Model: $model"
|
|
329
|
+
|
|
330
|
+
if [ "$provider" = "ollama" ]; then
|
|
331
|
+
local url
|
|
332
|
+
url=$(eagle_config_get "ollama" "url" "$EAGLE_DEFAULT_OLLAMA_URL")
|
|
333
|
+
echo "URL: $url"
|
|
334
|
+
local running
|
|
335
|
+
running=$(eagle_detect_ollama "$url")
|
|
336
|
+
if [ -n "$running" ]; then
|
|
337
|
+
echo "Status: running"
|
|
338
|
+
echo "Models: $(eagle_ollama_models "$url" | tr '\n' ', ' | sed 's/,$//')"
|
|
339
|
+
else
|
|
340
|
+
echo "Status: not running"
|
|
341
|
+
fi
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
echo ""
|
|
345
|
+
echo "Config: $EAGLE_CONFIG_FILE"
|
|
346
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Config management
|
|
4
|
+
# eagle-mem config [init|show|set|test]
|
|
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
|
+
. "$SCRIPT_DIR/style.sh"
|
|
13
|
+
. "$LIB_DIR/provider.sh"
|
|
14
|
+
|
|
15
|
+
eagle_header "Config"
|
|
16
|
+
|
|
17
|
+
subcommand="${1:-show}"
|
|
18
|
+
shift 2>/dev/null || true
|
|
19
|
+
|
|
20
|
+
case "$subcommand" in
|
|
21
|
+
init)
|
|
22
|
+
eagle_config_init
|
|
23
|
+
eagle_ok "Config created: $EAGLE_CONFIG_FILE"
|
|
24
|
+
echo ""
|
|
25
|
+
eagle_show_config
|
|
26
|
+
;;
|
|
27
|
+
|
|
28
|
+
show|status)
|
|
29
|
+
eagle_show_config
|
|
30
|
+
;;
|
|
31
|
+
|
|
32
|
+
set)
|
|
33
|
+
key="${1:-}"
|
|
34
|
+
value="${2:-}"
|
|
35
|
+
if [ -z "$key" ] || [ -z "$value" ]; then
|
|
36
|
+
eagle_err "Usage: eagle-mem config set <section.key> <value>"
|
|
37
|
+
eagle_info "Examples:"
|
|
38
|
+
eagle_info " eagle-mem config set provider.type ollama"
|
|
39
|
+
eagle_info " eagle-mem config set ollama.model mistral"
|
|
40
|
+
eagle_info " eagle-mem config set anthropic.model claude-haiku-4-5-20251001"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
section="${key%%.*}"
|
|
44
|
+
config_key="${key#*.}"
|
|
45
|
+
eagle_config_set "$section" "$config_key" "$value"
|
|
46
|
+
eagle_ok "Set [$section] $config_key = $value"
|
|
47
|
+
;;
|
|
48
|
+
|
|
49
|
+
test)
|
|
50
|
+
provider=$(eagle_config_get "provider" "type" "none")
|
|
51
|
+
if [ "$provider" = "none" ]; then
|
|
52
|
+
eagle_err "No provider configured. Run: eagle-mem config init"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
eagle_info "Testing $provider provider..."
|
|
57
|
+
result=$(eagle_llm_call "Respond with exactly: Eagle Mem provider test successful" "You are a test assistant. Follow instructions exactly." 50)
|
|
58
|
+
if [ -n "$result" ]; then
|
|
59
|
+
eagle_ok "Provider working"
|
|
60
|
+
echo " Response: $result"
|
|
61
|
+
else
|
|
62
|
+
eagle_err "Provider call failed. Check logs: $EAGLE_MEM_LOG"
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
;;
|
|
66
|
+
|
|
67
|
+
*)
|
|
68
|
+
eagle_err "Unknown config command: $subcommand"
|
|
69
|
+
eagle_info "Usage: eagle-mem config [init|show|set|test]"
|
|
70
|
+
exit 1
|
|
71
|
+
;;
|
|
72
|
+
esac
|