eagle-mem 4.10.3 → 4.10.4

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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to the **Eagle Mem** project are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## v4.10.4 Minor Release
8
+
9
+ This release introduces native relational **Knowledge Graph Memories** and an automated background **Dream Cycle** curator to consolidate multi-agent developer context:
10
+
11
+ - **Graph-based Memories**: Full integration of custom semantic code graph database primitives (`lib/db-graph.sh` and migrations `db/035_graph_memories.sql` and `db/036_graph_constraints.sql`) to link files, functions, variables, sessions, and memories.
12
+ - **Background Dream Cycle Curation**: Structured offline compilation in `scripts/curate.sh` that merges redundant, overlapping memories into clean `--- Compiled Truth ---` with an underlying `--- Evidence Trail ---`.
13
+ - **Database Firewalls**: Enforced strict enums check triggers on `node_type` and `edge_type` to guarantee complete data integrity.
14
+ - **Sanitized FTS Wildcards**: Safe search query sanitization in `eagle_graph_search` to prevent SQLite MATCH syntax errors on special characters.
15
+ - **Unified Global Porting**: Symlinked advanced nested specialist developer skills and HTTP Model Context Protocol (MCP) gateways natively into the Google Antigravity active configuration.
16
+
17
+ ---
18
+
7
19
  ## v4.10.2 Patch
8
20
 
9
21
  This release expands the Eagle Mem adapter layer and addresses multi-agent planning synchronization and gap remediations across all four supported agents:
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  Eagle Mem turns AI coding sessions into compounding project knowledge. It gives Claude Code, Codex, and Google Antigravity hook-backed shared memory, gives Grok the same skills and CLI memory surface, labels which agent created each memory, blocks risky release commands until affected features are verified, and lets broad work split into durable worker lanes.
13
13
 
14
- **v4.10.2 and onward focuses on Grok, Google Antigravity support, and Compaction Survival:** Grok users get first-class skill linking and `eagle-mem grok-bootstrap`, while Antigravity users get native Python SDK hook integration via `google_antigravity_hook.py`. Claude Code, Codex, and Antigravity receive the deepest automatic lifecycle support through hooks; Grok currently uses the shared CLI and skill workflow.
14
+ **v4.10.4 and onward focuses on Graph Memory, Dream Cycle Curation, Grok, Google Antigravity support, and Compaction Survival:** Grok users get first-class skill linking and `eagle-mem grok-bootstrap`, while Antigravity users get native Python SDK hook integration via `google_antigravity_hook.py`. Claude Code, Codex, and Antigravity receive the deepest automatic lifecycle support through hooks; Grok currently uses the shared CLI and skill workflow.
15
15
 
16
16
  **Website:** [Product](https://eagleisbatman.github.io/eagle-mem/) |
17
17
  [Architecture](https://eagleisbatman.github.io/eagle-mem/architecture.html) |
package/bin/eagle-mem CHANGED
@@ -27,6 +27,7 @@ case "$command" in
27
27
  statusline) "$SCRIPTS_DIR/statusline-em.sh" "$@" ;;
28
28
  guard) bash "$SCRIPTS_DIR/guard.sh" "$@" ;;
29
29
  overview) bash "$SCRIPTS_DIR/overview.sh" "$@" ;;
30
+ graph) bash "$SCRIPTS_DIR/memories.sh" graph "$@" ;;
30
31
  session|sessions)
31
32
  bash "$SCRIPTS_DIR/session.sh" "$@" ;;
32
33
  memories) bash "$SCRIPTS_DIR/memories.sh" "$@" ;;
