nano-brain 2026.6.7 → 2026.6.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-brain",
3
- "version": "2026.6.7",
3
+ "version": "2026.6.8",
4
4
  "description": "Persistent memory and code intelligence for AI coding agents. Local MCP server with self-learning hybrid search (BM25 + vector + knowledge graph + LLM reranking), automatic session ingestion, codebase indexing, and 22 tools. Learns your preferences over time. Works with OpenCode, Claude, Cursor, Windsurf, and any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,50 @@
1
+ import type { Store, MemoryConnection, MemoryConnectionRelationshipType } from './types.js';
2
+
3
+ export function isValidRelationshipType(type: string): type is MemoryConnectionRelationshipType {
4
+ return (['supports', 'contradicts', 'extends', 'supersedes', 'related', 'caused_by', 'refines', 'implements'] as string[]).includes(type);
5
+ }
6
+
7
+ export interface TraversalNode {
8
+ docId: number;
9
+ depth: number;
10
+ path: MemoryConnection[];
11
+ }
12
+
13
+ export function traverse(
14
+ store: Store,
15
+ startDocId: number,
16
+ options?: { maxDepth?: number; relationshipTypes?: string[] }
17
+ ): TraversalNode[] {
18
+ const maxDepth = options?.maxDepth ?? 2;
19
+ const visited = new Set<number>();
20
+ const result: TraversalNode[] = [];
21
+ const queue: TraversalNode[] = [{ docId: startDocId, depth: 0, path: [] }];
22
+ visited.add(startDocId);
23
+
24
+ while (queue.length > 0) {
25
+ const current = queue.shift()!;
26
+ if (current.depth > 0) result.push(current);
27
+ if (current.depth >= maxDepth) continue;
28
+
29
+ const connections = store.getConnectionsForDocument(current.docId, {
30
+ relationshipType: options?.relationshipTypes?.length === 1 ? options.relationshipTypes[0] : undefined,
31
+ });
32
+
33
+ for (const conn of connections) {
34
+ if (options?.relationshipTypes && options.relationshipTypes.length > 1) {
35
+ if (!options.relationshipTypes.includes(conn.relationshipType)) continue;
36
+ }
37
+ const neighborId = conn.fromDocId === current.docId ? conn.toDocId : conn.fromDocId;
38
+ if (visited.has(neighborId)) continue;
39
+ visited.add(neighborId);
40
+ queue.push({ docId: neighborId, depth: current.depth + 1, path: [...current.path, conn] });
41
+ }
42
+ }
43
+
44
+ return result;
45
+ }
46
+
47
+ export function getRelatedDocuments(store: Store, docId: number, relationshipType?: string): number[] {
48
+ const connections = store.getConnectionsForDocument(docId, { relationshipType });
49
+ return connections.map(c => c.fromDocId === docId ? c.toDocId : c.fromDocId);
50
+ }
@@ -169,6 +169,36 @@ Respond with ONLY a JSON array, no other text.`;
169
169
  result.overallConfidence,
170
170
  new Date().toISOString()
171
171
  );
172
+
173
+ if (result.connections.length > 0) {
174
+ const projectHashRow = result.sourceIds.length > 0
175
+ ? db.prepare('SELECT project_hash FROM documents WHERE id = ?').get(result.sourceIds[0]) as { project_hash: string } | undefined
176
+ : undefined;
177
+ const projectHash = projectHashRow?.project_hash ?? 'global';
178
+ let created = 0;
179
+ for (const conn of result.connections) {
180
+ if (!conn.fromId || !conn.toId || !conn.relationship) continue;
181
+ try {
182
+ if (this.store.getConnectionCount(conn.fromId) >= 50) continue;
183
+ this.store.insertConnection({
184
+ fromDocId: conn.fromId,
185
+ toDocId: conn.toId,
186
+ relationshipType: conn.relationship as any,
187
+ description: null,
188
+ strength: typeof conn.confidence === 'number' ? conn.confidence : result.overallConfidence,
189
+ createdBy: 'consolidation',
190
+ projectHash,
191
+ });
192
+ created++;
193
+ } catch (err) {
194
+ log('consolidation', 'Failed to create connection ' + conn.fromId + '->' + conn.toId + ': ' + (err instanceof Error ? err.message : String(err)), 'warn');
195
+ }
196
+ }
197
+ if (created > 0) {
198
+ log('consolidation', 'Created ' + created + ' memory connections from consolidation');
199
+ }
200
+ }
201
+
172
202
  log('consolidation', 'Applied consolidation for ' + result.sourceIds.length + ' memories, confidence=' + result.overallConfidence.toFixed(2));
173
203
  }
174
204
 
package/src/server.ts CHANGED
@@ -11,7 +11,8 @@ import * as os from 'os';
11
11
  import * as crypto from 'crypto';
12
12
  import * as http from 'http';
13
13
  import type { Store, SearchResult, IndexHealth, Collection, StorageConfig, CodebaseConfig, EmbeddingConfig, WatcherConfig, SearchConfig, ConsolidationConfig, ProactiveConfig } from './types.js'
14
- import { DEFAULT_PROACTIVE_CONFIG } from './types.js'
14
+ import { DEFAULT_PROACTIVE_CONFIG, VALID_RELATIONSHIP_TYPES } from './types.js'
15
+ import { traverse, isValidRelationshipType } from './connection-graph.js';
15
16
  import { extractEntitiesFromMemory } from './entity-extraction.js'
16
17
  import { createLLMProvider } from './llm-provider.js';
17
18
  import { ConsolidationAgent } from './consolidation.js';
@@ -2549,6 +2550,160 @@ export function createMcpServer(deps: ServerDeps): McpServer {
2549
2550
  }
2550
2551
  }
2551
2552
  );
2553
+
2554
+ server.tool(
2555
+ 'memory_connections',
2556
+ 'Get all connections for a document. Shows how memories relate to each other.',
2557
+ {
2558
+ doc_id: z.string().describe('Document ID or path'),
2559
+ relationship_type: z.string().optional().describe('Filter by type: supports, contradicts, extends, supersedes, related, caused_by, refines, implements'),
2560
+ direction: z.enum(['incoming', 'outgoing', 'both']).optional().default('both').describe('Connection direction'),
2561
+ workspace: z.string().optional().describe('Workspace path or hash. Required in daemon mode.'),
2562
+ },
2563
+ async ({ doc_id, relationship_type, direction, workspace }) => {
2564
+ if (checkReady()) return WARMUP_ERROR;
2565
+ log('mcp', 'memory_connections doc_id="' + doc_id + '" type="' + (relationship_type || '') + '"');
2566
+
2567
+ const wsResult = requireDaemonWorkspace(deps, workspace);
2568
+ if ('error' in wsResult) {
2569
+ return { content: [{ type: 'text', text: wsResult.error }], isError: true };
2570
+ }
2571
+
2572
+ const doc = wsResult.store.findDocument(doc_id);
2573
+ if (!doc) {
2574
+ return { content: [{ type: 'text', text: 'Document not found: ' + doc_id }], isError: true };
2575
+ }
2576
+
2577
+ if (relationship_type && !isValidRelationshipType(relationship_type)) {
2578
+ return { content: [{ type: 'text', text: 'Invalid relationship type: ' + relationship_type + '. Valid: ' + VALID_RELATIONSHIP_TYPES.join(', ') }], isError: true };
2579
+ }
2580
+
2581
+ const connections = wsResult.store.getConnectionsForDocument(doc.id, {
2582
+ direction: direction as 'incoming' | 'outgoing' | 'both',
2583
+ relationshipType: relationship_type,
2584
+ });
2585
+
2586
+ if (connections.length === 0) {
2587
+ return { content: [{ type: 'text', text: 'No connections found for ' + doc_id }] };
2588
+ }
2589
+
2590
+ const lines: string[] = [`## Connections for ${doc.title} (${connections.length})\n`];
2591
+ for (const conn of connections) {
2592
+ const otherId = conn.fromDocId === doc.id ? conn.toDocId : conn.fromDocId;
2593
+ const otherDoc = wsResult.store.findDocument(String(otherId));
2594
+ const dir = conn.fromDocId === doc.id ? '→' : '←';
2595
+ lines.push(`- ${dir} **${conn.relationshipType}** ${otherDoc?.title ?? 'doc#' + otherId} (strength: ${conn.strength.toFixed(2)}, by: ${conn.createdBy})`);
2596
+ if (conn.description) lines.push(` ${conn.description}`);
2597
+ }
2598
+
2599
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
2600
+ }
2601
+ );
2602
+
2603
+ server.tool(
2604
+ 'memory_traverse',
2605
+ 'Traverse the memory connection graph from a starting document. Finds related memories up to N hops away.',
2606
+ {
2607
+ start_doc_id: z.string().describe('Starting document ID or path'),
2608
+ max_depth: z.number().optional().default(2).describe('Maximum traversal depth (default: 2)'),
2609
+ relationship_types: z.array(z.string()).optional().describe('Only follow these relationship types'),
2610
+ workspace: z.string().optional().describe('Workspace path or hash. Required in daemon mode.'),
2611
+ },
2612
+ async ({ start_doc_id, max_depth, relationship_types, workspace }) => {
2613
+ if (checkReady()) return WARMUP_ERROR;
2614
+ log('mcp', 'memory_traverse start="' + start_doc_id + '" depth=' + max_depth);
2615
+
2616
+ const wsResult = requireDaemonWorkspace(deps, workspace);
2617
+ if ('error' in wsResult) {
2618
+ return { content: [{ type: 'text', text: wsResult.error }], isError: true };
2619
+ }
2620
+
2621
+ const doc = wsResult.store.findDocument(start_doc_id);
2622
+ if (!doc) {
2623
+ return { content: [{ type: 'text', text: 'Document not found: ' + start_doc_id }], isError: true };
2624
+ }
2625
+
2626
+ if (relationship_types) {
2627
+ for (const rt of relationship_types) {
2628
+ if (!isValidRelationshipType(rt)) {
2629
+ return { content: [{ type: 'text', text: 'Invalid relationship type: ' + rt + '. Valid: ' + VALID_RELATIONSHIP_TYPES.join(', ') }], isError: true };
2630
+ }
2631
+ }
2632
+ }
2633
+
2634
+ const nodes = traverse(wsResult.store, doc.id, { maxDepth: max_depth, relationshipTypes: relationship_types });
2635
+
2636
+ if (nodes.length === 0) {
2637
+ return { content: [{ type: 'text', text: 'No connected memories found within depth ' + max_depth }] };
2638
+ }
2639
+
2640
+ const lines: string[] = [`## Graph traversal from: ${doc.title}\n`];
2641
+ for (const node of nodes) {
2642
+ const nodeDoc = wsResult.store.findDocument(String(node.docId));
2643
+ const indent = ' '.repeat(node.depth);
2644
+ const lastConn = node.path[node.path.length - 1];
2645
+ lines.push(`${indent}[depth ${node.depth}] ${nodeDoc?.title ?? 'doc#' + node.docId} (via ${lastConn?.relationshipType ?? '?'})`);
2646
+ }
2647
+ lines.push(`\n**Total:** ${nodes.length} connected memories found`);
2648
+
2649
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
2650
+ }
2651
+ );
2652
+
2653
+ server.tool(
2654
+ 'memory_connect',
2655
+ 'Create a connection between two memories. Defines how they relate to each other.',
2656
+ {
2657
+ from_doc_id: z.string().describe('Source document ID or path'),
2658
+ to_doc_id: z.string().describe('Target document ID or path'),
2659
+ relationship_type: z.string().describe('Type: supports, contradicts, extends, supersedes, related, caused_by, refines, implements'),
2660
+ description: z.string().optional().describe('Description of the relationship'),
2661
+ strength: z.number().optional().default(1.0).describe('Connection strength 0.0-1.0 (default: 1.0)'),
2662
+ workspace: z.string().optional().describe('Workspace path or hash. Required in daemon mode.'),
2663
+ },
2664
+ async ({ from_doc_id, to_doc_id, relationship_type, description, strength, workspace }) => {
2665
+ if (checkReady()) return WARMUP_ERROR;
2666
+ log('mcp', 'memory_connect from="' + from_doc_id + '" to="' + to_doc_id + '" type="' + relationship_type + '"');
2667
+
2668
+ const wsResult = requireDaemonWorkspace(deps, workspace);
2669
+ if ('error' in wsResult) {
2670
+ return { content: [{ type: 'text', text: wsResult.error }], isError: true };
2671
+ }
2672
+
2673
+ if (!isValidRelationshipType(relationship_type)) {
2674
+ return { content: [{ type: 'text', text: 'Invalid relationship type: ' + relationship_type + '. Valid: ' + VALID_RELATIONSHIP_TYPES.join(', ') }], isError: true };
2675
+ }
2676
+
2677
+ const fromDoc = wsResult.store.findDocument(from_doc_id);
2678
+ if (!fromDoc) {
2679
+ return { content: [{ type: 'text', text: 'Source document not found: ' + from_doc_id }], isError: true };
2680
+ }
2681
+
2682
+ const toDoc = wsResult.store.findDocument(to_doc_id);
2683
+ if (!toDoc) {
2684
+ return { content: [{ type: 'text', text: 'Target document not found: ' + to_doc_id }], isError: true };
2685
+ }
2686
+
2687
+ const count = wsResult.store.getConnectionCount(fromDoc.id);
2688
+ if (count >= 50) {
2689
+ return { content: [{ type: 'text', text: 'Connection limit reached (50) for document: ' + from_doc_id }], isError: true };
2690
+ }
2691
+
2692
+ const id = wsResult.store.insertConnection({
2693
+ fromDocId: fromDoc.id,
2694
+ toDocId: toDoc.id,
2695
+ relationshipType: relationship_type as any,
2696
+ description: description ?? null,
2697
+ strength: strength ?? 1.0,
2698
+ createdBy: 'user',
2699
+ projectHash: wsResult.projectHash,
2700
+ });
2701
+
2702
+ return {
2703
+ content: [{ type: 'text', text: `✅ Connection created (#${id}): ${fromDoc.title} —[${relationship_type}]→ ${toDoc.title}` }],
2704
+ };
2705
+ }
2706
+ );
2552
2707
 
