nano-brain 2026.6.5 → 2026.6.7

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.5",
3
+ "version": "2026.6.7",
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": {
package/src/server.ts CHANGED
@@ -91,7 +91,7 @@ export function resolveWorkspace(deps: ServerDeps, filePath?: string, workspaceP
91
91
  }
92
92
  const wsStore = openWorkspaceStore(deps.dataDir, wsPath);
93
93
  if (wsStore) {
94
- return { store: wsStore, workspaceRoot: wsPath, projectHash: wsHash, needsClose: true };
94
+ return { store: wsStore, workspaceRoot: wsPath, projectHash: wsHash, needsClose: false };
95
95
  }
96
96
  }
97
97
  }
@@ -111,7 +111,7 @@ export function resolveWorkspace(deps: ServerDeps, filePath?: string, workspaceP
111
111
  }
112
112
  const wsStore = openWorkspaceStore(deps.dataDir, bestMatch.wsPath);
113
113
  if (wsStore) {
114
- return { store: wsStore, workspaceRoot: bestMatch.wsPath, projectHash: wsHash, needsClose: true };
114
+ return { store: wsStore, workspaceRoot: bestMatch.wsPath, projectHash: wsHash, needsClose: false };
115
115
  }
116
116
  }
117
117
  }
@@ -1122,7 +1122,7 @@ export function createMcpServer(deps: ServerDeps): McpServer {
1122
1122
  // Open the correct workspace's symbol graph DB (not deps.db which is the startup workspace)
1123
1123
  let symbolGraphDb = deps.db;
1124
1124
  let symbolGraphDbNeedsClose = false;
1125
- if (resolved?.needsClose && deps.dataDir && resolved.workspaceRoot) {
1125
+ if (resolved && resolved.projectHash !== deps.currentProjectHash && deps.dataDir && resolved.workspaceRoot) {
1126
1126
  const wsDbPath = resolveWorkspaceDbPath(deps.dataDir, resolved.workspaceRoot);
1127
1127
  symbolGraphDb = openDatabase(wsDbPath);
1128
1128
  symbolGraphDbNeedsClose = true;
@@ -2609,17 +2609,8 @@ export async function startServer(options: ServerOptions): Promise<void> {
2609
2609
  let resolvedWorkspaceRoot: string;
2610
2610
  if (daemon && config?.workspaces && Object.keys(config.workspaces).length > 0) {
2611
2611
  const configuredWorkspaces = Object.keys(config.workspaces);
2612
- const cwd = root || process.cwd();
2613
- const cwdMatch = configuredWorkspaces.find(ws => cwd === ws || cwd.startsWith(ws + '/'));
2614
- if (cwdMatch) {
2615
- resolvedWorkspaceRoot = cwdMatch;
2616
- log('server', 'Daemon mode: cwd matches configured workspace');
2617
- log('server', `Daemon mode: workspace from cwd = ${resolvedWorkspaceRoot}`);
2618
- } else {
2619
- resolvedWorkspaceRoot = configuredWorkspaces[0];
2620
- log('server', 'Daemon mode: cwd does not match any workspace, using first configured');
2621
- log('server', `Daemon mode: primary workspace = ${resolvedWorkspaceRoot}`);
2622
- }
2612
+ resolvedWorkspaceRoot = configuredWorkspaces[0];
2613
+ log('server', `Daemon mode: primary workspace = ${resolvedWorkspaceRoot}`);
2623
2614
  } else {
2624
2615
  resolvedWorkspaceRoot = root || process.cwd();
2625
2616
  }
package/src/store.ts CHANGED
@@ -80,7 +80,7 @@ export function createStore(dbPath: string): Store {
80
80
 
81
81
  const cached = storeCache.get(resolvedPath);
82
82
  if (cached) {
83
- log('store', 'createStore returning cached instance for ' + resolvedPath);
83
+ log('store', 'createStore cache hit for ' + resolvedPath, 'debug');
84
84
  return cached;
85
85
  }
86
86
 
@@ -1197,7 +1197,7 @@ export function createStore(dbPath: string): Store {
1197
1197
 
1198
1198
  close() {
1199
1199
  if (_cached) {
1200
- log('store', 'close() skipped for cached store');
1200
+ log('store', 'close() skipped for cached store', 'debug');
1201
1201
  return;
1202
1202
  }
1203
1203
  try { db.pragma('wal_checkpoint(PASSIVE)'); } catch { /* ignore checkpoint errors */ }
package/src/watcher.ts CHANGED
@@ -66,6 +66,54 @@ export interface WatcherStats {
66
66
  isReindexing: boolean;
67
67
  }
68
68
 
69
+ /**
70
+ * Convert a glob exclude pattern (e.g. from BUILTIN_EXCLUDE_PATTERNS) into a
71
+ * chokidar v5-compatible matcher. Chokidar v5 only does exact-string equality
72
+ * for string matchers, so we must return a RegExp or function instead.
73
+ *
74
+ * Supported pattern shapes:
75
+ * ** /dir/ ** → directory name anywhere → /[/\\]dir([/\\]|$)/
76
+ * ** /a/b/ ** → nested path segment → /[/\\]a[/\\]b([/\\]|$)/
77
+ * ** /*.ext → file extension anywhere → /\.ext$/
78
+ * ** /exact-file → filename anywhere → /[/\\]exact-file$/
79
+ * /absolute/ ** → absolute prefix → starts-with check
80
+ * plain-name → bare directory name → /[/\\]plain-name([/\\]|$)/
81
+ */
82
+ function globToChokidarMatcher(pattern: string): RegExp {
83
+ const p = pattern.replace(/\\/g, '/')
84
+
85
+ // Absolute prefix: /tmp/** → match paths starting with /tmp/
86
+ if (p.startsWith('/') && !p.startsWith('*')) {
87
+ const prefix = p.replace(/\/\*\*\/?$/, '')
88
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
89
+ return new RegExp(`^${escaped}([/\\\\]|$)`)
90
+ }
91
+
92
+ // Strip leading **/ and trailing /**
93
+ let core = p
94
+ if (core.startsWith('**/')) core = core.slice(3)
95
+ if (core.endsWith('/**')) core = core.slice(0, -3)
96
+
97
+ if (core.startsWith('*')) {
98
+ const ext = core.slice(1)
99
+ const escaped = ext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
100
+ // *.egg-info etc. can be directories — match as path segment, not just suffix
101
+ return new RegExp(`${escaped}([/\\\\]|$)`)
102
+ }
103
+
104
+ // Contains wildcard in filename: e.g. "assets/index-*.js" or "i18n/locales/*.json"
105
+ if (core.includes('*')) {
106
+ const escaped = core
107
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
108
+ .replace(/\*/g, '[^/\\\\]*')
109
+ return new RegExp(`[/\\\\]${escaped}$`)
110
+ }
111
+
112
+ // Directory or filename: "node_modules", "public/vs", "package-lock.json"
113
+ const escaped = core.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\//g, '[/\\\\]')
114
+ return new RegExp(`[/\\\\]${escaped}([/\\\\]|$)`)
115
+ }
116
+
69
117
  export function startWatcher(options: WatcherOptions): Watcher {
70
118
  const {
71
119
  store,
@@ -281,7 +329,7 @@ export function startWatcher(options: WatcherOptions): Watcher {
281
329
 
282
330
  const setupWatcher = () => {
283
331
  const pathsToWatch: string[] = []
284
- const ignoredPatterns: (string | RegExp)[] = [/(^|[\/])\../]
332
+ const ignoredPatterns: (string | RegExp | ((path: string) => boolean))[] = [/(^|[/\\])\./]
285
333
  for (const collection of collections) {
286
334
  const expandedPath = collection.path.replace(/^~/, os.homedir())
287
335
  if (fs.existsSync(expandedPath)) {
@@ -294,20 +342,20 @@ export function startWatcher(options: WatcherOptions): Watcher {
294
342
  watchedPaths.add(workspaceRoot)
295
343
  const excludePatterns = mergeExcludePatterns(codebaseConfig, workspaceRoot)
296
344
  for (const pattern of excludePatterns) {
297
- // Convert glob patterns to regex for chokidar directory-level matching
298
- // e.g. 'node_modules' -> /[\/]node_modules([\/]|$)/
299
- // e.g. '*.min.js' -> /\.min\.js$/
300
- if (pattern.startsWith('*')) {
301
- const escaped = pattern.slice(1).replace(/\./g, '\\.').replace(/\*/g, '.*')
302
- ignoredPatterns.push(new RegExp(`${escaped}$`))
303
- } else {
304
- const escaped = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')
305
- ignoredPatterns.push(new RegExp(`[\\/]${escaped}([\\/]|$)`))
306
- }
345
+ ignoredPatterns.push(globToChokidarMatcher(pattern))
346
+ }
347
+ }
348
+ const deduped: string[] = []
349
+ for (const p of pathsToWatch) {
350
+ const isSubpath = pathsToWatch.some(other =>
351
+ other !== p && p.startsWith(other.endsWith('/') ? other : other + '/')
352
+ )
353
+ if (!isSubpath) {
354
+ deduped.push(p)
307
355
  }
308
356
  }
309
- if (pathsToWatch.length === 0) return
310
- watcher = watch(pathsToWatch, {
357
+ if (deduped.length === 0) return
358
+ watcher = watch(deduped, {
311
359
  ignored: ignoredPatterns,
312
360
  persistent: true,
313
361
  ignoreInitial: true,