eagle-mem 1.2.0 → 1.3.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
@@ -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"
@@ -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 file-related tools
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/common.sh CHANGED
@@ -18,7 +18,13 @@ eagle_log() {
18
18
 
19
19
  eagle_project_from_cwd() {
20
20
  local cwd="${1:-$(pwd)}"
21
- basename "$cwd"
21
+ local git_root
22
+ git_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null)
23
+ if [ -n "$git_root" ]; then
24
+ basename "$git_root"
25
+ else
26
+ basename "$cwd"
27
+ fi
22
28
  }
23
29
 
24
30
  eagle_sql_escape() {
package/lib/db.sh CHANGED
@@ -240,6 +240,324 @@ 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
+
508
+ eagle_build_session_project_map() {
509
+ local claude_projects_dir="$HOME/.claude/projects"
510
+ [ ! -d "$claude_projects_dir" ] && return 0
511
+
512
+ for proj_dir in "$claude_projects_dir"/*/; do
513
+ [ ! -d "$proj_dir" ] && continue
514
+
515
+ local project=""
516
+ local sample_jsonl
517
+ sample_jsonl=$(ls "$proj_dir"*.jsonl 2>/dev/null | head -1)
518
+ if [ -n "$sample_jsonl" ] && [ -f "$sample_jsonl" ]; then
519
+ local cwd
520
+ cwd=$(head -10 "$sample_jsonl" | jq -r 'select(.cwd != null) | .cwd' 2>/dev/null | head -1)
521
+ if [ -n "$cwd" ]; then
522
+ project=$(eagle_project_from_cwd "$cwd")
523
+ fi
524
+ fi
525
+ [ -z "$project" ] && continue
526
+
527
+ for jsonl in "$proj_dir"*.jsonl; do
528
+ [ ! -f "$jsonl" ] && continue
529
+ local sid
530
+ sid=$(basename "$jsonl" .jsonl)
531
+ echo "$sid|$project"
532
+ done
533
+ done
534
+ }
535
+
536
+ eagle_backfill_projects() {
537
+ local updated=0
538
+ local map
539
+ map=$(eagle_build_session_project_map)
540
+ [ -z "$map" ] && echo "0" && return 0
541
+
542
+ while IFS='|' read -r sid project; do
543
+ [ -z "$sid" ] || [ -z "$project" ] && continue
544
+ local sid_sql proj_sql
545
+ sid_sql=$(eagle_sql_escape "$sid")
546
+ proj_sql=$(eagle_sql_escape "$project")
547
+
548
+ local changed
549
+ changed=$(eagle_db "UPDATE sessions SET project = '$proj_sql' WHERE id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
550
+ SELECT changes();")
551
+ changed=$(eagle_db "UPDATE claude_tasks SET project = '$proj_sql' WHERE source_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
552
+ SELECT changes();")
553
+ [ "${changed:-0}" -gt 0 ] && updated=$((updated + changed))
554
+ eagle_db "UPDATE claude_memories SET project = '$proj_sql' WHERE origin_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');"
555
+ eagle_db "UPDATE claude_plans SET project = '$proj_sql' WHERE origin_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');"
556
+ done <<< "$map"
557
+
558
+ echo "$updated"
559
+ }
560
+
243
561
  eagle_prune_orphan_chunks() {
244
562
  local project; project=$(eagle_sql_escape "$1")
245
563
  local target_dir="$2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "1.2.0",
3
+ "version": "1.3.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
@@ -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}"
@@ -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,584 @@
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 existing_hash
452
+ existing_hash=$(eagle_db "SELECT content_hash FROM claude_memories WHERE file_path = '$(eagle_sql_escape "$memfile")';")
453
+ local new_hash
454
+ new_hash=$(shasum -a 256 "$memfile" | awk '{print $1}')
455
+
456
+ if [ "$existing_hash" = "$new_hash" ]; then
457
+ mem_skipped=$((mem_skipped + 1))
458
+ continue
459
+ fi
460
+
461
+ eagle_capture_claude_memory "$memfile" "" ""
462
+ mem_synced=$((mem_synced + 1))
463
+ eagle_ok "Memory: $base"
464
+ done < <(find "$claude_mem_root" -path "*/memory/*.md" -print0 2>/dev/null)
465
+ fi
466
+
467
+ eagle_kv "Memories:" "$mem_synced synced, $mem_skipped unchanged"
468
+ echo ""
469
+
470
+ # ─── Sync plans ──────────────────────────────────────
471
+ eagle_info "Scanning for Claude Code plan files..."
472
+ echo ""
473
+
474
+ local plans_dir="$HOME/.claude/plans"
475
+ local plan_synced=0
476
+ local plan_skipped=0
477
+
478
+ if [ -d "$plans_dir" ]; then
479
+ for planfile in "$plans_dir"/*.md; do
480
+ [ ! -f "$planfile" ] && continue
481
+
482
+ local existing_hash
483
+ existing_hash=$(eagle_db "SELECT content_hash FROM claude_plans WHERE file_path = '$(eagle_sql_escape "$planfile")';")
484
+ local new_hash
485
+ new_hash=$(shasum -a 256 "$planfile" | awk '{print $1}')
486
+
487
+ if [ "$existing_hash" = "$new_hash" ]; then
488
+ plan_skipped=$((plan_skipped + 1))
489
+ continue
490
+ fi
491
+
492
+ eagle_capture_claude_plan "$planfile" "" ""
493
+ plan_synced=$((plan_synced + 1))
494
+ local ptitle
495
+ ptitle=$(awk '/^# /{print; exit}' "$planfile" | sed 's/^# //')
496
+ eagle_ok "Plan: $ptitle"
497
+ done
498
+ fi
499
+
500
+ eagle_kv "Plans:" "$plan_synced synced, $plan_skipped unchanged"
501
+ echo ""
502
+
503
+ # ─── Sync tasks ──────────────────────────────────────
504
+ eagle_info "Scanning for Claude Code task files..."
505
+ echo ""
506
+
507
+ local tasks_dir="$HOME/.claude/tasks"
508
+ local task_synced=0
509
+ local task_skipped=0
510
+
511
+ if [ -d "$tasks_dir" ]; then
512
+ for session_dir in "$tasks_dir"/*/; do
513
+ [ ! -d "$session_dir" ] && continue
514
+ local sid
515
+ sid=$(basename "$session_dir")
516
+
517
+ local task_project=""
518
+ task_project=$(eagle_db "SELECT project FROM sessions WHERE id = '$(eagle_sql_escape "$sid")' LIMIT 1;")
519
+
520
+ for taskfile in "$session_dir"*.json; do
521
+ [ ! -f "$taskfile" ] && continue
522
+
523
+ local existing_hash
524
+ existing_hash=$(eagle_db "SELECT content_hash FROM claude_tasks WHERE file_path = '$(eagle_sql_escape "$taskfile")';")
525
+ local new_hash
526
+ new_hash=$(shasum -a 256 "$taskfile" | awk '{print $1}')
527
+
528
+ if [ "$existing_hash" = "$new_hash" ]; then
529
+ task_skipped=$((task_skipped + 1))
530
+ continue
531
+ fi
532
+
533
+ eagle_capture_claude_task "$taskfile" "$sid" "$task_project"
534
+ task_synced=$((task_synced + 1))
535
+ done
536
+ done
537
+ fi
538
+
539
+ eagle_kv "Tasks:" "$task_synced synced, $task_skipped unchanged"
540
+ echo ""
541
+
542
+ # ─── Backfill project names ──────────────────────────
543
+ eagle_info "Resolving project names from Claude Code transcripts..."
544
+
545
+ local backfilled
546
+ backfilled=$(eagle_backfill_projects)
547
+ if [ "${backfilled:-0}" -gt 0 ]; then
548
+ eagle_ok "$backfilled rows updated with correct project names"
549
+ else
550
+ eagle_ok "All project names up to date"
551
+ fi
552
+
553
+ eagle_footer "Sync complete."
554
+ }
555
+
556
+ # ─── Dispatch ────────────────────────────────────────────
557
+
558
+ case "$action" in
559
+ list) memories_list ;;
560
+ search) memories_search ;;
561
+ show) memories_show ;;
562
+ plans)
563
+ case "$plan_action" in
564
+ list) plans_list ;;
565
+ search) plans_search ;;
566
+ show) plans_show ;;
567
+ *) plans_list ;;
568
+ esac
569
+ ;;
570
+ tasks)
571
+ case "$task_action" in
572
+ list) tasks_list ;;
573
+ search) tasks_search ;;
574
+ show) tasks_show ;;
575
+ *) tasks_list ;;
576
+ esac
577
+ ;;
578
+ sync) memories_sync ;;
579
+ *)
580
+ eagle_err "Unknown action: $action"
581
+ echo -e " ${DIM}Available: list, search, show, plans, tasks, sync${RESET}"
582
+ exit 1
583
+ ;;
584
+ esac
package/scripts/update.sh CHANGED
@@ -11,6 +11,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
11
11
 