2553
2708
  return server;
2554
2709
  }
package/src/store.ts CHANGED
@@ -80,7 +80,6 @@ export function createStore(dbPath: string): Store {
80
80
 
81
81
  const cached = storeCache.get(resolvedPath);
82
82
  if (cached) {
83
- log('store', 'createStore cache hit for ' + resolvedPath, 'debug');
84
83
  return cached;
85
84
  }
86
85
 
@@ -346,7 +345,7 @@ export function createStore(dbPath: string): Store {
346
345
 
347
346
  // Schema versioning
348
347
  const currentVersion = (db.pragma('user_version') as Array<{ user_version: number }>)[0].user_version;
349
- const TARGET_VERSION = 6;
348
+ const TARGET_VERSION = 8;
350
349
 
351
350
  if (currentVersion < 1) {
352
351
  db.exec(`
@@ -548,6 +547,29 @@ export function createStore(dbPath: string): Store {
548
547
  db.pragma(`user_version = 7`);
549
548
  log('store', 'Schema migrated to version 7 (entity pruning support)');
550
549
  }
550
+
551
+ if (currentVersion < 8) {
552
+ db.exec(`
553
+ CREATE TABLE IF NOT EXISTS memory_connections (
554
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
555
+ from_doc_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
556
+ to_doc_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
557
+ relationship_type TEXT NOT NULL,
558
+ description TEXT,
559
+ strength REAL NOT NULL DEFAULT 1.0,
560
+ created_by TEXT NOT NULL,
561
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
562
+ project_hash TEXT NOT NULL,
563
+ UNIQUE(from_doc_id, to_doc_id, relationship_type)
564
+ );
565
+ CREATE INDEX IF NOT EXISTS idx_mc_from ON memory_connections(from_doc_id);
566
+ CREATE INDEX IF NOT EXISTS idx_mc_to ON memory_connections(to_doc_id);
567
+ CREATE INDEX IF NOT EXISTS idx_mc_type ON memory_connections(relationship_type);
568
+ CREATE INDEX IF NOT EXISTS idx_mc_project ON memory_connections(project_hash);
569
+ `);
570
+ db.pragma(`user_version = 8`);
571
+ log('store', 'Schema migrated to version 8 (memory connections)');
572
+ }
551
573
 
552
574
  if (vecAvailable) {
553
575
  try {
@@ -596,6 +618,19 @@ export function createStore(dbPath: string): Store {
596
618
  const deactivateDocumentStmt = db.prepare(`
597
619
  UPDATE documents SET active = 0 WHERE collection = ? AND path = ?
598
620
  `);
621
+
622
+ const insertConnectionStmt = db.prepare(`
623
+ INSERT INTO memory_connections (from_doc_id, to_doc_id, relationship_type, description, strength, created_by, project_hash)
624
+ VALUES (?, ?, ?, ?, ?, ?, ?)
625
+ ON CONFLICT(from_doc_id, to_doc_id, relationship_type) DO UPDATE SET
626
+ description = excluded.description, strength = excluded.strength, created_by = excluded.created_by
627
+ `);
628
+ const getConnectionsFromStmt = db.prepare(`SELECT * FROM memory_connections WHERE from_doc_id = ? ORDER BY strength DESC`);
629
+ const getConnectionsToStmt = db.prepare(`SELECT * FROM memory_connections WHERE to_doc_id = ? ORDER BY strength DESC`);
630
+ const getConnectionsBothStmt = db.prepare(`SELECT * FROM memory_connections WHERE from_doc_id = ? OR to_doc_id = ? ORDER BY strength DESC`);
631
+ const getConnectionsByTypeStmt = db.prepare(`SELECT * FROM memory_connections WHERE (from_doc_id = ? OR to_doc_id = ?) AND relationship_type = ? ORDER BY strength DESC`);
632
+ const deleteConnectionStmt = db.prepare(`DELETE FROM memory_connections WHERE id = ?`);
633
+ const getConnectionCountStmt = db.prepare(`SELECT COUNT(*) as cnt FROM memory_connections WHERE from_doc_id = ? OR to_doc_id = ?`);
599
634
 
600
635
 
601
636
 
@@ -1197,7 +1232,7 @@ export function createStore(dbPath: string): Store {
1197
1232
 
1198
1233
  close() {
1199
1234
  if (_cached) {
1200
- log('store', 'close() skipped for cached store', 'debug');
1235
+ // cached store close is a no-op, real close happens via closeAllCachedStores()
1201
1236
  return;
1202
1237
  }
1203
1238
  try { db.pragma('wal_checkpoint(PASSIVE)'); } catch { /* ignore checkpoint errors */ }
@@ -2695,6 +2730,49 @@ export function createStore(dbPath: string): Store {
2695
2730
  params.push(limit);
2696
2731
  return db.prepare(sql).all(...params) as Array<{ id: number; path: string; body: string }>;
2697
2732
  },
2733
+
2734
+ insertConnection(conn) {
2735
+ const result = insertConnectionStmt.run(
2736
+ conn.fromDocId, conn.toDocId, conn.relationshipType,
2737
+ conn.description ?? null, conn.strength, conn.createdBy, conn.projectHash
2738
+ );
2739
+ return Number(result.lastInsertRowid);
2740
+ },
2741
+
2742
+ getConnectionsForDocument(docId, options) {
2743
+ const dir = options?.direction ?? 'both';
2744
+ const relType = options?.relationshipType;
2745
+ let rows: any[];
2746
+ if (relType) {
2747
+ rows = getConnectionsByTypeStmt.all(docId, docId, relType);
2748
+ } else if (dir === 'outgoing') {
2749
+ rows = getConnectionsFromStmt.all(docId);
2750
+ } else if (dir === 'incoming') {
2751
+ rows = getConnectionsToStmt.all(docId);
2752
+ } else {
2753
+ rows = getConnectionsBothStmt.all(docId, docId);
2754
+ }
2755
+ return rows.map((r: any) => ({
2756
+ id: r.id,
2757
+ fromDocId: r.from_doc_id,
2758
+ toDocId: r.to_doc_id,
2759
+ relationshipType: r.relationship_type,
2760
+ description: r.description,
2761
+ strength: r.strength,
2762
+ createdBy: r.created_by,
2763
+ createdAt: r.created_at,
2764
+ projectHash: r.project_hash,
2765
+ }));
2766
+ },
2767
+
2768
+ deleteConnection(id) {
2769
+ deleteConnectionStmt.run(id);
2770
+ },
2771
+
2772
+ getConnectionCount(docId) {
2773
+ const row = getConnectionCountStmt.get(docId, docId) as { cnt: number } | undefined;
2774
+ return row?.cnt ?? 0;
2775
+ },
2698
2776
  };
2699
2777
 
2700
2778
  _cached = true;
package/src/types.ts CHANGED
@@ -612,6 +612,26 @@ export interface RemoveWorkspaceResult {
612
612
  executionFlowsDeleted: number;
613
613
  }
614
614
 
615
+ export type MemoryConnectionRelationshipType = 'supports' | 'contradicts' | 'extends' | 'supersedes' | 'related' | 'caused_by' | 'refines' | 'implements';
616
+
617
+ export type MemoryConnectionCreatedBy = 'consolidation' | 'user' | 'extraction';
618
+
619
+ export const VALID_RELATIONSHIP_TYPES: MemoryConnectionRelationshipType[] = [
620
+ 'supports', 'contradicts', 'extends', 'supersedes', 'related', 'caused_by', 'refines', 'implements'
621
+ ];
622
+
623
+ export interface MemoryConnection {
624
+ id: number;
625
+ fromDocId: number;
626
+ toDocId: number;
627
+ relationshipType: MemoryConnectionRelationshipType;
628
+ description: string | null;
629
+ strength: number;
630
+ createdBy: MemoryConnectionCreatedBy;
631
+ createdAt: string;
632
+ projectHash: string;
633
+ }
634
+
615
635
  export interface Store {
616
636
  getDb(): import('better-sqlite3').Database;
617
637
  close(): void;
@@ -798,4 +818,9 @@ export interface Store {
798
818
  deduplicateEdges(entityId: number): void;
799
819
 
800
820
  getUncategorizedDocuments(limit: number, projectHash?: string): Array<{ id: number; path: string; body: string }>;
821
+
822
+ insertConnection(conn: Omit<MemoryConnection, 'id' | 'createdAt'>): number;
823
+ getConnectionsForDocument(docId: number, options?: { direction?: 'incoming' | 'outgoing' | 'both'; relationshipType?: string; projectHash?: string }): MemoryConnection[];
824
+ deleteConnection(id: number): void;
825
+ getConnectionCount(docId: number): number;
801
826
  }