eagle-mem 1.6.0 → 1.6.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 CHANGED
@@ -29,7 +29,7 @@ case "$command" in
29
29
  help|--help|-h)
30
30
  bash "$SCRIPTS_DIR/help.sh" ;;
31
31
  version|--version|-v|-V)
32
- version=$(node -e "console.log(require('$PACKAGE_DIR/package.json').version)" 2>/dev/null || echo "unknown")
32
+ version=$(jq -r .version "$PACKAGE_DIR/package.json" 2>/dev/null || echo "unknown")
33
33
  echo -e " ${BOLD}Eagle Mem${RESET} v${version}"
34
34
  ;;
35
35
  *)
@@ -0,0 +1,29 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Migration 009: Drop dead tasks table
3
+ -- The original tasks table (from schema.sql / migration 001)
4
+ -- was replaced by claude_tasks (migration 007). No code
5
+ -- references the old table. This cleans up the vestigial
6
+ -- schema: 3 FTS triggers, the FTS virtual table, 3 indexes,
7
+ -- and the table itself.
8
+ --
9
+ -- NOTE: schema.sql (migration 001) still contains the CREATE
10
+ -- TABLE tasks definition. We do NOT edit applied migrations.
11
+ -- Fresh installs will CREATE (001) then DROP (009). Existing
12
+ -- installs just run 009. Verified 0 rows in production.
13
+ -- ═══════════════════════════════════════════════════════════
14
+
15
+ -- Drop FTS sync triggers first (they reference the tasks table)
16
+ DROP TRIGGER IF EXISTS tasks_ai;
17
+ DROP TRIGGER IF EXISTS tasks_ad;
18
+ DROP TRIGGER IF EXISTS tasks_au;
19
+
20
+ -- Drop the FTS virtual table
21
+ DROP TABLE IF EXISTS tasks_fts;
22
+
23
+ -- Drop indexes (implicit in DROP TABLE, but explicit for clarity)
24
+ DROP INDEX IF EXISTS idx_tasks_project;
25
+ DROP INDEX IF EXISTS idx_tasks_status;
26
+ DROP INDEX IF EXISTS idx_tasks_parent;
27
+
28
+ -- Drop the dead tasks table
29
+ DROP TABLE IF EXISTS tasks;
package/db/migrate.sh CHANGED
@@ -25,7 +25,8 @@ run_migration() {
25
25
  body=$(grep -v -E '^[[:space:]]*PRAGMA ' "$file")
26
26
 
27
27
  # Set connection PRAGMAs, then run migration body + tracking insert atomically
28
- { echo "PRAGMA trusted_schema=ON;"; echo "PRAGMA foreign_keys=ON;"; echo "PRAGMA busy_timeout=5000;"; echo "BEGIN;"; echo "$body"; echo "INSERT INTO _migrations (name) VALUES ('$name');"; echo "COMMIT;"; } | sqlite3 "$DB"
28
+ # .bail on ensures sqlite3 stops on the first error instead of continuing
29
+ { echo ".bail on"; echo "PRAGMA trusted_schema=ON;"; echo "PRAGMA foreign_keys=ON;"; echo "PRAGMA busy_timeout=5000;"; echo "BEGIN;"; echo "$body"; echo "INSERT INTO _migrations (name) VALUES ('$name');"; echo "COMMIT;"; } | sqlite3 "$DB"
29
30
  echo " applied: $name"
30
31
  fi
31
32
  }
@@ -64,4 +65,7 @@ run_migration "007_claude_tasks" "$SCRIPT_DIR/007_claude_tasks.sql"
64
65
  # ─── Migration 008: Summary UPSERT (unique session_id) ───
65
66
  run_migration "008_summary_upsert" "$SCRIPT_DIR/008_summary_upsert.sql"
66
67
 
68
+ # ─── Migration 009: Drop dead tasks table ────────────────
69
+ run_migration "009_drop_dead_tasks" "$SCRIPT_DIR/009_drop_dead_tasks.sql"
70
+
67
71
  echo " Eagle Mem database ready: $DB"
