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 +1 -1
- package/src/cli/commands/status.ts +31 -8
- package/src/http/routes.ts +21 -0
- package/src/providers/reranker.ts +84 -0
- package/src/server/bootstrap.ts +1 -0
- package/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nano-brain",
|
|
3
|
-
"version": "2026.8.
|
|
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(
|
|
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
|
-
|
|
276
|
-
cliOutput(`
|
|
277
|
-
cliOutput(`
|
|
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());
|
package/src/http/routes.ts
CHANGED
|
@@ -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);
|
package/src/server/bootstrap.ts
CHANGED
|
@@ -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) => {
|