@@ -0,0 +1,65 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Migration 035: Graph-based memories (nodes and edges relational tables)
3
+ -- ═══════════════════════════════════════════════════════════
4
+
5
+ -- ─── Graph Nodes ───────────────────────────────────────────
6
+ -- Represents semantic entities in the codebase (files, features, memories, tasks, sessions, tags, etc.)
7
+ CREATE TABLE IF NOT EXISTS graph_nodes (
8
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9
+ project TEXT NOT NULL,
10
+ node_type TEXT NOT NULL, -- 'file', 'feature', 'memory', 'task', 'session', 'tag', etc.
11
+ node_name TEXT NOT NULL, -- e.g., 'lib/db-graph.sh', 'auth-middleware', 'agy-session-123'
12
+ node_value TEXT, -- Optional content, JSON payload, description, etc.
13
+ source_path TEXT, -- Optional filepath or link
14
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
15
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
16
+ UNIQUE(project, node_type, node_name)
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_graph_nodes_project ON graph_nodes(project);
20
+ CREATE INDEX IF NOT EXISTS idx_graph_nodes_type_name ON graph_nodes(node_type, node_name);
21
+
22
+ -- ─── Graph Edges ───────────────────────────────────────────
23
+ -- Represents relationships between nodes with optional weights
24
+ CREATE TABLE IF NOT EXISTS graph_edges (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ project TEXT NOT NULL,
27
+ source_node_id INTEGER NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
28
+ target_node_id INTEGER NOT NULL REFERENCES graph_nodes(id) ON DELETE CASCADE,
29
+ edge_type TEXT NOT NULL, -- 'imports', 'declares', 'references', 'verifies', 'contains', 'supersedes', 'co_edited', 'modified'
30
+ weight REAL NOT NULL DEFAULT 1.0,
31
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
32
+ UNIQUE(project, source_node_id, target_node_id, edge_type)
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_project ON graph_edges(project);
36
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_source ON graph_edges(source_node_id);
37
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_target ON graph_edges(target_node_id);
38
+
39
+ -- ─── FTS5: Full-text search on graph nodes ──────────────────
40
+ CREATE VIRTUAL TABLE IF NOT EXISTS graph_nodes_fts USING fts5(
41
+ node_name,
42
+ node_value,
43
+ content='graph_nodes',
44
+ content_rowid='id'
45
+ );
46
+
47
+ -- Triggers to keep FTS in sync
48
+ CREATE TRIGGER IF NOT EXISTS graph_nodes_ai AFTER INSERT ON graph_nodes BEGIN
49
+ INSERT INTO graph_nodes_fts(rowid, node_name, node_value)
50
+ VALUES (new.id, new.node_name, new.node_value);
51
+ END;
52
+
53
+ CREATE TRIGGER IF NOT EXISTS graph_nodes_ad AFTER DELETE ON graph_nodes BEGIN
54
+ INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, node_name, node_value)
55
+ VALUES ('delete', old.id, old.node_name, old.node_value);
56
+ END;
57
+
58
+ CREATE TRIGGER IF NOT EXISTS graph_nodes_au
59
+ AFTER UPDATE OF node_name, node_value ON graph_nodes
60
+ BEGIN
61
+ INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, node_name, node_value)
62
+ VALUES ('delete', old.id, old.node_name, old.node_value);
63
+ INSERT INTO graph_nodes_fts(rowid, node_name, node_value)
64
+ VALUES (new.id, new.node_name, new.node_value);
65
+ END;
@@ -0,0 +1,37 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- Migration 036: Database-level enums constraints triggers
3
+ -- ═══════════════════════════════════════════════════════════
4
+
5
+ -- ─── Node Type Constraints ──────────────────────────────────
6
+ CREATE TRIGGER IF NOT EXISTS val_graph_nodes_insert BEFORE INSERT ON graph_nodes
7
+ BEGIN
8
+ SELECT CASE
9
+ WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'def')
10
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, def')
11
+ END;
12
+ END;
13
+
14
+ CREATE TRIGGER IF NOT EXISTS val_graph_nodes_update BEFORE UPDATE OF node_type ON graph_nodes
15
+ BEGIN
16
+ SELECT CASE
17
+ WHEN NEW.node_type NOT IN ('project', 'file', 'feature', 'memory', 'task', 'session', 'tag', 'class', 'def')
18
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid node type. Must be one of project, file, feature, memory, task, session, tag, class, def')
19
+ END;
20
+ END;
21
+
22
+ -- ─── Edge Type Constraints ──────────────────────────────────
23
+ CREATE TRIGGER IF NOT EXISTS val_graph_edges_insert BEFORE INSERT ON graph_edges
24
+ BEGIN
25
+ SELECT CASE
26
+ WHEN NEW.edge_type NOT IN ('imports', 'declares', 'references', 'verifies', 'contains', 'supersedes', 'co_edited', 'modified', 'read')
27
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid edge type. Must be one of imports, declares, references, verifies, contains, supersedes, co_edited, modified, read')
28
+ END;
29
+ END;
30
+
31
+ CREATE TRIGGER IF NOT EXISTS val_graph_edges_update BEFORE UPDATE OF edge_type ON graph_edges
32
+ BEGIN
33
+ SELECT CASE
34
+ WHEN NEW.edge_type NOT IN ('imports', 'declares', 'references', 'verifies', 'contains', 'supersedes', 'co_edited', 'modified', 'read')
35
+ THEN RAISE(ABORT, 'Data Integrity Error: Invalid edge type. Must be one of imports, declares, references, verifies, contains, supersedes, co_edited, modified, read')
36
+ END;
37
+ END;
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════
3
+ # Eagle Mem — Graph memories helpers
4
+ # Primitives for self-wiring codebase graph and relations
5
+ # ═══════════════════════════════════════════════════════════
6
+ [ -n "${_EAGLE_DB_GRAPH_LOADED:-}" ] && return 0
7
+ _EAGLE_DB_GRAPH_LOADED=1
8
+
9
+ eagle_graph_add_node() {
10
+ local project; project=$(eagle_sql_escape "$1")
11
+ local node_type; node_type=$(eagle_sql_escape "$2")
12
+ local node_name; node_name=$(eagle_sql_escape "$3")
13
+ local node_value; node_value=$(eagle_sql_escape "${4:-}")
14
+ local source_path; source_path=$(eagle_sql_escape "${5:-}")
15
+
16
+ eagle_db "INSERT INTO graph_nodes (project, node_type, node_name, node_value, source_path)
17
+ VALUES ('$project', '$node_type', '$node_name', '$node_value', '$source_path')
18
+ ON CONFLICT(project, node_type, node_name) DO UPDATE SET
19
+ node_value = CASE WHEN excluded.node_value != '' THEN excluded.node_value ELSE node_value END,
20
+ source_path = CASE WHEN excluded.source_path != '' THEN excluded.source_path ELSE source_path END,
21
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
22
+ }
23
+
24
+ eagle_graph_get_node_id() {
25
+ local project; project=$(eagle_sql_escape "$1")
26
+ local node_type; node_type=$(eagle_sql_escape "$2")
27
+ local node_name; node_name=$(eagle_sql_escape "$3")
28
+
29
+ eagle_db "SELECT id FROM graph_nodes
30
+ WHERE project = '$project'
31
+ AND node_type = '$node_type'
32
+ AND node_name = '$node_name'
33
+ LIMIT 1;"
34
+ }
35
+
36
+ eagle_graph_add_edge() {
37
+ local project; project=$(eagle_sql_escape "$1")
38
+ local source_id; source_id=$(eagle_sql_int "$2")
39
+ local target_id; target_id=$(eagle_sql_int "$3")
40
+ local edge_type; edge_type=$(eagle_sql_escape "$4")
41
+ local weight="${5:-1.0}"
42
+ if ! [[ "$weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
43
+ weight="1.0"
44
+ fi
45
+
46
+ [ -z "$source_id" ] || [ -z "$target_id" ] && return 1
47
+ [ "$source_id" = "$target_id" ] && return 0 # avoid self-loops
48
+
49
+ eagle_db "INSERT INTO graph_edges (project, source_node_id, target_node_id, edge_type, weight)
50
+ VALUES ('$project', $source_id, $target_id, '$edge_type', $weight)
51
+ ON CONFLICT(project, source_node_id, target_node_id, edge_type) DO UPDATE SET
52
+ weight = weight + excluded.weight;"
53
+ }
54
+
55
+ eagle_graph_query_neighbors() {
56
+ local node_id; node_id=$(eagle_sql_int "$1")
57
+ local direction="${2:-out}" # 'out', 'in', or 'both'
58
+
59
+ if [ "$direction" = "out" ]; then
60
+ eagle_db "SELECT e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
61
+ FROM graph_edges e
62
+ JOIN graph_nodes n ON e.target_node_id = n.id
63
+ WHERE e.source_node_id = $node_id
64
+ ORDER BY e.weight DESC, n.node_name;"
65
+ elif [ "$direction" = "in" ]; then
66
+ eagle_db "SELECT e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
67
+ FROM graph_edges e
68
+ JOIN graph_nodes n ON e.source_node_id = n.id
69
+ WHERE e.target_node_id = $node_id
70
+ ORDER BY e.weight DESC, n.node_name;"
71
+ else
72
+ eagle_db "SELECT 'out' as dir, e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
73
+ FROM graph_edges e
74
+ JOIN graph_nodes n ON e.target_node_id = n.id
75
+ WHERE e.source_node_id = $node_id
76
+ UNION ALL
77
+ SELECT 'in' as dir, e.edge_type, e.weight, n.id, n.node_type, n.node_name, n.node_value
78
+ FROM graph_edges e
79
+ JOIN graph_nodes n ON e.source_node_id = n.id
80
+ WHERE e.target_node_id = $node_id
81
+ ORDER BY weight DESC;"
82
+ fi
83
+ }
84
+
85
+ eagle_graph_search() {
86
+ local project; project=$(eagle_sql_escape "$1")
87
+ local query; query=$(eagle_fts_sanitize "$2")
88
+ local type_filter="${3:-}"
89
+
90
+ local sql
91
+ if [ -n "$type_filter" ]; then
92
+ local t_esc; t_esc=$(eagle_sql_escape "$type_filter")
93
+ sql="SELECT n.id, n.node_type, n.node_name, n.node_value, n.source_path
94
+ FROM graph_nodes n
95
+ JOIN graph_nodes_fts f ON f.rowid = n.id
96
+ WHERE n.project = '$project'
97
+ AND n.node_type = '$t_esc'
98
+ AND graph_nodes_fts MATCH '$query'
99
+ ORDER BY rank;"
100
+ else
101
+ sql="SELECT n.id, n.node_type, n.node_name, n.node_value, n.source_path
102
+ FROM graph_nodes n
103
+ JOIN graph_nodes_fts f ON f.rowid = n.id
104
+ WHERE n.project = '$project'
105
+ AND graph_nodes_fts MATCH '$query'
106
+ ORDER BY rank;"
107
+ fi
108
+
109
+ eagle_db "$sql"
110
+ }
111
+
112
+ eagle_graph_get_node_by_id() {
113
+ local node_id; node_id=$(eagle_sql_int "$1")
114
+ eagle_db "SELECT id, node_type, node_name, node_value, source_path, created_at, updated_at
115
+ FROM graph_nodes
116
+ WHERE id = $node_id
117
+ LIMIT 1;"
118
+ }
119
+
120
+ eagle_graph_delete_node() {
121
+ local node_id; node_id=$(eagle_sql_int "$1")
122
+ eagle_db "DELETE FROM graph_nodes WHERE id = $node_id;"
123
+ }
124
+
125
+ eagle_graph_prune_orphans() {
126
+ local project; project=$(eagle_sql_escape "$1")
127
+ # Delete file nodes that no longer exist on disk
128
+ local result
129
+ result=$(eagle_db "SELECT id, node_name FROM graph_nodes WHERE project = '$project' AND node_type = 'file';")
130
+ [ -z "$result" ] && return 0
131
+
132
+ while IFS='|' read -r nid nname; do
133
+ [ -z "$nid" ] && continue
134
+ if [ ! -f "$nname" ]; then
135
+ eagle_graph_delete_node "$nid"
136
+ fi
137
+ done <<< "$result"
138
+ }
package/lib/db.sh CHANGED
@@ -17,3 +17,4 @@ _eagle_db_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
17
  . "$_eagle_db_dir/db-hints.sh"
18
18
  . "$_eagle_db_dir/db-backfill.sh"
19
19
  . "$_eagle_db_dir/db-guardrails.sh"
20
+ . "$_eagle_db_dir/db-graph.sh"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "4.10.3",
3
+ "version": "4.10.4",
4
4
  "description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, Grok, and Google Antigravity",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/curate.sh CHANGED
@@ -521,7 +521,138 @@ else
521
521
  eagle_dim " Not enough session data for hot file detection (need 3+ sessions, have ${total_sessions:-0})"
522
522
  fi
523
523
 
524
- # ─── 7. Session compression (--full only) ─────────────────
524
+ # ─── 7. Knowledge Graph Wiring & Dream Cycle (Consolidation) ───
525
+
526
+ eagle_info "Executing Dream Cycle (Knowledge Graph & Memory Consolidation)..."
527
+
528
+ # 7.1 Wire co-edit edges in the graph
529
+ if [ -n "$co_edit_data" ]; then
530
+ co_wire_count=0
531
+ while IFS='|' read -r f1 f2 co_sessions; do
532
+ [ -z "$f1" ] || [ -z "$f2" ] && continue
533
+ f1_id=$(eagle_graph_get_node_id "$project" "file" "$f1")
534
+ f2_id=$(eagle_graph_get_node_id "$project" "file" "$f2")
535
+ if [ -n "$f1_id" ] && [ -n "$f2_id" ]; then
536
+ if [ "$DRY_RUN" -eq 0 ]; then
537
+ eagle_graph_add_edge "$project" "$f1_id" "$f2_id" "co_edited" "$co_sessions"
538
+ fi
539
+ co_wire_count=$((co_wire_count + 1))
540
+ fi
541
+ done <<< "$co_edit_data"
542
+ eagle_ok "Wired $co_wire_count co-edited file edges"
543
+ fi
544
+
545
+ # 7.2 Wire session nodes and access edges
546
+ recent_sessions=$(eagle_db "SELECT id, started_at, model FROM sessions WHERE project = '$p_esc' ORDER BY started_at DESC LIMIT 15;")
547
+ if [ -n "$recent_sessions" ]; then
548
+ session_wire_count=0
549
+ while IFS='|' read -r sid sstart smodel; do
550
+ [ -z "$sid" ] && continue
551
+ if [ "$DRY_RUN" -eq 0 ]; then
552
+ eagle_graph_add_node "$project" "session" "$sid" "Session run on $sstart using $smodel" ""
553
+ sid_node=$(eagle_graph_get_node_id "$project" "session" "$sid")
554
+ if [ -n "$sid_node" ]; then
555
+ # Find read/modified files in this session from observations
556
+ session_files=$(eagle_db "SELECT files_read, files_modified FROM observations WHERE session_id = '$(eagle_sql_escape "$sid")';")
557
+ if [ -n "$session_files" ]; then
558
+ while IFS='|' read -r f_read f_mod; do
559
+ # Parse files_read JSON list
560
+ if [ -n "$f_read" ] && [ "$f_read" != "[]" ]; then
561
+ echo "$f_read" | grep -oE '"[^"]+"' | tr -d '"' | while read -r rf; do
562
+ [ -z "$rf" ] && continue
563
+ rfid=$(eagle_graph_get_node_id "$project" "file" "$rf")
564
+ [ -n "$rfid" ] && eagle_graph_add_edge "$project" "$sid_node" "$rfid" "read" 1.0
565
+ done
566
+ fi
567
+ # Parse files_modified JSON list
568
+ if [ -n "$f_mod" ] && [ "$f_mod" != "[]" ]; then
569
+ echo "$f_mod" | grep -oE '"[^"]+"' | tr -d '"' | while read -r mf; do
570
+ [ -z "$mf" ] && continue
571
+ mfid=$(eagle_graph_get_node_id "$project" "file" "$mf")
572
+ [ -n "$mfid" ] && eagle_graph_add_edge "$project" "$sid_node" "$mfid" "modified" 2.0
573
+ done
574
+ fi
575
+ done <<< "$session_files"
576
+ fi
577
+ fi
578
+ fi
579
+ session_wire_count=$((session_wire_count + 1))
580
+ done <<< "$recent_sessions"
581
+ eagle_ok "Wired $session_wire_count recent session nodes and edges"
582
+ fi
583
+
584
+ # 7.3 Offline Memory Consolidation (Compiled Truth vs Evidence)
585
+ active_memories=$(eagle_db "SELECT memory_name, memory_type, description, content FROM agent_memories WHERE project = '$p_esc';")
586
+ if [ -n "$active_memories" ]; then
587
+ consolidation_prompt="Analyze these mirrored agent memories for project '$project'. Identify any memories that are redundant, overlap in scope, or describe the same subsystem/gotcha/concept.
588
+
589
+ MEMORIES:
590
+ $active_memories
591
+
592
+ For any memories that should be consolidated, merge them into a single 'Compiled Truth' summary.
593
+ The consolidated memory MUST be formatted exactly as:
594
+ --- Compiled Truth ---
595
+ <A structured, clear, up-to-date summary of the topic, gotten by merging the duplicate/overlapping memories. Keep it extremely precise.>
596
+
597
+ --- Evidence Trail ---
598
+ - <Original memory title 1>: <brief original description or timestamp>
599
+ - <Original memory title 2>: <brief original description or timestamp>
600
+
601
+ Format your output as a series of instructions:
602
+ CONSOLIDATE: <original memory name 1>, <original memory name 2> -> <new consolidated memory name> | description: <new description> | value: <the merged compiled truth + evidence content>
603
+
604
+ If no memories need consolidation, output: NONE"
605
+
606
+ consolidation_result=$(eagle_llm_call "$consolidation_prompt" "You consolidate software development memories into a single compiled truth. Be precise. Output CONSOLIDATE lines or NONE." 1024)
607
+
608
+ if [ -n "$consolidation_result" ] && ! echo "$consolidation_result" | grep -q "^NONE$"; then
609
+ cons_count=0
610
+ while IFS= read -r line; do
611
+ case "$line" in
612
+ CONSOLIDATE:*)
613
+ cons_data=$(echo "$line" | sed 's/^CONSOLIDATE:[[:space:]]*//')
614
+
615
+ # Parse matching: original_names -> new_name | description: desc | value: val
616
+ names_part=$(echo "$cons_data" | cut -d'-' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
617
+ rest_part=$(echo "$cons_data" | cut -d'>' -f2-)
618
+ new_name=$(echo "$rest_part" | cut -d'|' -f1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
619
+
620
+ desc_part=$(echo "$rest_part" | grep -oE "description:[[:space:]]*[^|]+" | sed 's/description:[[:space:]]*//')
621
+ val_part=$(echo "$rest_part" | grep -oE "value:[[:space:]]*.+" | sed 's/value:[[:space:]]*//')
622
+
623
+ [ -z "$new_name" ] || [ -z "$names_part" ] && continue
624
+
625
+ if [ "$DRY_RUN" -eq 1 ]; then
626
+ eagle_info " Would consolidate: $names_part → $new_name"
627
+ else
628
+ # 1. Add new consolidated memory node
629
+ eagle_graph_add_node "$project" "memory" "$new_name" "$val_part" ""
630
+ new_node_id=$(eagle_graph_get_node_id "$project" "memory" "$new_name")
631
+
632
+ # 2. Wire supersedes edges from new node to old nodes, and mark old nodes as inactive/superseded
633
+ IFS=',' read -ra name_arr <<< "$names_part"
634
+ for old_n in "${name_arr[@]}"; do
635
+ old_n=$(echo "$old_n" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
636
+ [ -z "$old_n" ] && continue
637
+ old_node_id=$(eagle_graph_get_node_id "$project" "memory" "$old_n")
638
+ if [ -n "$old_node_id" ] && [ -n "$new_node_id" ]; then
639
+ eagle_graph_add_edge "$project" "$new_node_id" "$old_node_id" "supersedes" 1.0
640
+ fi
641
+ done
642
+ cons_count=$((cons_count + 1))
643
+ fi
644
+ ;;
645
+ esac
646
+ done <<< "$consolidation_result"
647
+ eagle_ok "Consolidated $cons_count sets of overlapping agent memories"
648
+ else
649
+ eagle_ok "Agent memories are fully consolidated and up to date"
650
+ fi
651
+ else
652
+ eagle_dim " No active agent memories to consolidate"
653
+ fi
654
+
655
+ # ─── 8. Session compression (--full only) ─────────────────
525
656
 
526
657
  if [ "$FULL" -eq 1 ]; then
527
658
  eagle_info "Compressing old sessions..."
package/scripts/index.sh CHANGED
@@ -178,6 +178,50 @@ COMMIT;"
178
178
 
179
179
  eagle_db_pipe <<< "$txn_sql"
180
180
 
181
+ # Static syntax relation extraction & graph wiring
182
+ # Wire node & edge static parser
183
+ file_node_id=$(eagle_graph_get_node_id "$PROJECT" "file" "$file")
184
+ if [ -n "$file_node_id" ]; then
185
+ # 1. Parse function/class declarations
186
+ # e.g., "def name", "class name", "fn name", "function name", "func name"
187
+ declarations=$(grep -oE '\<(class|struct|function|def|fn|func)[[:space:]]+[A-Za-z0-9_]+' "$full_path" 2>/dev/null | awk '{print $1 ":" $2}' | sort -u || true)
188
+ if [ -n "$declarations" ]; then
189
+ while IFS=':' read -r dtype dname; do
190
+ [ -z "$dname" ] && continue
191
+ eagle_graph_add_node "$PROJECT" "$dtype" "$dname" "Declared in $file" "$file"
192
+ decl_node_id=$(eagle_graph_get_node_id "$PROJECT" "$dtype" "$dname")
193
+ if [ -n "$decl_node_id" ]; then
194
+ eagle_graph_add_edge "$PROJECT" "$file_node_id" "$decl_node_id" "declares" 1.0
195
+ fi
196
+ done <<< "$declarations"
197
+ fi
198
+
199
+ # 2. Parse local relative imports/requires/sources
200
+ # Matches paths starting with dot (./ or ../) or sourcing .sh files
201
+ local_imports=$(grep -oE "['\"](\.[^'\"]+)['\"]" "$full_path" 2>/dev/null | tr -d "'\"" || true)
202
+ # Also grab shell source files
203
+ shell_sources=$(grep -E "^[[:space:]]*(\.|source) " "$full_path" 2>/dev/null | sed -E "s/^[[:space:]]*(\.|source)[[:space:]]+(.*)/\\2/" || true)
204
+
205
+ all_refs=$(printf "%s\n%s\n" "$local_imports" "$shell_sources" | sort -u)
206
+ if [ -n "$all_refs" ]; then
207
+ while IFS= read -r ref; do
208
+ [ -z "$ref" ] && continue
209
+ # Clean up path variables in shell sources (e.g. $_eagle_db_dir/db-core.sh -> db-core.sh)
210
+ ref_clean=$(echo "$ref" | sed -E 's/.*\///; s/\.sh$//; s/\.js$//; s/\.ts$//')
211
+ [ -z "$ref_clean" ] && continue
212
+
213
+ # Check if there is a known file node in our graph that matches this basename or path
214
+ matched_file=$(eagle_db "SELECT node_name FROM graph_nodes WHERE project = '$project_sql' AND node_type = 'file' AND (node_name LIKE '%/$ref_clean%' OR node_name = '$ref_clean') LIMIT 1;")
215
+ if [ -n "$matched_file" ]; then
216
+ target_file_id=$(eagle_graph_get_node_id "$PROJECT" "file" "$matched_file")
217
+ if [ -n "$target_file_id" ]; then
218
+ eagle_graph_add_edge "$PROJECT" "$file_node_id" "$target_file_id" "imports" 1.0
219
+ fi
220
+ fi
221
+ done <<< "$all_refs"
222
+ fi
223
+ fi
224
+
181
225
  file_count=$((file_count + 1))
182
226
 
183
227
  if [ $((file_count % 10)) -eq 0 ]; then
@@ -45,6 +45,9 @@ show_help() {
45
45
  echo -e " eagle-mem memories tasks search ${CYAN}<query>${RESET} ${DIM}# full-text search tasks${RESET}"
46
46
  echo -e " eagle-mem memories tasks show ${CYAN}<file_path>${RESET} ${DIM}# show a specific task${RESET}"
47
47
  echo -e " eagle-mem memories sync ${DIM}# backfill memories + plans + tasks${RESET}"
48
+ echo -e " eagle-mem memories graph ${DIM}# view codebase knowledge graph summary${RESET}"
49
+ echo -e " eagle-mem memories graph query ${CYAN}<term>${RESET} ${DIM}# search knowledge graph nodes${RESET}"
50
+ echo -e " eagle-mem memories graph neighbors ${CYAN}<name>${RESET} ${DIM}# view a node's local network connections${RESET}"
48
51
  echo ""
49
52
  echo -e " ${BOLD}Options:${RESET}"
50
53
  echo -e " ${CYAN}-p, --project${RESET} <name> Filter by project (default: current project)"
@@ -62,6 +65,7 @@ show_help() {
62
65
 
63
66
  plan_action=""
64
67
  task_action=""
68
+ graph_action=""
65
69
 
66
70
  case "$action" in
67
71
  --help|-h) show_help ;;
@@ -73,6 +77,10 @@ case "$action" in
73
77
  task_action="${1:-list}"
74
78
  shift 2>/dev/null || true
75
79
  ;;
80
+ graph)
81
+ graph_action="${1:-summary}"
82
+ shift 2>/dev/null || true
83
+ ;;
76
84
  esac
77
85
 
78
86
  while [ $# -gt 0 ]; do
@@ -858,6 +866,116 @@ EOF
858
866
  eagle_footer "Sync complete."
859
867
  }
860
868
 
869
+ memories_graph() {
870
+ local sub_action="${graph_action:-summary}"
871
+
872
+ case "$sub_action" in
873
+ query)
874
+ local qstr="${query:-}"
875
+ if [ -z "$qstr" ]; then
876
+ eagle_err "Usage: eagle-mem memories graph query <search_term>"
877
+ exit 1
878
+ fi
879
+ eagle_header "Knowledge Graph Query"
880
+ eagle_info "Search Term: $qstr"
881
+ echo ""
882
+
883
+ local nodes
884
+ nodes=$(eagle_graph_search "$project" "$qstr" "")
885
+ if [ -z "$nodes" ]; then
886
+ eagle_dim "No graph nodes found matching '$qstr'."
887
+ echo ""
888
+ return
889
+ fi
890
+
891
+ while IFS='|' read -r nid ntype nname nval npath; do
892
+ [ -z "$nid" ] && continue
893
+ echo -e " ${BOLD}${nname}${RESET} ${CYAN}[${ntype}]${RESET} ${DIM}(ID: ${nid})${RESET}"
894
+ [ -n "$nval" ] && echo -e " ${DIM}${nval}${RESET}"
895
+ [ -n "$npath" ] && echo -e " ${DIM}Path: ${npath}${RESET}"
896
+ echo ""
897
+ done <<< "$nodes"
898
+ ;;
899
+ neighbors)
900
+ local target_name="${query:-}"
901
+ if [ -z "$target_name" ]; then
902
+ eagle_err "Usage: eagle-mem memories graph neighbors <node_name>"
903
+ exit 1
904
+ fi
905
+
906
+ # Find node by name (fuzzy or exact)
907
+ local matched
908
+ matched=$(eagle_db "SELECT id, node_type, node_name FROM graph_nodes WHERE project = '$(eagle_sql_escape "$project")' AND (node_name = '$(eagle_sql_escape "$target_name")' OR node_name LIKE '%$(eagle_sql_escape "$target_name")%') LIMIT 1;")
909
+ if [ -z "$matched" ]; then
910
+ eagle_err "Node not found matching '$target_name'."
911
+ exit 1
912
+ fi
913
+
914
+ IFS='|' read -r nid ntype nname <<< "$matched"
915
+ eagle_header "Graph Neighbors"
916
+ eagle_info "Node: ${BOLD}${nname}${RESET} ${CYAN}[${ntype}]${RESET} (ID: ${nid})"
917
+ echo ""
918
+
919
+ # Query inbound and outbound neighbors
920
+ eagle_info "Outbound Connections (Node interacts with/references/contains):"
921
+ local outbound
922
+ outbound=$(eagle_graph_query_neighbors "$nid" "out")
923
+ if [ -n "$outbound" ]; then
924
+ while IFS='|' read -r etype eweight target_id target_type target_name _; do
925
+ [ -z "$target_id" ] && continue
926
+ echo -e " ${BOLD}───[ ${etype} ]───>${RESET} ${CYAN}${target_name}${RESET} [${target_type}] ${DIM}(weight: ${eweight})${RESET}"
927
+ done <<< "$outbound"
928
+ else
929
+ eagle_dim " None"
930
+ fi
931
+ echo ""
932
+
933
+ eagle_info "Inbound Connections (Other nodes reference/call/contain this node):"
934
+ local inbound
935
+ inbound=$(eagle_graph_query_neighbors "$nid" "in")
936
+ if [ -n "$inbound" ]; then
937
+ while IFS='|' read -r etype eweight source_id source_type source_name _; do
938
+ [ -z "$source_id" ] && continue
939
+ echo -e " ${CYAN}${source_name}${RESET} [${source_type}] ${BOLD}───[ ${etype} ]───>${RESET} this ${DIM}(weight: ${eweight})${RESET}"
940
+ done <<< "$inbound"
941
+ else
942
+ eagle_dim " None"
943
+ fi
944
+ echo ""
945
+ ;;
946
+ summary|*)
947
+ eagle_header "Knowledge Graph Summary"
948
+ echo ""
949
+
950
+ local totals
951
+ totals=$(eagle_db "SELECT node_type, COUNT(*) FROM graph_nodes WHERE project = '$(eagle_sql_escape "$project")' GROUP BY node_type;")
952
+
953
+ local total_edges
954
+ total_edges=$(eagle_db "SELECT COUNT(*) FROM graph_edges WHERE project = '$(eagle_sql_escape "$project")';")
955
+
956
+ if [ -z "$totals" ]; then
957
+ eagle_dim "Graph is empty for this project."
958
+ eagle_dim "Run 'eagle-mem scan' or 'eagle-mem index' to auto-populate codebase nodes."
959
+ echo ""
960
+ return
961
+ fi
962
+
963
+ eagle_info "Entities (Nodes):"
964
+ while IFS='|' read -r ntype count; do
965
+ [ -z "$ntype" ] && continue
966
+ eagle_kv " ${ntype}:" "${count} nodes"
967
+ done <<< "$totals"
968
+ echo ""
969
+ eagle_kv "Total relationships (Edges):" "${total_edges:-0} edges"
970
+ echo ""
971
+ eagle_dim "Commands:"
972
+ eagle_dim " eagle-mem memories graph query <search_term> # search graph nodes"
973
+ eagle_dim " eagle-mem memories graph neighbors <node_name> # view neighbor connections"
974
+ echo ""
975
+ ;;
976
+ esac
977
+ }
978
+
861
979
  # ─── Dispatch ────────────────────────────────────────────
862
980
 
863
981
  case "$action" in
@@ -880,6 +998,7 @@ case "$action" in
880
998
  *) tasks_list ;;