package/db/schema.sql CHANGED
@@ -75,28 +75,6 @@ CREATE TABLE IF NOT EXISTS summaries (
75
75
  CREATE INDEX IF NOT EXISTS idx_summaries_session ON summaries(session_id);
76
76
  CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project);
77
77
 
78
- -- ─── Tasks (TaskAware Compact Loop) ───────────────────────
79
- -- Subtasks for multi-step work with compaction between each
80
-
81
- CREATE TABLE IF NOT EXISTS tasks (
82
- id INTEGER PRIMARY KEY AUTOINCREMENT,
83
- project TEXT NOT NULL,
84
- session_id TEXT REFERENCES sessions(id),
85
- parent_id INTEGER REFERENCES tasks(id),
86
- title TEXT NOT NULL,
87
- instructions TEXT,
88
- context_snapshot TEXT,
89
- status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'done', 'blocked', 'cancelled')),
90
- ordinal INTEGER NOT NULL DEFAULT 0,
91
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
92
- started_at TEXT,
93
- completed_at TEXT
94
- );
95
-
96
- CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project);
97
- CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
98
- CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
99
-
100
78
  -- ─── FTS5: Full-text search on summaries ───────────────────
101
79
 
102
80
  CREATE VIRTUAL TABLE IF NOT EXISTS summaries_fts USING fts5(
@@ -127,30 +105,3 @@ CREATE TRIGGER IF NOT EXISTS summaries_au AFTER UPDATE ON summaries BEGIN
127
105
  INSERT INTO summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
128
106
  VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
129
107
  END;
130
-
131
- -- ─── FTS5: Full-text search on tasks ──────────────────────
132
-
133
- CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
134
- title,
135
- instructions,
136
- context_snapshot,
137
- content='tasks',
138
- content_rowid='id'
139
- );
140
-
141
- CREATE TRIGGER IF NOT EXISTS tasks_ai AFTER INSERT ON tasks BEGIN
142
- INSERT INTO tasks_fts(rowid, title, instructions, context_snapshot)
143
- VALUES (new.id, new.title, new.instructions, new.context_snapshot);
144
- END;
145
-
146
- CREATE TRIGGER IF NOT EXISTS tasks_ad AFTER DELETE ON tasks BEGIN
147
- INSERT INTO tasks_fts(tasks_fts, rowid, title, instructions, context_snapshot)
148
- VALUES ('delete', old.id, old.title, old.instructions, old.context_snapshot);
149
- END;
150
-
151
- CREATE TRIGGER IF NOT EXISTS tasks_au AFTER UPDATE ON tasks BEGIN
152
- INSERT INTO tasks_fts(tasks_fts, rowid, title, instructions, context_snapshot)
153
- VALUES ('delete', old.id, old.title, old.instructions, old.context_snapshot);
154
- INSERT INTO tasks_fts(rowid, title, instructions, context_snapshot)
155
- VALUES (new.id, new.title, new.instructions, new.context_snapshot);
156
- END;
@@ -83,7 +83,11 @@ esac
83
83
  case "$tool_name" in
84
84
  Write|Edit)
85
85
  if [ -n "$fp" ]; then
86
+ # Reject path traversal: bash case `*` matches `/`, so
87
+ # patterns like projects/*/memory/*.md would match paths
88
+ # containing /../ segments. Block any path with `..` first.
86
89
  case "$fp" in
