eagle-mem 1.2.0 → 1.3.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 +1 -0
- package/db/005_claude_memories.sql +49 -0
- package/db/006_claude_plans.sql +45 -0
- package/db/007_claude_tasks.sql +50 -0
- package/db/migrate.sh +9 -0
- package/hooks/post-tool-use.sh +46 -2
- package/lib/db.sh +265 -0
- package/package.json +1 -1
- package/scripts/help.sh +6 -0
- package/scripts/install.sh +1 -1
- package/scripts/memories.sh +575 -0
- package/scripts/update.sh +6 -1
package/bin/eagle-mem
CHANGED
|
@@ -25,6 +25,7 @@ case "$command" in
|
|
|
25
25
|
tasks) bash "$SCRIPTS_DIR/tasks.sh" "$@" ;;
|
|
26
26
|
overview) bash "$SCRIPTS_DIR/overview.sh" "$@" ;;
|
|
27
27
|
prune) bash "$SCRIPTS_DIR/prune.sh" "$@" ;;
|
|
28
|
+
memories) bash "$SCRIPTS_DIR/memories.sh" "$@" ;;
|
|
28
29
|
help|--help|-h)
|
|
29
30
|
bash "$SCRIPTS_DIR/help.sh" ;;
|
|
30
31
|
version|--version|-v|-V)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- ═══════════════════════════════════════════════════════════
|
|
2
|
+
-- Eagle Mem — Migration 005: Claude Code memory mirror
|
|
3
|
+
-- Captures Claude Code auto-memory writes into Eagle Mem
|
|
4
|
+
-- ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = ON;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS claude_memories (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
project TEXT NOT NULL,
|
|
11
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
12
|
+
memory_name TEXT,
|
|
13
|
+
description TEXT,
|
|
14
|
+
memory_type TEXT,
|
|
15
|
+
content TEXT,
|
|
16
|
+
content_hash TEXT,
|
|
17
|
+
origin_session_id TEXT,
|
|
18
|
+
captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
19
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_claude_memories_project ON claude_memories(project);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_claude_memories_type ON claude_memories(memory_type);
|
|
24
|
+
|
|
25
|
+
-- FTS5 for searching across memory content
|
|
26
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS claude_memories_fts USING fts5(
|
|
27
|
+
memory_name,
|
|
28
|
+
description,
|
|
29
|
+
content,
|
|
30
|
+
content='claude_memories',
|
|
31
|
+
content_rowid='id'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TRIGGER IF NOT EXISTS claude_memories_ai AFTER INSERT ON claude_memories BEGIN
|
|
35
|
+
INSERT INTO claude_memories_fts(rowid, memory_name, description, content)
|
|
36
|
+
VALUES (new.id, new.memory_name, new.description, new.content);
|
|
37
|
+
END;
|
|
38
|
+
|
|
39
|
+
CREATE TRIGGER IF NOT EXISTS claude_memories_ad AFTER DELETE ON claude_memories BEGIN
|
|
40
|
+
INSERT INTO claude_memories_fts(claude_memories_fts, rowid, memory_name, description, content)
|
|
41
|
+
VALUES ('delete', old.id, old.memory_name, old.description, old.content);
|
|
42
|
+
END;
|
|
43
|
+
|
|
44
|
+
CREATE TRIGGER IF NOT EXISTS claude_memories_au AFTER UPDATE ON claude_memories BEGIN
|
|
45
|
+
INSERT INTO claude_memories_fts(claude_memories_fts, rowid, memory_name, description, content)
|
|
46
|
+
VALUES ('delete', old.id, old.memory_name, old.description, old.content);
|
|
47
|
+
INSERT INTO claude_memories_fts(rowid, memory_name, description, content)
|
|
48
|
+
VALUES (new.id, new.memory_name, new.description, new.content);
|
|
49
|
+
END;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- ═══════════════════════════════════════════════════════════
|
|
2
|
+
-- Eagle Mem — Migration 006: Claude Code plan mirror
|
|
3
|
+
-- Captures Claude Code plan documents into Eagle Mem
|
|
4
|
+
-- ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = ON;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS claude_plans (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
project TEXT NOT NULL DEFAULT '',
|
|
11
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
12
|
+
title TEXT,
|
|
13
|
+
content TEXT,
|
|
14
|
+
content_hash TEXT,
|
|
15
|
+
origin_session_id TEXT,
|
|
16
|
+
captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
17
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_claude_plans_project ON claude_plans(project);
|
|
21
|
+
|
|
22
|
+
-- FTS5 for searching across plan content
|
|
23
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS claude_plans_fts USING fts5(
|
|
24
|
+
title,
|
|
25
|
+
content,
|
|
26
|
+
content='claude_plans',
|
|
27
|
+
content_rowid='id'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TRIGGER IF NOT EXISTS claude_plans_ai AFTER INSERT ON claude_plans BEGIN
|
|
31
|
+
INSERT INTO claude_plans_fts(rowid, title, content)
|
|
32
|
+
VALUES (new.id, new.title, new.content);
|
|
33
|
+
END;
|
|
34
|
+
|
|
35
|
+
CREATE TRIGGER IF NOT EXISTS claude_plans_ad AFTER DELETE ON claude_plans BEGIN
|
|
36
|
+
INSERT INTO claude_plans_fts(claude_plans_fts, rowid, title, content)
|
|
37
|
+
VALUES ('delete', old.id, old.title, old.content);
|
|
38
|
+
END;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER IF NOT EXISTS claude_plans_au AFTER UPDATE ON claude_plans BEGIN
|
|
41
|
+
INSERT INTO claude_plans_fts(claude_plans_fts, rowid, title, content)
|
|
42
|
+
VALUES ('delete', old.id, old.title, old.content);
|
|
43
|
+
INSERT INTO claude_plans_fts(rowid, title, content)
|
|
44
|
+
VALUES (new.id, new.title, new.content);
|
|
45
|
+
END;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- ═══════════════════════════════════════════════════════════
|
|
2
|
+
-- Migration 007: Claude Code task mirror
|
|
3
|
+
-- Mirrors TaskCreate/TaskUpdate artifacts from ~/.claude/tasks/
|
|
4
|
+
-- ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS claude_tasks (
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
project TEXT NOT NULL DEFAULT '',
|
|
9
|
+
source_session_id TEXT NOT NULL,
|
|
10
|
+
source_task_id TEXT NOT NULL,
|
|
11
|
+
file_path TEXT UNIQUE,
|
|
12
|
+
subject TEXT,
|
|
13
|
+
description TEXT,
|
|
14
|
+
active_form TEXT,
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
16
|
+
blocks TEXT DEFAULT '[]',
|
|
17
|
+
blocked_by TEXT DEFAULT '[]',
|
|
18
|
+
content_hash TEXT,
|
|
19
|
+
captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
20
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_claude_tasks_project ON claude_tasks(project);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_claude_tasks_session ON claude_tasks(source_session_id);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_claude_tasks_status ON claude_tasks(status);
|
|
26
|
+
|
|
27
|
+
-- FTS5 for full-text search on subject + description
|
|
28
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS claude_tasks_fts USING fts5(
|
|
29
|
+
subject,
|
|
30
|
+
description,
|
|
31
|
+
content='claude_tasks',
|
|
32
|
+
content_rowid='id'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TRIGGER IF NOT EXISTS claude_tasks_ai AFTER INSERT ON claude_tasks BEGIN
|
|
36
|
+
INSERT INTO claude_tasks_fts(rowid, subject, description)
|
|
37
|
+
VALUES (new.id, new.subject, new.description);
|
|
38
|
+
END;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER IF NOT EXISTS claude_tasks_ad AFTER DELETE ON claude_tasks BEGIN
|
|
41
|
+
INSERT INTO claude_tasks_fts(claude_tasks_fts, rowid, subject, description)
|
|
42
|
+
VALUES ('delete', old.id, old.subject, old.description);
|
|
43
|
+
END;
|
|
44
|
+
|
|
45
|
+
CREATE TRIGGER IF NOT EXISTS claude_tasks_au AFTER UPDATE ON claude_tasks BEGIN
|
|
46
|
+
INSERT INTO claude_tasks_fts(claude_tasks_fts, rowid, subject, description)
|
|
47
|
+
VALUES ('delete', old.id, old.subject, old.description);
|
|
48
|
+
INSERT INTO claude_tasks_fts(rowid, subject, description)
|
|
49
|
+
VALUES (new.id, new.subject, new.description);
|
|
50
|
+
END;
|
package/db/migrate.sh
CHANGED
|
@@ -47,4 +47,13 @@ run_migration "003_code_chunks" "$SCRIPT_DIR/003_code_chunks.sql"
|
|
|
47
47
|
# ─── Migration 004: Observation indexes ──────────────────
|
|
48
48
|
run_migration "004_observation_indexes" "$SCRIPT_DIR/004_observation_indexes.sql"
|
|
49
49
|
|
|
50
|
+
# ─── Migration 005: Claude Code memory mirror ────────────
|
|
51
|
+
run_migration "005_claude_memories" "$SCRIPT_DIR/005_claude_memories.sql"
|
|
52
|
+
|
|
53
|
+
# ─── Migration 006: Claude Code plan mirror ──────────────
|
|
54
|
+
run_migration "006_claude_plans" "$SCRIPT_DIR/006_claude_plans.sql"
|
|
55
|
+
|
|
56
|
+
# ─── Migration 007: Claude Code task mirror ──────────────
|
|
57
|
+
run_migration "007_claude_tasks" "$SCRIPT_DIR/007_claude_tasks.sql"
|
|
58
|
+
|
|
50
59
|
echo " Eagle Mem database ready: $DB"
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -21,9 +21,9 @@ tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
|
|
21
21
|
|
|
22
22
|
if [ -z "$session_id" ] || [ -z "$tool_name" ]; then exit 0; fi
|
|
23
23
|
|
|
24
|
-
# Only track
|
|
24
|
+
# Only track relevant tools
|
|
25
25
|
case "$tool_name" in
|
|
26
|
-
Read|Write|Edit|Bash) ;;
|
|
26
|
+
Read|Write|Edit|Bash|TaskCreate|TaskUpdate) ;;
|
|
27
27
|
*) exit 0 ;;
|
|
28
28
|
esac
|
|
29
29
|
|
|
@@ -63,6 +63,50 @@ case "$tool_name" in
|
|
|
63
63
|
-e 's/(Authorization: )[^ ]*/\1[REDACTED]/gi')
|
|
64
64
|
tool_summary="Bash: $cmd"
|
|
65
65
|
;;
|
|
66
|
+
TaskCreate|TaskUpdate)
|
|
67
|
+
task_subject=$(echo "$input" | jq -r '.tool_input.subject // empty')
|
|
68
|
+
tool_summary="$tool_name: $task_subject"
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
# ─── Claude memory + plan mirror ─────────────────────────
|
|
73
|
+
# Intercept writes to Claude Code's auto-memory and plan files
|
|
74
|
+
case "$tool_name" in
|
|
75
|
+
Write|Edit)
|
|
76
|
+
if [ -n "$fp" ]; then
|
|
77
|
+
case "$fp" in
|
|
78
|
+
"$HOME/.claude/projects"/*/memory/*.md)
|
|
79
|
+
mem_base=$(basename "$fp")
|
|
80
|
+
if [ "$mem_base" != "MEMORY.md" ] && [ -f "$fp" ]; then
|
|
81
|
+
eagle_capture_claude_memory "$fp" "$session_id" "$project"
|
|
82
|
+
fi
|
|
83
|
+
;;
|
|
84
|
+
"$HOME/.claude/plans/"*.md)
|
|
85
|
+
if [ -f "$fp" ]; then
|
|
86
|
+
eagle_capture_claude_plan "$fp" "$session_id" "$project"
|
|
87
|
+
fi
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
fi
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
|
|
94
|
+
# ─── Claude task mirror ─────────────────────────────────
|
|
95
|
+
# Intercept TaskCreate/TaskUpdate and capture the resulting JSON files
|
|
96
|
+
case "$tool_name" in
|
|
97
|
+
TaskCreate|TaskUpdate)
|
|
98
|
+
task_dir="$HOME/.claude/tasks/$session_id"
|
|
99
|
+
if [ -d "$task_dir" ]; then
|
|
100
|
+
task_id=$(echo "$input" | jq -r '.tool_input.id // empty')
|
|
101
|
+
if [ -z "$task_id" ]; then
|
|
102
|
+
newest=$(ls -t "$task_dir"/*.json 2>/dev/null | head -1)
|
|
103
|
+
[ -n "$newest" ] && [ -f "$newest" ] && eagle_capture_claude_task "$newest" "$session_id" "$project"
|
|
104
|
+
else
|
|
105
|
+
task_json="$task_dir/$task_id.json"
|
|
106
|
+
[ -f "$task_json" ] && eagle_capture_claude_task "$task_json" "$session_id" "$project"
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
;;
|
|
66
110
|
esac
|
|
67
111
|
|
|
68
112
|
# Deduplicate: skip if exact same observation within last 5 seconds
|
package/lib/db.sh
CHANGED
|
@@ -240,6 +240,271 @@ eagle_prune_observations() {
|
|
|
240
240
|
eagle_db "DELETE FROM observations WHERE created_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-$days days') $project_filter;"
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
eagle_capture_claude_memory() {
|
|
244
|
+
local file_path="$1"
|
|
245
|
+
local session_id="${2:-}"
|
|
246
|
+
local project="${3:-}"
|
|
247
|
+
|
|
248
|
+
[ ! -f "$file_path" ] && return 0
|
|
249
|
+
|
|
250
|
+
local chash
|
|
251
|
+
chash=$(shasum -a 256 "$file_path" | awk '{print $1}')
|
|
252
|
+
|
|
253
|
+
local fm body
|
|
254
|
+
fm=$(awk '/^---$/{c++; next} c==1' "$file_path")
|
|
255
|
+
body=$(awk '/^---$/{c++; next} c>=2' "$file_path")
|
|
256
|
+
|
|
257
|
+
_fm_field() { printf '%s\n' "$fm" | awk -F': *' -v k="$1" '$1==k{sub(/^[^:]+: */,""); gsub(/^"|"$/,""); print; exit}'; }
|
|
258
|
+
|
|
259
|
+
local mname mdesc mtype morigin
|
|
260
|
+
mname=$(_fm_field "name")
|
|
261
|
+
mdesc=$(_fm_field "description")
|
|
262
|
+
mtype=$(_fm_field "type")
|
|
263
|
+
morigin=$(_fm_field "originSessionId")
|
|
264
|
+
[ -z "$morigin" ] && morigin="$session_id"
|
|
265
|
+
|
|
266
|
+
local fp_sql proj_sql name_sql desc_sql type_sql content_sql hash_sql origin_sql
|
|
267
|
+
fp_sql=$(eagle_sql_escape "$file_path")
|
|
268
|
+
proj_sql=$(eagle_sql_escape "$project")
|
|
269
|
+
name_sql=$(eagle_sql_escape "$mname")
|
|
270
|
+
desc_sql=$(eagle_sql_escape "$mdesc")
|
|
271
|
+
type_sql=$(eagle_sql_escape "$mtype")
|
|
272
|
+
content_sql=$(eagle_sql_escape "$body")
|
|
273
|
+
hash_sql=$(eagle_sql_escape "$chash")
|
|
274
|
+
origin_sql=$(eagle_sql_escape "$morigin")
|
|
275
|
+
|
|
276
|
+
eagle_db_pipe <<SQL
|
|
277
|
+
INSERT INTO claude_memories (project, file_path, memory_name, description, memory_type, content, content_hash, origin_session_id)
|
|
278
|
+
VALUES ('$proj_sql', '$fp_sql', '$name_sql', '$desc_sql', '$type_sql', '$content_sql', '$hash_sql', '$origin_sql')
|
|
279
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
280
|
+
memory_name = excluded.memory_name,
|
|
281
|
+
description = excluded.description,
|
|
282
|
+
memory_type = excluded.memory_type,
|
|
283
|
+
content = excluded.content,
|
|
284
|
+
content_hash = excluded.content_hash,
|
|
285
|
+
origin_session_id = excluded.origin_session_id,
|
|
286
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
287
|
+
WHERE claude_memories.content_hash != excluded.content_hash;
|
|
288
|
+
SQL
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
eagle_search_claude_memories() {
|
|
292
|
+
local query; query=$(eagle_fts_sanitize "$1")
|
|
293
|
+
query=$(eagle_sql_escape "$query")
|
|
294
|
+
local project="${2:-}"
|
|
295
|
+
local limit; limit=$(eagle_sql_int "${3:-10}")
|
|
296
|
+
|
|
297
|
+
local where_clause=""
|
|
298
|
+
if [ -n "$project" ]; then
|
|
299
|
+
project=$(eagle_sql_escape "$project")
|
|
300
|
+
where_clause="AND m.project = '$project'"
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
eagle_db "SELECT m.memory_name, m.memory_type, m.description,
|
|
304
|
+
replace(substr(m.content, 1, 200), char(10), ' '),
|
|
305
|
+
m.file_path, m.updated_at
|
|
306
|
+
FROM claude_memories m
|
|
307
|
+
JOIN claude_memories_fts f ON f.rowid = m.id
|
|
308
|
+
WHERE claude_memories_fts MATCH '$query'
|
|
309
|
+
$where_clause
|
|
310
|
+
ORDER BY rank
|
|
311
|
+
LIMIT $limit;"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
eagle_list_claude_memories() {
|
|
315
|
+
local project="${1:-}"
|
|
316
|
+
local limit; limit=$(eagle_sql_int "${2:-20}")
|
|
317
|
+
|
|
318
|
+
local where_clause=""
|
|
319
|
+
if [ -n "$project" ]; then
|
|
320
|
+
project=$(eagle_sql_escape "$project")
|
|
321
|
+
where_clause="WHERE project = '$project'"
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at
|
|
325
|
+
FROM claude_memories
|
|
326
|
+
$where_clause
|
|
327
|
+
ORDER BY updated_at DESC
|
|
328
|
+
LIMIT $limit;"
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
eagle_get_claude_memory() {
|
|
332
|
+
local file_path; file_path=$(eagle_sql_escape "$1")
|
|
333
|
+
eagle_db "SELECT memory_name, memory_type, description, content, file_path, updated_at, origin_session_id
|
|
334
|
+
FROM claude_memories
|
|
335
|
+
WHERE file_path = '$file_path';"
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
eagle_capture_claude_plan() {
|
|
339
|
+
local file_path="$1"
|
|
340
|
+
local session_id="${2:-}"
|
|
341
|
+
local project="${3:-}"
|
|
342
|
+
|
|
343
|
+
[ ! -f "$file_path" ] && return 0
|
|
344
|
+
|
|
345
|
+
local chash
|
|
346
|
+
chash=$(shasum -a 256 "$file_path" | awk '{print $1}')
|
|
347
|
+
|
|
348
|
+
local title content
|
|
349
|
+
title=$(awk '/^# /{print; exit}' "$file_path" | sed 's/^# //')
|
|
350
|
+
content=$(cat "$file_path")
|
|
351
|
+
|
|
352
|
+
local fp_sql proj_sql title_sql content_sql hash_sql origin_sql
|
|
353
|
+
fp_sql=$(eagle_sql_escape "$file_path")
|
|
354
|
+
proj_sql=$(eagle_sql_escape "$project")
|
|
355
|
+
title_sql=$(eagle_sql_escape "$title")
|
|
356
|
+
content_sql=$(eagle_sql_escape "$content")
|
|
357
|
+
hash_sql=$(eagle_sql_escape "$chash")
|
|
358
|
+
origin_sql=$(eagle_sql_escape "$session_id")
|
|
359
|
+
|
|
360
|
+
eagle_db_pipe <<SQL
|
|
361
|
+
INSERT INTO claude_plans (project, file_path, title, content, content_hash, origin_session_id)
|
|
362
|
+
VALUES ('$proj_sql', '$fp_sql', '$title_sql', '$content_sql', '$hash_sql', '$origin_sql')
|
|
363
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
364
|
+
title = excluded.title,
|
|
365
|
+
content = excluded.content,
|
|
366
|
+
content_hash = excluded.content_hash,
|
|
367
|
+
origin_session_id = COALESCE(NULLIF(excluded.origin_session_id, ''), claude_plans.origin_session_id),
|
|
368
|
+
project = CASE WHEN excluded.project != '' THEN excluded.project ELSE claude_plans.project END,
|
|
369
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
370
|
+
WHERE claude_plans.content_hash != excluded.content_hash;
|
|
371
|
+
SQL
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
eagle_search_claude_plans() {
|
|
375
|
+
local query; query=$(eagle_fts_sanitize "$1")
|
|
376
|
+
query=$(eagle_sql_escape "$query")
|
|
377
|
+
local project="${2:-}"
|
|
378
|
+
local limit; limit=$(eagle_sql_int "${3:-10}")
|
|
379
|
+
|
|
380
|
+
local where_clause=""
|
|
381
|
+
if [ -n "$project" ]; then
|
|
382
|
+
project=$(eagle_sql_escape "$project")
|
|
383
|
+
where_clause="AND p.project = '$project'"
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
eagle_db "SELECT p.title, p.project,
|
|
387
|
+
replace(substr(p.content, 1, 200), char(10), ' '),
|
|
388
|
+
p.file_path, p.updated_at
|
|
389
|
+
FROM claude_plans p
|
|
390
|
+
JOIN claude_plans_fts f ON f.rowid = p.id
|
|
391
|
+
WHERE claude_plans_fts MATCH '$query'
|
|
392
|
+
$where_clause
|
|
393
|
+
ORDER BY rank
|
|
394
|
+
LIMIT $limit;"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
eagle_list_claude_plans() {
|
|
398
|
+
local project="${1:-}"
|
|
399
|
+
local limit; limit=$(eagle_sql_int "${2:-20}")
|
|
400
|
+
|
|
401
|
+
local where_clause=""
|
|
402
|
+
if [ -n "$project" ]; then
|
|
403
|
+
project=$(eagle_sql_escape "$project")
|
|
404
|
+
where_clause="WHERE project = '$project'"
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
eagle_db "SELECT title, project, file_path, updated_at
|
|
408
|
+
FROM claude_plans
|
|
409
|
+
$where_clause
|
|
410
|
+
ORDER BY updated_at DESC
|
|
411
|
+
LIMIT $limit;"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
eagle_capture_claude_task() {
|
|
415
|
+
local file_path="$1"
|
|
416
|
+
local session_id="${2:-}"
|
|
417
|
+
local project="${3:-}"
|
|
418
|
+
|
|
419
|
+
[ ! -f "$file_path" ] && return 0
|
|
420
|
+
|
|
421
|
+
local chash
|
|
422
|
+
chash=$(shasum -a 256 "$file_path" | awk '{print $1}')
|
|
423
|
+
|
|
424
|
+
local task_json
|
|
425
|
+
task_json=$(cat "$file_path")
|
|
426
|
+
|
|
427
|
+
local task_id subject desc active_form status blocks blocked_by
|
|
428
|
+
task_id=$(printf '%s' "$task_json" | jq -r '.id // empty')
|
|
429
|
+
subject=$(printf '%s' "$task_json" | jq -r '.subject // empty')
|
|
430
|
+
desc=$(printf '%s' "$task_json" | jq -r '.description // empty')
|
|
431
|
+
active_form=$(printf '%s' "$task_json" | jq -r '.activeForm // empty')
|
|
432
|
+
status=$(printf '%s' "$task_json" | jq -r '.status // "pending"')
|
|
433
|
+
blocks=$(printf '%s' "$task_json" | jq -c '.blocks // []')
|
|
434
|
+
blocked_by=$(printf '%s' "$task_json" | jq -c '.blockedBy // []')
|
|
435
|
+
|
|
436
|
+
[ -z "$task_id" ] && return 0
|
|
437
|
+
|
|
438
|
+
local fp_sql proj_sql sid_sql tid_sql subj_sql desc_sql af_sql status_sql blocks_sql bb_sql hash_sql
|
|
439
|
+
fp_sql=$(eagle_sql_escape "$file_path")
|
|
440
|
+
proj_sql=$(eagle_sql_escape "$project")
|
|
441
|
+
sid_sql=$(eagle_sql_escape "$session_id")
|
|
442
|
+
tid_sql=$(eagle_sql_escape "$task_id")
|
|
443
|
+
subj_sql=$(eagle_sql_escape "$subject")
|
|
444
|
+
desc_sql=$(eagle_sql_escape "$desc")
|
|
445
|
+
af_sql=$(eagle_sql_escape "$active_form")
|
|
446
|
+
status_sql=$(eagle_sql_escape "$status")
|
|
447
|
+
blocks_sql=$(eagle_sql_escape "$blocks")
|
|
448
|
+
bb_sql=$(eagle_sql_escape "$blocked_by")
|
|
449
|
+
hash_sql=$(eagle_sql_escape "$chash")
|
|
450
|
+
|
|
451
|
+
eagle_db_pipe <<SQL
|
|
452
|
+
INSERT INTO claude_tasks (project, source_session_id, source_task_id, file_path, subject, description, active_form, status, blocks, blocked_by, content_hash)
|
|
453
|
+
VALUES ('$proj_sql', '$sid_sql', '$tid_sql', '$fp_sql', '$subj_sql', '$desc_sql', '$af_sql', '$status_sql', '$blocks_sql', '$bb_sql', '$hash_sql')
|
|
454
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
455
|
+
subject = excluded.subject,
|
|
456
|
+
description = excluded.description,
|
|
457
|
+
active_form = excluded.active_form,
|
|
458
|
+
status = excluded.status,
|
|
459
|
+
blocks = excluded.blocks,
|
|
460
|
+
blocked_by = excluded.blocked_by,
|
|
461
|
+
content_hash = excluded.content_hash,
|
|
462
|
+
project = CASE WHEN excluded.project != '' THEN excluded.project ELSE claude_tasks.project END,
|
|
463
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
464
|
+
WHERE claude_tasks.content_hash != excluded.content_hash;
|
|
465
|
+
SQL
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
eagle_list_claude_tasks() {
|
|
469
|
+
local project="${1:-}"
|
|
470
|
+
local limit; limit=$(eagle_sql_int "${2:-20}")
|
|
471
|
+
|
|
472
|
+
local where_clause=""
|
|
473
|
+
if [ -n "$project" ]; then
|
|
474
|
+
project=$(eagle_sql_escape "$project")
|
|
475
|
+
where_clause="WHERE project = '$project'"
|
|
476
|
+
fi
|
|
477
|
+
|
|
478
|
+
eagle_db "SELECT subject, status, source_session_id, source_task_id, updated_at
|
|
479
|
+
FROM claude_tasks
|
|
480
|
+
$where_clause
|
|
481
|
+
ORDER BY updated_at DESC
|
|
482
|
+
LIMIT $limit;"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
eagle_search_claude_tasks() {
|
|
486
|
+
local query; query=$(eagle_fts_sanitize "$1")
|
|
487
|
+
query=$(eagle_sql_escape "$query")
|
|
488
|
+
local project="${2:-}"
|
|
489
|
+
local limit; limit=$(eagle_sql_int "${3:-10}")
|
|
490
|
+
|
|
491
|
+
local where_clause=""
|
|
492
|
+
if [ -n "$project" ]; then
|
|
493
|
+
project=$(eagle_sql_escape "$project")
|
|
494
|
+
where_clause="AND t.project = '$project'"
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
eagle_db "SELECT t.subject, t.status,
|
|
498
|
+
replace(substr(t.description, 1, 200), char(10), ' '),
|
|
499
|
+
t.source_session_id, t.source_task_id, t.updated_at
|
|
500
|
+
FROM claude_tasks t
|
|
501
|
+
JOIN claude_tasks_fts f ON f.rowid = t.id
|
|
502
|
+
WHERE claude_tasks_fts MATCH '$query'
|
|
503
|
+
$where_clause
|
|
504
|
+
ORDER BY rank
|
|
505
|
+
LIMIT $limit;"
|
|
506
|
+
}
|
|
507
|
+
|
|
243
508
|
eagle_prune_orphan_chunks() {
|
|
244
509
|
local project; project=$(eagle_sql_escape "$1")
|
|
245
510
|
local target_dir="$2"
|
package/package.json
CHANGED
package/scripts/help.sh
CHANGED
|
@@ -22,6 +22,7 @@ echo -e " ${BOLD}Commands:${RESET}"
|
|
|
22
22
|
echo -e " ${CYAN}search${RESET} Search past sessions, files, and observations"
|
|
23
23
|
echo -e " ${CYAN}tasks${RESET} Manage tracked tasks (add, done, block, list)"
|
|
24
24
|
echo -e " ${CYAN}overview${RESET} View or set project overview"
|
|
25
|
+
echo -e " ${CYAN}memories${RESET} Browse and search Claude Code memories, plans, and tasks"
|
|
25
26
|
echo -e " ${CYAN}scan${RESET} Analyze a project and generate an overview"
|
|
26
27
|
echo -e " ${CYAN}index${RESET} Index source files for code-level search"
|
|
27
28
|
echo -e " ${CYAN}prune${RESET} Remove old observations and orphaned chunks"
|
|
@@ -39,6 +40,10 @@ echo -e " ${DIM}\$${RESET} eagle-mem tasks add \"Fix X\" ${DIM}# Add a task$
|
|
|
39
40
|
echo -e " ${DIM}\$${RESET} eagle-mem overview ${DIM}# View overview${RESET}"
|
|
40
41
|
echo -e " ${DIM}\$${RESET} eagle-mem scan . ${DIM}# Scan current project${RESET}"
|
|
41
42
|
echo -e " ${DIM}\$${RESET} eagle-mem index . ${DIM}# Index source files${RESET}"
|
|
43
|
+
echo -e " ${DIM}\$${RESET} eagle-mem memories ${DIM}# List mirrored memories${RESET}"
|
|
44
|
+
echo -e " ${DIM}\$${RESET} eagle-mem memories sync ${DIM}# Backfill memories + plans + tasks${RESET}"
|
|
45
|
+
echo -e " ${DIM}\$${RESET} eagle-mem memories plans ${DIM}# List captured plans${RESET}"
|
|
46
|
+
echo -e " ${DIM}\$${RESET} eagle-mem memories tasks ${DIM}# List captured tasks${RESET}"
|
|
42
47
|
echo -e " ${DIM}\$${RESET} eagle-mem prune ${DIM}# Clean old data${RESET}"
|
|
43
48
|
echo -e " ${DIM}\$${RESET} eagle-mem install ${DIM}# First-time setup${RESET}"
|
|
44
49
|
echo ""
|
|
@@ -47,6 +52,7 @@ echo -e " ${DOT} Saves session summaries to a shared SQLite database"
|
|
|
47
52
|
echo -e " ${DOT} Injects relevant memory at session start"
|
|
48
53
|
echo -e " ${DOT} Searches past sessions when you ask related questions"
|
|
49
54
|
echo -e " ${DOT} Tracks file operations across sessions"
|
|
55
|
+
echo -e " ${DOT} Mirrors Claude Code memories, plans, and tasks into FTS5-searchable storage"
|
|
50
56
|
echo -e " ${DOT} Provides task management for complex multi-step work"
|
|
51
57
|
echo ""
|
|
52
58
|
echo -e " ${BOLD}Skills${RESET} ${DIM}(available inside Claude Code):${RESET}"
|
package/scripts/install.sh
CHANGED
|
@@ -190,7 +190,7 @@ patch_hook "Stop" "" \
|
|
|
190
190
|
"$EAGLE_MEM_DIR/hooks/stop.sh" \
|
|
191
191
|
"Stop hook"
|
|
192
192
|
|
|
193
|
-
patch_hook "PostToolUse" "Read|Write|Edit|Bash" \
|
|
193
|
+
patch_hook "PostToolUse" "Read|Write|Edit|Bash|TaskCreate|TaskUpdate" \
|
|
194
194
|
"$EAGLE_MEM_DIR/hooks/post-tool-use.sh" \
|
|
195
195
|
"PostToolUse hook"
|
|
196
196
|
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Claude Code Memory Mirror CLI
|
|
4
|
+
# List, show, search, and sync Claude Code auto-memories
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
LIB_DIR="$SCRIPTS_DIR/../lib"
|
|
10
|
+
|
|
11
|
+
. "$SCRIPTS_DIR/style.sh"
|
|
12
|
+
. "$LIB_DIR/common.sh"
|
|
13
|
+
. "$LIB_DIR/db.sh"
|
|
14
|
+
|
|
15
|
+
eagle_ensure_db
|
|
16
|
+
|
|
17
|
+
# ─── Parse arguments ──────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
action="${1:-list}"
|
|
20
|
+
shift 2>/dev/null || true
|
|
21
|
+
|
|
22
|
+
project=""
|
|
23
|
+
limit=20
|
|
24
|
+
query=""
|
|
25
|
+
|
|
26
|
+
show_help() {
|
|
27
|
+
echo -e " ${BOLD}eagle-mem memories${RESET} — Claude Code memory, plan & task mirror"
|
|
28
|
+
echo ""
|
|
29
|
+
echo -e " ${BOLD}Usage:${RESET}"
|
|
30
|
+
echo -e " eagle-mem memories ${DIM}# list all mirrored memories${RESET}"
|
|
31
|
+
echo -e " eagle-mem memories list ${DIM}# same as above${RESET}"
|
|
32
|
+
echo -e " eagle-mem memories search ${CYAN}<query>${RESET} ${DIM}# full-text search memories${RESET}"
|
|
33
|
+
echo -e " eagle-mem memories show ${CYAN}<file_path>${RESET} ${DIM}# show a specific memory${RESET}"
|
|
34
|
+
echo -e " eagle-mem memories plans ${DIM}# list captured plans${RESET}"
|
|
35
|
+
echo -e " eagle-mem memories plans search ${CYAN}<query>${RESET} ${DIM}# full-text search plans${RESET}"
|
|
36
|
+
echo -e " eagle-mem memories plans show ${CYAN}<file_path>${RESET} ${DIM}# show a specific plan${RESET}"
|
|
37
|
+
echo -e " eagle-mem memories tasks ${DIM}# list captured tasks${RESET}"
|
|
38
|
+
echo -e " eagle-mem memories tasks search ${CYAN}<query>${RESET} ${DIM}# full-text search tasks${RESET}"
|
|
39
|
+
echo -e " eagle-mem memories tasks show ${CYAN}<file_path>${RESET} ${DIM}# show a specific task${RESET}"
|
|
40
|
+
echo -e " eagle-mem memories sync ${DIM}# backfill memories + plans + tasks${RESET}"
|
|
41
|
+
echo ""
|
|
42
|
+
echo -e " ${BOLD}Options:${RESET}"
|
|
43
|
+
echo -e " ${CYAN}-p, --project${RESET} <name> Filter by project"
|
|
44
|
+
echo -e " ${CYAN}-l, --limit${RESET} <N> Max results (default: 20)"
|
|
45
|
+
echo ""
|
|
46
|
+
echo -e " ${BOLD}How it works:${RESET}"
|
|
47
|
+
echo -e " ${DOT} Eagle Mem intercepts Claude Code's auto-memory, plan, and task writes"
|
|
48
|
+
echo -e " ${DOT} All are mirrored into Eagle Mem's SQLite + FTS5"
|
|
49
|
+
echo -e " ${DOT} Use ${CYAN}sync${RESET} to backfill items written before mirroring was enabled"
|
|
50
|
+
echo ""
|
|
51
|
+
exit 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
plan_action=""
|
|
55
|
+
task_action=""
|
|
56
|
+
|
|
57
|
+
case "$action" in
|
|
58
|
+
--help|-h) show_help ;;
|
|
59
|
+
plans)
|
|
60
|
+
plan_action="${1:-list}"
|
|
61
|
+
shift 2>/dev/null || true
|
|
62
|
+
;;
|
|
63
|
+
tasks)
|
|
64
|
+
task_action="${1:-list}"
|
|
65
|
+
shift 2>/dev/null || true
|
|
66
|
+
;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
while [ $# -gt 0 ]; do
|
|
70
|
+
case "$1" in
|
|
71
|
+
--project|-p) project="$2"; shift 2 ;;
|
|
72
|
+
--limit|-l) limit="$2"; shift 2 ;;
|
|
73
|
+
--help|-h) show_help ;;
|
|
74
|
+
*)
|
|
75
|
+
if [ -z "$query" ]; then
|
|
76
|
+
query="$1"; shift
|
|
77
|
+
else
|
|
78
|
+
eagle_err "Unknown option: $1"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
# ─── Actions ─────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
memories_list() {
|
|
88
|
+
eagle_header "Memories"
|
|
89
|
+
|
|
90
|
+
local result
|
|
91
|
+
result=$(eagle_list_claude_memories "$project" "$limit")
|
|
92
|
+
|
|
93
|
+
if [ -z "$result" ]; then
|
|
94
|
+
eagle_dim "No mirrored memories found."
|
|
95
|
+
echo ""
|
|
96
|
+
eagle_dim "Memories are captured automatically when Claude Code writes to its auto-memory."
|
|
97
|
+
eagle_dim "Run 'eagle-mem memories sync' to backfill existing memories."
|
|
98
|
+
echo ""
|
|
99
|
+
return
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
local count=0
|
|
103
|
+
while IFS='|' read -r name mtype desc _fp updated; do
|
|
104
|
+
[ -z "$name" ] && continue
|
|
105
|
+
count=$((count + 1))
|
|
106
|
+
|
|
107
|
+
local type_color="$DIM"
|
|
108
|
+
case "$mtype" in
|
|
109
|
+
user) type_color="$CYAN" ;;
|
|
110
|
+
feedback) type_color="$YELLOW" ;;
|
|
111
|
+
project) type_color="$GREEN" ;;
|
|
112
|
+
reference) type_color="$BLUE" ;;
|
|
113
|
+
esac
|
|
114
|
+
|
|
115
|
+
echo -e " ${BOLD}${name}${RESET} ${type_color}[${mtype}]${RESET}"
|
|
116
|
+
[ -n "$desc" ] && echo -e " ${DIM}${desc}${RESET}"
|
|
117
|
+
echo -e " ${DIM}updated: ${updated}${RESET}"
|
|
118
|
+
echo ""
|
|
119
|
+
done <<< "$result"
|
|
120
|
+
|
|
121
|
+
eagle_dim "$count memories shown"
|
|
122
|
+
echo ""
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
memories_search() {
|
|
126
|
+
if [ -z "$query" ]; then
|
|
127
|
+
eagle_err "Usage: eagle-mem memories search <query>"
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
eagle_header "Memory Search"
|
|
132
|
+
eagle_info "Query: $query"
|
|
133
|
+
echo ""
|
|
134
|
+
|
|
135
|
+
local result
|
|
136
|
+
result=$(eagle_search_claude_memories "$query" "$project" "$limit")
|
|
137
|
+
|
|
138
|
+
if [ -z "$result" ]; then
|
|
139
|
+
eagle_dim "No memories matching '$query'"
|
|
140
|
+
echo ""
|
|
141
|
+
return
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
local count=0
|
|
145
|
+
while IFS='|' read -r name mtype desc content _fp updated; do
|
|
146
|
+
[ -z "$name" ] && continue
|
|
147
|
+
count=$((count + 1))
|
|
148
|
+
|
|
149
|
+
local type_color="$DIM"
|
|
150
|
+
case "$mtype" in
|
|
151
|
+
user) type_color="$CYAN" ;;
|
|
152
|
+
feedback) type_color="$YELLOW" ;;
|
|
153
|
+
project) type_color="$GREEN" ;;
|
|
154
|
+
reference) type_color="$BLUE" ;;
|
|
155
|
+
esac
|
|
156
|
+
|
|
157
|
+
echo -e " ${BOLD}${name}${RESET} ${type_color}[${mtype}]${RESET}"
|
|
158
|
+
[ -n "$desc" ] && echo -e " ${DIM}${desc}${RESET}"
|
|
159
|
+
local snippet
|
|
160
|
+
snippet=$(printf '%s' "$content" | head -c 200)
|
|
161
|
+
[ -n "$snippet" ] && echo -e " ${snippet}"
|
|
162
|
+
echo -e " ${DIM}updated: ${updated}${RESET}"
|
|
163
|
+
echo ""
|
|
164
|
+
done <<< "$result"
|
|
165
|
+
|
|
166
|
+
eagle_dim "$count results"
|
|
167
|
+
echo ""
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
memories_show() {
|
|
171
|
+
if [ -z "$query" ]; then
|
|
172
|
+
eagle_err "Usage: eagle-mem memories show <file_path>"
|
|
173
|
+
exit 1
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
local meta
|
|
177
|
+
meta=$(eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at, origin_session_id
|
|
178
|
+
FROM claude_memories WHERE file_path = '$(eagle_sql_escape "$query")';")
|
|
179
|
+
|
|
180
|
+
if [ -z "$meta" ]; then
|
|
181
|
+
eagle_err "Memory not found: $query"
|
|
182
|
+
exit 1
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
IFS='|' read -r name mtype desc fp updated origin <<< "$meta"
|
|
186
|
+
|
|
187
|
+
eagle_header "Memory Detail"
|
|
188
|
+
|
|
189
|
+
eagle_kv "Name:" "$name"
|
|
190
|
+
eagle_kv "Type:" "$mtype"
|
|
191
|
+
eagle_kv "File:" "$fp"
|
|
192
|
+
eagle_kv "Updated:" "$updated"
|
|
193
|
+
[ -n "$origin" ] && eagle_kv "Session:" "$origin"
|
|
194
|
+
echo ""
|
|
195
|
+
|
|
196
|
+
[ -n "$desc" ] && echo -e " ${BOLD}Description:${RESET} $desc"
|
|
197
|
+
echo ""
|
|
198
|
+
|
|
199
|
+
if [ -f "$fp" ]; then
|
|
200
|
+
echo -e " ${BOLD}Content:${RESET}"
|
|
201
|
+
awk '/^---$/{c++; next} c>=2' "$fp" | while IFS= read -r line; do
|
|
202
|
+
echo " $line"
|
|
203
|
+
done
|
|
204
|
+
else
|
|
205
|
+
eagle_dim "Source file no longer exists on disk."
|
|
206
|
+
fi
|
|
207
|
+
echo ""
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
plans_list() {
|
|
211
|
+
eagle_header "Plans"
|
|
212
|
+
|
|
213
|
+
local result
|
|
214
|
+
result=$(eagle_list_claude_plans "$project" "$limit")
|
|
215
|
+
|
|
216
|
+
if [ -z "$result" ]; then
|
|
217
|
+
eagle_dim "No captured plans found."
|
|
218
|
+
echo ""
|
|
219
|
+
eagle_dim "Plans are captured when Claude Code writes to ~/.claude/plans/"
|
|
220
|
+
eagle_dim "Run 'eagle-mem memories sync' to backfill existing plans."
|
|
221
|
+
echo ""
|
|
222
|
+
return
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
local count=0
|
|
226
|
+
while IFS='|' read -r title proj _fp updated; do
|
|
227
|
+
[ -z "$title" ] && continue
|
|
228
|
+
count=$((count + 1))
|
|
229
|
+
|
|
230
|
+
local proj_label=""
|
|
231
|
+
[ -n "$proj" ] && proj_label=" ${DIM}[${proj}]${RESET}"
|
|
232
|
+
|
|
233
|
+
echo -e " ${BOLD}${title}${RESET}${proj_label}"
|
|
234
|
+
echo -e " ${DIM}updated: ${updated}${RESET}"
|
|
235
|
+
echo ""
|
|
236
|
+
done <<< "$result"
|
|
237
|
+
|
|
238
|
+
eagle_dim "$count plans shown"
|
|
239
|
+
echo ""
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
plans_search() {
|
|
243
|
+
if [ -z "$query" ]; then
|
|
244
|
+
eagle_err "Usage: eagle-mem memories plans search <query>"
|
|
245
|
+
exit 1
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
eagle_header "Plan Search"
|
|
249
|
+
eagle_info "Query: $query"
|
|
250
|
+
echo ""
|
|
251
|
+
|
|
252
|
+
local result
|
|
253
|
+
result=$(eagle_search_claude_plans "$query" "$project" "$limit")
|
|
254
|
+
|
|
255
|
+
if [ -z "$result" ]; then
|
|
256
|
+
eagle_dim "No plans matching '$query'"
|
|
257
|
+
echo ""
|
|
258
|
+
return
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
local count=0
|
|
262
|
+
while IFS='|' read -r title proj snippet _fp updated; do
|
|
263
|
+
[ -z "$title" ] && continue
|
|
264
|
+
count=$((count + 1))
|
|
265
|
+
|
|
266
|
+
local proj_label=""
|
|
267
|
+
[ -n "$proj" ] && proj_label=" ${DIM}[${proj}]${RESET}"
|
|
268
|
+
|
|
269
|
+
echo -e " ${BOLD}${title}${RESET}${proj_label}"
|
|
270
|
+
[ -n "$snippet" ] && echo -e " ${snippet}"
|
|
271
|
+
echo -e " ${DIM}updated: ${updated}${RESET}"
|
|
272
|
+
echo ""
|
|
273
|
+
done <<< "$result"
|
|
274
|
+
|
|
275
|
+
eagle_dim "$count results"
|
|
276
|
+
echo ""
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
plans_show() {
|
|
280
|
+
if [ -z "$query" ]; then
|
|
281
|
+
eagle_err "Usage: eagle-mem memories plans show <file_path>"
|
|
282
|
+
exit 1
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
local meta
|
|
286
|
+
meta=$(eagle_db "SELECT title, project, file_path, updated_at, origin_session_id
|
|
287
|
+
FROM claude_plans WHERE file_path = '$(eagle_sql_escape "$query")';")
|
|
288
|
+
|
|
289
|
+
if [ -z "$meta" ]; then
|
|
290
|
+
eagle_err "Plan not found: $query"
|
|
291
|
+
exit 1
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
IFS='|' read -r title proj fp updated origin <<< "$meta"
|
|
295
|
+
|
|
296
|
+
eagle_header "Plan Detail"
|
|
297
|
+
|
|
298
|
+
eagle_kv "Title:" "$title"
|
|
299
|
+
[ -n "$proj" ] && eagle_kv "Project:" "$proj"
|
|
300
|
+
eagle_kv "File:" "$fp"
|
|
301
|
+
eagle_kv "Updated:" "$updated"
|
|
302
|
+
[ -n "$origin" ] && eagle_kv "Session:" "$origin"
|
|
303
|
+
echo ""
|
|
304
|
+
|
|
305
|
+
if [ -f "$fp" ]; then
|
|
306
|
+
echo -e " ${BOLD}Content:${RESET}"
|
|
307
|
+
cat "$fp" | while IFS= read -r line; do
|
|
308
|
+
echo " $line"
|
|
309
|
+
done
|
|
310
|
+
else
|
|
311
|
+
eagle_dim "Source file no longer exists on disk."
|
|
312
|
+
fi
|
|
313
|
+
echo ""
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
tasks_list() {
|
|
317
|
+
eagle_header "Claude Code Tasks"
|
|
318
|
+
|
|
319
|
+
local result
|
|
320
|
+
result=$(eagle_list_claude_tasks "$project" "$limit")
|
|
321
|
+
|
|
322
|
+
if [ -z "$result" ]; then
|
|
323
|
+
eagle_dim "No captured tasks found."
|
|
324
|
+
echo ""
|
|
325
|
+
eagle_dim "Tasks are captured when Claude Code uses TaskCreate/TaskUpdate."
|
|
326
|
+
eagle_dim "Run 'eagle-mem memories sync' to backfill existing tasks."
|
|
327
|
+
echo ""
|
|
328
|
+
return
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
local count=0
|
|
332
|
+
while IFS='|' read -r subject status sid tid updated; do
|
|
333
|
+
[ -z "$subject" ] && continue
|
|
334
|
+
count=$((count + 1))
|
|
335
|
+
|
|
336
|
+
local status_color="$DIM"
|
|
337
|
+
case "$status" in
|
|
338
|
+
pending) status_color="$DIM" ;;
|
|
339
|
+
in_progress) status_color="$CYAN" ;;
|
|
340
|
+
completed) status_color="$GREEN" ;;
|
|
341
|
+
esac
|
|
342
|
+
|
|
343
|
+
echo -e " ${BOLD}${subject}${RESET} ${status_color}[${status}]${RESET}"
|
|
344
|
+
echo -e " ${DIM}session: ${sid:0:8}… task: #${tid} updated: ${updated}${RESET}"
|
|
345
|
+
echo ""
|
|
346
|
+
done <<< "$result"
|
|
347
|
+
|
|
348
|
+
eagle_dim "$count tasks shown"
|
|
349
|
+
echo ""
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
tasks_search() {
|
|
353
|
+
if [ -z "$query" ]; then
|
|
354
|
+
eagle_err "Usage: eagle-mem memories tasks search <query>"
|
|
355
|
+
exit 1
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
eagle_header "Task Search"
|
|
359
|
+
eagle_info "Query: $query"
|
|
360
|
+
echo ""
|
|
361
|
+
|
|
362
|
+
local result
|
|
363
|
+
result=$(eagle_search_claude_tasks "$query" "$project" "$limit")
|
|
364
|
+
|
|
365
|
+
if [ -z "$result" ]; then
|
|
366
|
+
eagle_dim "No tasks matching '$query'"
|
|
367
|
+
echo ""
|
|
368
|
+
return
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
local count=0
|
|
372
|
+
while IFS='|' read -r subject status desc sid tid updated; do
|
|
373
|
+
[ -z "$subject" ] && continue
|
|
374
|
+
count=$((count + 1))
|
|
375
|
+
|
|
376
|
+
local status_color="$DIM"
|
|
377
|
+
case "$status" in
|
|
378
|
+
pending) status_color="$DIM" ;;
|
|
379
|
+
in_progress) status_color="$CYAN" ;;
|
|
380
|
+
completed) status_color="$GREEN" ;;
|
|
381
|
+
esac
|
|
382
|
+
|
|
383
|
+
echo -e " ${BOLD}${subject}${RESET} ${status_color}[${status}]${RESET}"
|
|
384
|
+
[ -n "$desc" ] && echo -e " ${desc}"
|
|
385
|
+
echo -e " ${DIM}session: ${sid:0:8}… task: #${tid} updated: ${updated}${RESET}"
|
|
386
|
+
echo ""
|
|
387
|
+
done <<< "$result"
|
|
388
|
+
|
|
389
|
+
eagle_dim "$count results"
|
|
390
|
+
echo ""
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
tasks_show() {
|
|
394
|
+
if [ -z "$query" ]; then
|
|
395
|
+
eagle_err "Usage: eagle-mem memories tasks show <file_path>"
|
|
396
|
+
exit 1
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
local meta
|
|
400
|
+
meta=$(eagle_db "SELECT subject, status, description, active_form, source_session_id, source_task_id, file_path, updated_at
|
|
401
|
+
FROM claude_tasks WHERE file_path = '$(eagle_sql_escape "$query")';")
|
|
402
|
+
|
|
403
|
+
if [ -z "$meta" ]; then
|
|
404
|
+
eagle_err "Task not found: $query"
|
|
405
|
+
exit 1
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
IFS='|' read -r subject status desc af sid tid fp updated <<< "$meta"
|
|
409
|
+
|
|
410
|
+
eagle_header "Task Detail"
|
|
411
|
+
|
|
412
|
+
eagle_kv "Subject:" "$subject"
|
|
413
|
+
eagle_kv "Status:" "$status"
|
|
414
|
+
eagle_kv "Task ID:" "$tid"
|
|
415
|
+
eagle_kv "Session:" "$sid"
|
|
416
|
+
eagle_kv "File:" "$fp"
|
|
417
|
+
eagle_kv "Updated:" "$updated"
|
|
418
|
+
echo ""
|
|
419
|
+
|
|
420
|
+
[ -n "$desc" ] && echo -e " ${BOLD}Description:${RESET} $desc" && echo ""
|
|
421
|
+
[ -n "$af" ] && echo -e " ${BOLD}Active Form:${RESET} $af" && echo ""
|
|
422
|
+
|
|
423
|
+
if [ -f "$fp" ]; then
|
|
424
|
+
echo -e " ${BOLD}Raw JSON:${RESET}"
|
|
425
|
+
jq '.' "$fp" 2>/dev/null | while IFS= read -r line; do
|
|
426
|
+
echo " $line"
|
|
427
|
+
done
|
|
428
|
+
else
|
|
429
|
+
eagle_dim "Source file no longer exists on disk."
|
|
430
|
+
fi
|
|
431
|
+
echo ""
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
memories_sync() {
|
|
435
|
+
eagle_header "Memory, Plan & Task Sync"
|
|
436
|
+
|
|
437
|
+
# ─── Sync memories ───────────────────────────────────
|
|
438
|
+
eagle_info "Scanning for Claude Code auto-memory files..."
|
|
439
|
+
echo ""
|
|
440
|
+
|
|
441
|
+
local claude_mem_root="$HOME/.claude/projects"
|
|
442
|
+
local mem_synced=0
|
|
443
|
+
local mem_skipped=0
|
|
444
|
+
|
|
445
|
+
if [ -d "$claude_mem_root" ]; then
|
|
446
|
+
while IFS= read -r -d '' memfile; do
|
|
447
|
+
local base
|
|
448
|
+
base=$(basename "$memfile")
|
|
449
|
+
[ "$base" = "MEMORY.md" ] && continue
|
|
450
|
+
|
|
451
|
+
local proj_slug proj_name
|
|
452
|
+
proj_slug=$(echo "$memfile" | sed "s|$claude_mem_root/||" | cut -d'/' -f1)
|
|
453
|
+
proj_name=$(echo "$proj_slug" | rev | cut -d'-' -f1 | rev)
|
|
454
|
+
|
|
455
|
+
local existing_hash
|
|
456
|
+
existing_hash=$(eagle_db "SELECT content_hash FROM claude_memories WHERE file_path = '$(eagle_sql_escape "$memfile")';")
|
|
457
|
+
local new_hash
|
|
458
|
+
new_hash=$(shasum -a 256 "$memfile" | awk '{print $1}')
|
|
459
|
+
|
|
460
|
+
if [ "$existing_hash" = "$new_hash" ]; then
|
|
461
|
+
mem_skipped=$((mem_skipped + 1))
|
|
462
|
+
continue
|
|
463
|
+
fi
|
|
464
|
+
|
|
465
|
+
eagle_capture_claude_memory "$memfile" "" "$proj_name"
|
|
466
|
+
mem_synced=$((mem_synced + 1))
|
|
467
|
+
eagle_ok "Memory: $base → $proj_name"
|
|
468
|
+
done < <(find "$claude_mem_root" -path "*/memory/*.md" -print0 2>/dev/null)
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
eagle_kv "Memories:" "$mem_synced synced, $mem_skipped unchanged"
|
|
472
|
+
echo ""
|
|
473
|
+
|
|
474
|
+
# ─── Sync plans ──────────────────────────────────────
|
|
475
|
+
eagle_info "Scanning for Claude Code plan files..."
|
|
476
|
+
echo ""
|
|
477
|
+
|
|
478
|
+
local plans_dir="$HOME/.claude/plans"
|
|
479
|
+
local plan_synced=0
|
|
480
|
+
local plan_skipped=0
|
|
481
|
+
|
|
482
|
+
if [ -d "$plans_dir" ]; then
|
|
483
|
+
for planfile in "$plans_dir"/*.md; do
|
|
484
|
+
[ ! -f "$planfile" ] && continue
|
|
485
|
+
|
|
486
|
+
local existing_hash
|
|
487
|
+
existing_hash=$(eagle_db "SELECT content_hash FROM claude_plans WHERE file_path = '$(eagle_sql_escape "$planfile")';")
|
|
488
|
+
local new_hash
|
|
489
|
+
new_hash=$(shasum -a 256 "$planfile" | awk '{print $1}')
|
|
490
|
+
|
|
491
|
+
if [ "$existing_hash" = "$new_hash" ]; then
|
|
492
|
+
plan_skipped=$((plan_skipped + 1))
|
|
493
|
+
continue
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
eagle_capture_claude_plan "$planfile" "" ""
|
|
497
|
+
plan_synced=$((plan_synced + 1))
|
|
498
|
+
local ptitle
|
|
499
|
+
ptitle=$(awk '/^# /{print; exit}' "$planfile" | sed 's/^# //')
|
|
500
|
+
eagle_ok "Plan: $ptitle"
|
|
501
|
+
done
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
eagle_kv "Plans:" "$plan_synced synced, $plan_skipped unchanged"
|
|
505
|
+
echo ""
|
|
506
|
+
|
|
507
|
+
# ─── Sync tasks ──────────────────────────────────────
|
|
508
|
+
eagle_info "Scanning for Claude Code task files..."
|
|
509
|
+
echo ""
|
|
510
|
+
|
|
511
|
+
local tasks_dir="$HOME/.claude/tasks"
|
|
512
|
+
local task_synced=0
|
|
513
|
+
local task_skipped=0
|
|
514
|
+
|
|
515
|
+
if [ -d "$tasks_dir" ]; then
|
|
516
|
+
for session_dir in "$tasks_dir"/*/; do
|
|
517
|
+
[ ! -d "$session_dir" ] && continue
|
|
518
|
+
local sid
|
|
519
|
+
sid=$(basename "$session_dir")
|
|
520
|
+
|
|
521
|
+
local task_project=""
|
|
522
|
+
task_project=$(eagle_db "SELECT project FROM sessions WHERE id = '$(eagle_sql_escape "$sid")' LIMIT 1;")
|
|
523
|
+
|
|
524
|
+
for taskfile in "$session_dir"*.json; do
|
|
525
|
+
[ ! -f "$taskfile" ] && continue
|
|
526
|
+
|
|
527
|
+
local existing_hash
|
|
528
|
+
existing_hash=$(eagle_db "SELECT content_hash FROM claude_tasks WHERE file_path = '$(eagle_sql_escape "$taskfile")';")
|
|
529
|
+
local new_hash
|
|
530
|
+
new_hash=$(shasum -a 256 "$taskfile" | awk '{print $1}')
|
|
531
|
+
|
|
532
|
+
if [ "$existing_hash" = "$new_hash" ]; then
|
|
533
|
+
task_skipped=$((task_skipped + 1))
|
|
534
|
+
continue
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
eagle_capture_claude_task "$taskfile" "$sid" "$task_project"
|
|
538
|
+
task_synced=$((task_synced + 1))
|
|
539
|
+
done
|
|
540
|
+
done
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
eagle_kv "Tasks:" "$task_synced synced, $task_skipped unchanged"
|
|
544
|
+
eagle_footer "Sync complete."
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
# ─── Dispatch ────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
case "$action" in
|
|
550
|
+
list) memories_list ;;
|
|
551
|
+
search) memories_search ;;
|
|
552
|
+
show) memories_show ;;
|
|
553
|
+
plans)
|
|
554
|
+
case "$plan_action" in
|
|
555
|
+
list) plans_list ;;
|
|
556
|
+
search) plans_search ;;
|
|
557
|
+
show) plans_show ;;
|
|
558
|
+
*) plans_list ;;
|
|
559
|
+
esac
|
|
560
|
+
;;
|
|
561
|
+
tasks)
|
|
562
|
+
case "$task_action" in
|
|
563
|
+
list) tasks_list ;;
|
|
564
|
+
search) tasks_search ;;
|
|
565
|
+
show) tasks_show ;;
|
|
566
|
+
*) tasks_list ;;
|
|
567
|
+
esac
|
|
568
|
+
;;
|
|
569
|
+
sync) memories_sync ;;
|
|
570
|
+
*)
|
|
571
|
+
eagle_err "Unknown action: $action"
|
|
572
|
+
echo -e " ${DIM}Available: list, search, show, plans, tasks, sync${RESET}"
|
|
573
|
+
exit 1
|
|
574
|
+
;;
|
|
575
|
+
esac
|
package/scripts/update.sh
CHANGED
|
@@ -79,7 +79,12 @@ if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
|
|
|
79
79
|
|
|
80
80
|
patch_hook "SessionStart" "" "$EAGLE_MEM_DIR/hooks/session-start.sh"
|
|
81
81
|
patch_hook "Stop" "" "$EAGLE_MEM_DIR/hooks/stop.sh"
|
|
82
|
-
|
|
82
|
+
# Update PostToolUse matcher if it has the old value (pre-v1.3.0)
|
|
83
|
+
if jq -e '.hooks.PostToolUse[]? | select(.matcher == "Read|Write|Edit|Bash")' "$SETTINGS" &>/dev/null; then
|
|
84
|
+
_tmp=$(mktemp)
|
|
85
|
+
jq '(.hooks.PostToolUse[] | select(.matcher == "Read|Write|Edit|Bash")).matcher = "Read|Write|Edit|Bash|TaskCreate|TaskUpdate"' "$SETTINGS" > "$_tmp" && mv "$_tmp" "$SETTINGS"
|
|
86
|
+
fi
|
|
87
|
+
patch_hook "PostToolUse" "Read|Write|Edit|Bash|TaskCreate|TaskUpdate" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
|
|
83
88
|
patch_hook "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh"
|
|
84
89
|
patch_hook "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
|
|
85
90
|
|