nano-brain 2026.8.5 → 2026.8.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.8.5",
3
+ "version": "2026.8.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": {
@@ -26,10 +26,25 @@ function formatBytes(bytes: number): string {
26
26
  return `${mb.toFixed(1)} MB`;
27
27
  }
28
28
 
29
- async function getVectorStoreHealth(config: ReturnType<typeof loadCollectionConfig>): Promise<VectorStoreHealth | null> {
29
+ async function getVectorStoreHealth(
30
+ config: ReturnType<typeof loadCollectionConfig>,
31
+ serverRunning: boolean,
32
+ port: number
33
+ ): Promise<VectorStoreHealth | null> {
30
34
  const vectorConfig = config?.vector;
31
35
  if (!vectorConfig || vectorConfig.provider !== 'qdrant') return null;
32
36
 
37
+ // If server is running, proxy the request through it
38
+ if (serverRunning) {
39
+ try {
40
+ const health = await proxyGet(port, '/api/vector-health');
41
+ return health as VectorStoreHealth;
42
+ } catch (err) {
43
+ log('cli', 'Vector health proxy failed: ' + (err instanceof Error ? err.message : String(err)));
44
+ // Fall through to direct check
45
+ }
46
+ }
47
+
33
48
  try {
34
49
  const vectorStore = createVectorStore(vectorConfig);
35
50
  const health = await Promise.race([
@@ -110,11 +125,18 @@ async function printEmbeddingServerStatus(config: ReturnType<typeof loadCollecti
110
125
  export async function handleStatus(globalOpts: GlobalOptions, commandArgs: string[]): Promise<void> {
111
126
  log('cli', 'status command invoked');
112
127
  const serverRunning = await detectRunningServer(DEFAULT_HTTP_PORT);
113
- let serverInfo: { uptime: number; ready: boolean } | null = null;
128
+ let serverInfo: { uptime: number; ready: boolean; index?: { documentCount: number; embeddedCount: number; pendingEmbeddings: number } } | null = null;
114
129
  if (serverRunning) {
115
130
  try {
116
- const data = await proxyGet(DEFAULT_HTTP_PORT, '/api/status') as { uptime?: number; ready?: boolean };
131
+ const data = await proxyGet(DEFAULT_HTTP_PORT, '/api/status') as { uptime?: number; ready?: boolean; index?: any };
117
132
  serverInfo = { uptime: data.uptime ?? 0, ready: data.ready ?? false };
133
+ if (data.index) {
134
+ serverInfo.index = {
135
+ documentCount: data.index.documentCount ?? 0,
136
+ embeddedCount: data.index.embeddedCount ?? 0,
137
+ pendingEmbeddings: data.index.pendingEmbeddings ?? 0
138
+ };
139
+ }
118
140
  } catch (err) {
119
141
  log('cli', 'HTTP proxy failed for server info: ' + (err instanceof Error ? err.message : String(err)));
120
142
  }
@@ -207,7 +229,7 @@ export async function handleStatus(globalOpts: GlobalOptions, commandArgs: strin
207
229
  await printEmbeddingServerStatus(config);
208
230
  cliOutput('');
209
231
 
210
- const vectorHealth = await getVectorStoreHealth(config);
232
+ const vectorHealth = await getVectorStoreHealth(config, serverRunning, DEFAULT_HTTP_PORT);
211
233
  printVectorStoreSection(vectorHealth);
212
234
 
213
235
  const allTokenUsage = new Map<string, { totalTokens: number; requestCount: number; lastUpdated: string }>();
@@ -272,9 +294,10 @@ export async function handleStatus(globalOpts: GlobalOptions, commandArgs: strin
272
294
  cliOutput('');
273
295
 
274
296
  cliOutput('Index:');
275
- cliOutput(` Documents: ${health.documentCount.toLocaleString()}`);
276
- cliOutput(` Embedded: ${health.embeddedCount.toLocaleString()}`);
277
- cliOutput(` Pending embeddings: ${health.pendingEmbeddings.toLocaleString()}`);
297
+ const indexData = serverInfo?.index ?? health;
298
+ cliOutput(` Documents: ${indexData.documentCount.toLocaleString()}`);
299
+ cliOutput(` Embedded: ${indexData.embeddedCount.toLocaleString()}`);
300
+ cliOutput(` Pending embeddings: ${indexData.pendingEmbeddings.toLocaleString()}`);
278
301
  cliOutput('');
279
302
 
280
303
  if (health.collections.length > 0) {
@@ -319,7 +342,7 @@ export async function handleStatus(globalOpts: GlobalOptions, commandArgs: strin
319
342
  await printEmbeddingServerStatus(config);
320
343
  cliOutput('');
321
344
 
322
- const vectorHealth = await getVectorStoreHealth(config);
345
+ const vectorHealth = await getVectorStoreHealth(config, serverRunning, DEFAULT_HTTP_PORT);
323
346
  printVectorStoreSection(vectorHealth, vectorHealth ? undefined : store.getSqliteVecCount());
324
347
 
325
348
  printTokenUsageSection(store.getTokenUsage());
@@ -140,6 +140,27 @@ export async function handleRequest(
140
140
  return;
141
141
  }
142
142
 
143
+ if (req.method === 'GET' && pathname === '/api/vector-health') {
144
+ const vectorStore = store.getVectorStore();
145
+ if (!vectorStore) {
146
+ res.writeHead(200, { 'Content-Type': 'application/json' });
147
+ res.end(JSON.stringify({ provider: 'sqlite-vec', ok: true, vectorCount: store.getSqliteVecCount() }));
148
+ return;
149
+ }
150
+ try {
151
+ const health = await Promise.race([
152
+ vectorStore.health(),
153
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
154
+ ]);
155
+ res.writeHead(200, { 'Content-Type': 'application/json' });
156
+ res.end(JSON.stringify(health));
157
+ } catch (err) {
158
+ res.writeHead(200, { 'Content-Type': 'application/json' });
159
+ res.end(JSON.stringify({ ok: false, provider: 'qdrant', vectorCount: 0, error: err instanceof Error ? err.message : String(err) }));
160
+ }
161
+ return;
162
+ }
163
+
143
164
  if (req.method === 'POST' && pathname === '/api/query') {
144
165
  const body = await readBody(req);
145
166
  try {
@@ -9,11 +9,14 @@ export interface Reranker {
9
9
  export interface RerankerOptions {
10
10
  apiKey?: string;
11
11
  model?: string;
12
+ provider?: 'voyageai' | 'cohere';
12
13
  onTokenUsage?: (model: string, tokens: number) => void;
13
14
  }
14
15
 
15
16
  const VOYAGE_RERANK_URL = 'https://api.voyageai.com/v1/rerank';
17
+ const COHERE_RERANK_URL = 'https://api.cohere.com/v2/rerank';
16
18
  const DEFAULT_MODEL = 'rerank-2.5-lite';
19
+ const DEFAULT_COHERE_MODEL = 'rerank-v3.5';
17
20
 
18
21
  class VoyageAIReranker implements Reranker {
19
22
  private apiKey: string;
@@ -89,6 +92,79 @@ class VoyageAIReranker implements Reranker {
89
92
  dispose(): void {}
90
93
  }
91
94
 
95
+ class CohereReranker implements Reranker {
96
+ private apiKey: string;
97
+ private model: string;
98
+ private onTokenUsage?: (model: string, tokens: number) => void;
99
+
100
+ constructor(apiKey: string, model: string, onTokenUsage?: (model: string, tokens: number) => void) {
101
+ this.apiKey = apiKey;
102
+ this.model = model;
103
+ this.onTokenUsage = onTokenUsage;
104
+ }
105
+
106
+ async rerank(query: string, documents: RerankDocument[]): Promise<RerankResult> {
107
+ if (documents.length === 0) {
108
+ return { results: [], model: this.model };
109
+ }
110
+
111
+ try {
112
+ const response = await fetch(COHERE_RERANK_URL, {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Authorization': `Bearer ${this.apiKey}`,
116
+ 'Content-Type': 'application/json',
117
+ },
118
+ body: JSON.stringify({
119
+ query,
120
+ documents: documents.map(d => d.text),
121
+ model: this.model,
122
+ top_n: documents.length,
123
+ }),
124
+ signal: AbortSignal.timeout(30000),
125
+ });
126
+
127
+ if (!response.ok) {
128
+ const body = await response.text().catch(() => '');
129
+ log('reranker', `Cohere rerank failed: HTTP ${response.status} ${body}`, 'warn');
130
+ return { results: [], model: this.model };
131
+ }
132
+
133
+ const data = await response.json() as {
134
+ results: Array<{ index: number; relevance_score: number }>;
135
+ meta?: { billed_units?: { search_units?: number } };
136
+ };
137
+
138
+ if (this.onTokenUsage && data.meta?.billed_units?.search_units) {
139
+ this.onTokenUsage(this.model, data.meta.billed_units.search_units);
140
+ }
141
+
142
+ if (!data.results || !Array.isArray(data.results)) {
143
+ log('reranker', `Cohere rerank returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`, 'warn');
144
+ return { results: [], model: this.model };
145
+ }
146
+
147
+ const results = data.results
148
+ .filter(r => r.index >= 0 && r.index < documents.length)
149
+ .map(r => ({
150
+ file: documents[r.index].file,
151
+ score: r.relevance_score,
152
+ index: r.index,
153
+ }));
154
+
155
+ log('reranker', `Cohere rerank model=${this.model} docs=${documents.length} units=${data.meta?.billed_units?.search_units ?? 1}`, 'debug');
156
+
157
+ return { results, model: this.model };
158
+ } catch (err) {
159
+ const msg = err instanceof Error ? err.message : String(err);
160
+ log('reranker', `Cohere rerank error: ${msg}`, 'warn');
161
+ return { results: [], model: this.model };
162
+ }
163
+ }
164
+
165
+ dispose(): void {}
166
+ }
167
+
92
168
  export async function createReranker(
93
169
  options?: RerankerOptions
94
170
  ): Promise<Reranker | null> {
@@ -98,6 +174,14 @@ export async function createReranker(
98
174
  return null;
99
175
  }
100
176
 
177
+ const provider = options?.provider || 'voyageai';
178
+
179
+ if (provider === 'cohere') {
180
+ const model = options?.model || DEFAULT_COHERE_MODEL;
181
+ log('reranker', `Cohere reranker initialized model=${model}`);
182
+ return new CohereReranker(apiKey, model, options?.onTokenUsage);
183
+ }
184
+
101
185
  const model = options?.model || DEFAULT_MODEL;
102
186
  log('reranker', `VoyageAI reranker initialized model=${model}`);
103
187
  return new VoyageAIReranker(apiKey, model, options?.onTokenUsage);
@@ -362,6 +362,7 @@ export async function startServer(options: ServerOptions): Promise<void> {
362
362
  createReranker({
363
363
  apiKey: config?.reranker?.apiKey || config?.embedding?.apiKey,
364
364
  model: config?.reranker?.model,
365
+ provider: config?.reranker?.provider as 'voyageai' | 'cohere' | undefined,
365
366
  onTokenUsage: (model, tokens) => store.recordTokenUsage(model, tokens),
366
367
  })
367
368
  .then((loadedReranker) => {
package/src/types.ts CHANGED
@@ -139,6 +139,7 @@ export interface EmbeddingConfig {
139
139
  export interface RerankerConfig {
140
140
  model?: string
141
141
  apiKey?: string
142
+ provider?: 'voyageai' | 'cohere'
142
143
  }
143
144
 
144
145
  export interface WatcherConfig {