90
+ *..*) ;; # path traversal — skip
87
91
  "$HOME/.claude/projects"/*/memory/*.md)
88
92
  mem_base=$(basename "$fp")
89
93
  if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
@@ -31,8 +31,10 @@ eagle_log "INFO" "SessionStart: session=$session_id project=$project source=$sou
31
31
  eagle_upsert_session "$session_id" "$project" "$cwd" "$model" "$source_type"
32
32
 
33
33
  # ─── Sweep stuck sessions (older than 4 hours) ─────────────
34
+ # Exclude the current session — it may be a resumed session older than 4h
34
35
  eagle_db "UPDATE sessions SET status = 'abandoned'
35
36
  WHERE status = 'active'
37
+ AND id != '$(eagle_sql_escape "$session_id")'
36
38
  AND started_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-4 hours');"
37
39
 
38
40
  # ─── Build context injection ────────────────────────────────
@@ -115,7 +117,7 @@ fi
115
117
  synced_tasks=$(eagle_db "SELECT subject, status, blocked_by FROM claude_tasks
116
118
  WHERE project = '$(eagle_sql_escape "$project")'
117
119
  AND status IN ('in_progress', 'pending')
118
- AND updated_at > datetime('now', '-7 days')
120
+ AND updated_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days')
119
121
  ORDER BY
120
122
  CASE status WHEN 'in_progress' THEN 0 ELSE 1 END,
121
123
  updated_at DESC
@@ -144,8 +146,7 @@ You have persistent memory powered by Eagle Mem. When you recall context from a
144
146
  IMPORTANT: At the start of your VERY NEXT response (this fires on session start, /clear, AND context compaction — always show this block, even if you think you showed it before, because prior context may have been compressed away). Show the user what Eagle Mem loaded using this exact format:
145
147
 
146
148
  \`\`\`
147
- █▀▀ ▄▀█ █▀▀ █ █▀▀ █▀▄▀█ █▀▀ █▀▄▀█
148
- ██▄ █▀█ █▄█ █▄▄ ██▄ █ ▀ █ ██▄ █ ▀ █
149
+ $eagle_logo
149
150
 
150
151
  Project: <project name>
151
152
  Sessions: N recent | Memories: N | Tasks: N pending
package/lib/common.sh CHANGED
@@ -46,6 +46,8 @@ eagle_fts_sanitize() {
46
46
  # Claude Code session IDs are UUIDs or hex strings — reject anything else.
47
47
  eagle_validate_session_id() {
48
48
  local sid="$1"
49
+ # Length cap: Claude Code IDs are UUIDs/hex (36-64 chars). Reject oversized input.
50
+ [ ${#sid} -gt 128 ] && return 1
49
51
  [[ "$sid" =~ ^[A-Za-z0-9_-]+$ ]]
50
52
  }
51
53
 
package/lib/db.sh CHANGED
@@ -18,7 +18,7 @@ eagle_db() {
18
18
  }
19
19
 
20
20
  eagle_db_pipe() {
21
- { echo "$EAGLE_DB_SETUP"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>>"$EAGLE_MEM_LOG"
21
+ { echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>>"$EAGLE_MEM_LOG"
22
22
  }
23
23
 
24
24
  eagle_db_json() {
@@ -45,7 +45,8 @@ eagle_upsert_session() {
45
45
  ON CONFLICT(id) DO UPDATE SET
46
46
  cwd = COALESCE(excluded.cwd, sessions.cwd),
47
47
  model = COALESCE(excluded.model, sessions.model),
48
- source = COALESCE(excluded.source, sessions.source);"
48
+ source = COALESCE(excluded.source, sessions.source),
49
+ status = 'active';"
49
50
  }
50
51
 
51
52
  eagle_end_session() {
@@ -78,7 +79,7 @@ eagle_insert_summary() {
78
79
  local notes; notes=$(eagle_sql_escape "${10:-}")
79
80
 
80
81
  eagle_db_pipe <<SQL
81
- INSERT OR REPLACE INTO summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_modified, notes)
82
+ INSERT INTO summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_modified, notes)
82
83
  VALUES (
83
84
  '$session_id',
84
85
  '$project',
@@ -90,7 +91,17 @@ VALUES (
90
91
  '$files_read',
91
92
  '$files_modified',
92
93
  '$notes'
93
- );
94
+ )
95
+ ON CONFLICT(session_id) DO UPDATE SET
96
+ project = excluded.project,
97
+ request = COALESCE(NULLIF(excluded.request, ''), summaries.request),
98
+ investigated = COALESCE(NULLIF(excluded.investigated, ''), summaries.investigated),
99
+ learned = COALESCE(NULLIF(excluded.learned, ''), summaries.learned),
100
+ completed = COALESCE(NULLIF(excluded.completed, ''), summaries.completed),
101
+ next_steps = COALESCE(NULLIF(excluded.next_steps, ''), summaries.next_steps),
102
+ files_read = COALESCE(NULLIF(excluded.files_read, '[]'), summaries.files_read),
103
+ files_modified = COALESCE(NULLIF(excluded.files_modified, '[]'), summaries.files_modified),
104
+ notes = COALESCE(NULLIF(excluded.notes, ''), summaries.notes);
94
105
  SQL
95
106
  }
96
107
 
@@ -107,7 +118,8 @@ eagle_get_recent_summaries() {
107
118
  }
108
119
 
109
120
  eagle_search_summaries() {
110
- local query; query=$(eagle_sql_escape "$1")
121
+ local query; query=$(eagle_fts_sanitize "$1")
122
+ query=$(eagle_sql_escape "$query")
111
123
  local project="${2:-}"
112
124
  local limit; limit=$(eagle_sql_int "${3:-10}")
113
125
 
@@ -156,7 +168,8 @@ eagle_get_overview() {
156
168
  }
157
169
 
158
170
  eagle_search_code_chunks() {
159
- local query; query=$(eagle_sql_escape "$1")
171
+ local query; query=$(eagle_fts_sanitize "$1")
172
+ query=$(eagle_sql_escape "$query")
160
173
  local project; project=$(eagle_sql_escape "$2")
161
174
  local limit; limit=$(eagle_sql_int "${3:-5}")
162
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Lightweight persistent memory for Claude Code — SQLite + FTS5, no daemon, no bloat",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/help.sh CHANGED
@@ -8,7 +8,7 @@ PACKAGE_DIR="$(cd "$SCRIPTS_DIR/.." && pwd)"
8
8
 
9
9
  . "$SCRIPTS_DIR/style.sh"
10
10
 
11
- version=$(node -e "console.log(require('$PACKAGE_DIR/package.json').version)" 2>/dev/null || echo "unknown")
11
+ version=$(jq -r .version "$PACKAGE_DIR/package.json" 2>/dev/null || echo "unknown")
12
12
 
13
13
  eagle_banner
14
14
 
package/scripts/index.sh CHANGED
@@ -104,7 +104,6 @@ fi
104
104
  project_sql=$(eagle_sql_escape "$PROJECT")
105
105
  NEEDS_INDEX="$TMPDIR_IDX/needs_index"
106
106
 
107
- indexed_count=0
108
107
  skipped_count=0
109
108
 
110
109
  while IFS= read -r file; do
package/scripts/prune.sh CHANGED
@@ -15,6 +15,33 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
15
15
 
16
16
  eagle_ensure_db
17
17
 
18
+ # ─── Help ────────────────────────────────────────────────
19
+
20
+ show_help() {
21
+ echo -e " ${BOLD}eagle-mem prune${RESET} — Clean up old data"
22
+ echo ""
23
+ echo -e " ${BOLD}Usage:${RESET}"
24
+ echo -e " eagle-mem prune ${DIM}# prune observations > 90 days${RESET}"
25
+ echo -e " eagle-mem prune ${CYAN}--days 30${RESET} ${DIM}# prune observations > 30 days${RESET}"
26
+ echo -e " eagle-mem prune ${CYAN}--dry-run${RESET} ${DIM}# show what would be pruned${RESET}"
27
+ echo ""
28
+ echo -e " ${BOLD}What gets pruned:${RESET}"
29
+ echo -e " ${DOT} Observations older than --days (default: 90)"
30
+ echo -e " ${DOT} Code chunks for files that no longer exist"
31
+ echo ""
32
+ echo -e " ${BOLD}What is preserved:${RESET}"
33
+ echo -e " ${DOT} All sessions and summaries (your session history)"
34
+ echo -e " ${DOT} All tasks"
35
+ echo -e " ${DOT} Project overviews"
36
+ echo ""
37
+ echo -e " ${BOLD}Options:${RESET}"
38
+ echo -e " ${CYAN}-d, --days${RESET} <N> Age threshold (default: 90)"
39
+ echo -e " ${CYAN}-p, --project${RESET} <name> Only prune for this project"
40
+ echo -e " ${CYAN}-n, --dry-run${RESET} Show counts without deleting"
41
+ echo ""
42
+ exit 0
43
+ }
44
+
18
45
  # ─── Parse arguments ──────────────────────────────────────
19
46
 
20
47
  days=90
@@ -26,30 +53,7 @@ while [ $# -gt 0 ]; do
26
53
  --days|-d) days="$2"; shift 2 ;;
27
54
  --project|-p) project="$2"; shift 2 ;;
28
55
  --dry-run|-n) dry_run=true; shift ;;
29
- --help|-h)
30
- echo -e " ${BOLD}eagle-mem prune${RESET} — Clean up old data"
31
- echo ""
32
- echo -e " ${BOLD}Usage:${RESET}"
33
- echo -e " eagle-mem prune ${DIM}# prune observations > 90 days${RESET}"
34
- echo -e " eagle-mem prune ${CYAN}--days 30${RESET} ${DIM}# prune observations > 30 days${RESET}"
35
- echo -e " eagle-mem prune ${CYAN}--dry-run${RESET} ${DIM}# show what would be pruned${RESET}"
36
- echo ""
37
- echo -e " ${BOLD}What gets pruned:${RESET}"
38
- echo -e " ${DOT} Observations older than --days (default: 90)"
39
- echo -e " ${DOT} Code chunks for files that no longer exist"
40
- echo ""
41
- echo -e " ${BOLD}What is preserved:${RESET}"
42
- echo -e " ${DOT} All sessions and summaries (your session history)"
43
- echo -e " ${DOT} All tasks"
44
- echo -e " ${DOT} Project overviews"
45
- echo ""
46
- echo -e " ${BOLD}Options:${RESET}"
47
- echo -e " ${CYAN}-d, --days${RESET} <N> Age threshold (default: 90)"
48
- echo -e " ${CYAN}-p, --project${RESET} <name> Only prune for this project"
49
- echo -e " ${CYAN}-n, --dry-run${RESET} Show counts without deleting"
50
- echo ""
51
- exit 0
52
- ;;
56
+ --help|-h) show_help ;;
53
57
  *)
54
58
  eagle_err "Unknown option: $1"
55
59
  exit 1
@@ -109,7 +113,6 @@ if [ -n "$projects" ]; then
109
113
 
110
114
  if [ -n "$proj_cwd" ] && [ -d "$proj_cwd" ]; then
111
115
  if [ "$dry_run" = true ]; then
112
- orphan_count=$(eagle_db "SELECT COUNT(DISTINCT file_path) FROM code_chunks WHERE project = '$(eagle_sql_escape "$proj")';")
113
116
  # Count files that no longer exist
114
117
  orphans=0
115
118
  paths=$(eagle_db "SELECT DISTINCT file_path FROM code_chunks WHERE project = '$(eagle_sql_escape "$proj")';")
package/scripts/search.sh CHANGED
@@ -14,6 +14,27 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
14
14
 
15
15
  eagle_ensure_db
16
16
 
17
+ # ─── Help ────────────────────────────────────────────────
18
+
19
+ show_help() {
20
+ echo -e " ${BOLD}eagle-mem search${RESET} — Search persistent memory"
21
+ echo ""
22
+ echo -e " ${BOLD}Usage:${RESET}"
23
+ echo -e " eagle-mem search ${CYAN}<query>${RESET} ${DIM}# keyword search${RESET}"
24
+ echo -e " eagle-mem search ${CYAN}--timeline${RESET} ${DIM}# recent sessions${RESET}"
25
+ echo -e " eagle-mem search ${CYAN}--session <id>${RESET} ${DIM}# session details${RESET}"
26
+ echo -e " eagle-mem search ${CYAN}--files${RESET} ${DIM}# frequently modified files${RESET}"
27
+ echo -e " eagle-mem search ${CYAN}--stats${RESET} ${DIM}# project statistics${RESET}"
28
+ echo ""
29
+ echo -e " ${BOLD}Options:${RESET}"
30
+ echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current dir)"
31
+ echo -e " ${CYAN}-n, --limit${RESET} <N> Max results (default: 10)"
32
+ echo -e " ${CYAN}-a, --all${RESET} Search across all projects"
33
+ echo -e " ${CYAN}-j, --json${RESET} Output as JSON"
34
+ echo ""
35
+ exit 0
36
+ }
37
+
17
38
  # ─── Parse arguments ──────────────────────────────────────
18
39
 
19
40
  mode="keyword"
@@ -34,24 +55,7 @@ while [ $# -gt 0 ]; do
34
55
  --limit|-n) limit="$2"; shift 2 ;;
35
56
  --all|-a) cross_project=true; shift ;;
36
57
  --json|-j) json_output=true; shift ;;
37
- --help|-h)
38
- echo -e " ${BOLD}eagle-mem search${RESET} — Search persistent memory"
39
- echo ""
40
- echo -e " ${BOLD}Usage:${RESET}"
41
- echo -e " eagle-mem search ${CYAN}<query>${RESET} ${DIM}# keyword search${RESET}"
42
- echo -e " eagle-mem search ${CYAN}--timeline${RESET} ${DIM}# recent sessions${RESET}"
43
- echo -e " eagle-mem search ${CYAN}--session <id>${RESET} ${DIM}# session details${RESET}"
44
- echo -e " eagle-mem search ${CYAN}--files${RESET} ${DIM}# frequently modified files${RESET}"
45
- echo -e " eagle-mem search ${CYAN}--stats${RESET} ${DIM}# project statistics${RESET}"
46
- echo ""
47
- echo -e " ${BOLD}Options:${RESET}"
48
- echo -e " ${CYAN}-p, --project${RESET} <name> Project name (default: current dir)"
49
- echo -e " ${CYAN}-n, --limit${RESET} <N> Max results (default: 10)"
50
- echo -e " ${CYAN}-a, --all${RESET} Search across all projects"
51
- echo -e " ${CYAN}-j, --json${RESET} Output as JSON"
52
- echo ""
53
- exit 0
54
- ;;
58
+ --help|-h) show_help ;;
55
59
  -*)
56
60
  eagle_err "Unknown option: $1"
57
61
  exit 1
package/scripts/tasks.sh CHANGED
@@ -29,6 +29,7 @@ show_help() {
29
29
  echo -e " ${BOLD}Usage:${RESET}"
30
30
  echo -e " eagle-mem tasks ${DIM}# list pending/in-progress tasks${RESET}"
31
31
  echo -e " eagle-mem tasks ${CYAN}list${RESET} ${DIM}# list all tasks${RESET}"
32
+ echo -e " eagle-mem tasks ${CYAN}completed${RESET} ${DIM}# list completed tasks${RESET}"
32
33
  echo -e " eagle-mem tasks ${CYAN}search${RESET} <query> ${DIM}# search tasks by keyword${RESET}"
33
34
  echo ""
34
35
  echo -e " ${BOLD}Options:${RESET}"
package/scripts/update.sh CHANGED
@@ -117,5 +117,5 @@ fi
117
117
 
118
118
  # ─── Summary ───────────────────────────────────────────────
119
119
 
120
- version=$(node -e "console.log(require('$PACKAGE_DIR/package.json').version)" 2>/dev/null || echo "unknown")
120
+ version=$(jq -r .version "$PACKAGE_DIR/package.json" 2>/dev/null || echo "unknown")
121
121
  eagle_footer "Eagle Mem updated to v${version}."