12
12
  . "$SCRIPTS_DIR/style.sh"
13
13
  . "$LIB_DIR/common.sh"
14
+ . "$LIB_DIR/db.sh"
14
15
 
15
16
  SETTINGS="$EAGLE_SETTINGS"
16
17
 
@@ -79,7 +80,12 @@ if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
79
80
 
80
81
  patch_hook "SessionStart" "" "$EAGLE_MEM_DIR/hooks/session-start.sh"
81
82
  patch_hook "Stop" "" "$EAGLE_MEM_DIR/hooks/stop.sh"
82
- patch_hook "PostToolUse" "Read|Write|Edit|Bash" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
83
+ # Update PostToolUse matcher if it has the old value (pre-v1.3.0)
84
+ if jq -e '.hooks.PostToolUse[]? | select(.matcher == "Read|Write|Edit|Bash")' "$SETTINGS" &>/dev/null; then
85
+ _tmp=$(mktemp)
86
+ jq '(.hooks.PostToolUse[] | select(.matcher == "Read|Write|Edit|Bash")).matcher = "Read|Write|Edit|Bash|TaskCreate|TaskUpdate"' "$SETTINGS" > "$_tmp" && mv "$_tmp" "$SETTINGS"
87
+ fi
88
+ patch_hook "PostToolUse" "Read|Write|Edit|Bash|TaskCreate|TaskUpdate" "$EAGLE_MEM_DIR/hooks/post-tool-use.sh"
83
89
  patch_hook "SessionEnd" "" "$EAGLE_MEM_DIR/hooks/session-end.sh"
84
90
  patch_hook "UserPromptSubmit" "" "$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh"
85
91
 
@@ -100,6 +106,15 @@ if [ -d "$PACKAGE_DIR/skills" ]; then
100
106
  eagle_ok "Skills updated"
101
107
  fi
102
108
 
109
+ # ─── Backfill project names ───────────────────────────────
110
+
111
+ backfilled=$(eagle_backfill_projects 2>/dev/null)
112
+ if [ "${backfilled:-0}" -gt 0 ]; then
113
+ eagle_ok "Project names: $backfilled rows corrected"
114
+ else
115
+ eagle_ok "Project names up to date"
116
+ fi
117
+
103
118
  # ─── Summary ───────────────────────────────────────────────
104
119
 
105
120
  version=$(node -e "console.log(require('$PACKAGE_DIR/package.json').version)" 2>/dev/null || echo "unknown")