881
999
  esac
882
1000
  ;;
1001
+ graph) memories_graph "$@" ;;
883
1002
  sync) memories_sync ;;
884
1003
  *)
885
1004
  eagle_err "Unknown action: $action"
package/scripts/scan.sh CHANGED
@@ -367,7 +367,29 @@ fi
367
367
  # Store in database
368
368
  eagle_upsert_overview "$PROJECT" "$overview" "scan"
369
369
 
370
- eagle_ok "Overview saved for project '$PROJECT'"
370
+ # Populate/wire codebase knowledge graph
371
+ eagle_graph_add_node "$PROJECT" "project" "$PROJECT" "$overview" ""
372
+ project_node_id=$(eagle_graph_get_node_id "$PROJECT" "project" "$PROJECT")
373
+
374
+ # Prune deleted/removed files from graph
375
+ eagle_graph_prune_orphans "$PROJECT"
376
+
377
+ file_node_count=0
378
+ if [ -n "$project_node_id" ]; then
379
+ while IFS= read -r file; do
380
+ [ -z "$file" ] && continue
381
+ # Add file node
382
+ eagle_graph_add_node "$PROJECT" "file" "$file" "" "$TARGET_DIR/$file"
383
+ file_node_id=$(eagle_graph_get_node_id "$PROJECT" "file" "$file")
384
+ if [ -n "$file_node_id" ]; then
385
+ # Connect project containing this file
386
+ eagle_graph_add_edge "$PROJECT" "$project_node_id" "$file_node_id" "contains" 1.0
387
+ fi
388
+ file_node_count=$((file_node_count + 1))
389
+ done < "$TMPFILE"
390
+ fi
391
+
392
+ eagle_ok "Overview saved for project '$PROJECT' (wired $file_node_count files in knowledge graph)"
371
393
  echo ""
372
394
 
373
395
  echo -e " ${BOLD}Generated overview:${RESET}"