nano-brain 2026.8.7 → 2026.8.9
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 -2
- package/src/bench/runner.ts +56 -12
- package/src/bench.ts +1 -1
- package/src/cli/commands/embed.ts +0 -2
- package/src/cli/commands/init.ts +0 -1
- package/src/cli/commands/qdrant.ts +6 -465
- package/src/cli/commands/status.ts +14 -9
- package/src/codebase.ts +6 -5
- package/src/http/routes.ts +1 -1
- package/src/providers/qdrant.ts +7 -0
- package/src/providers/vector-store.ts +1 -12
- package/src/search.ts +0 -9
- package/src/server/bootstrap.ts +0 -5
- package/src/server/utils.ts +1 -1
- package/src/store/documents.ts +1 -11
- package/src/store/index.ts +0 -13
- package/src/store/schema.ts +7 -1
- package/src/store/vectors.ts +4 -158
- package/src/types.ts +1 -4
- package/src/providers/sqlite-vec.ts +0 -227
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nano-brain",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.9",
|
|
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": {
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"chokidar": "^5.0.0",
|
|
41
41
|
"fast-glob": "^3.3.3",
|
|
42
42
|
"p-limit": "^7.3.0",
|
|
43
|
-
"sqlite-vec": "^0.1.7-alpha.2",
|
|
44
43
|
"tree-sitter": "^0.22.4",
|
|
45
44
|
"tree-sitter-javascript": "^0.23.1",
|
|
46
45
|
"tree-sitter-python": "^0.23.6",
|
package/src/bench/runner.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createStore } from '../store.js';
|
|
|
7
7
|
import { hybridSearch } from '../search.js';
|
|
8
8
|
import { createEmbeddingProvider } from '../embeddings.js';
|
|
9
9
|
import { generateCorpus, computeCorpusHash } from './generator.js';
|
|
10
|
+
import { QdrantVecStore } from '../providers/qdrant.js';
|
|
10
11
|
import type {
|
|
11
12
|
BenchResult,
|
|
12
13
|
BenchEnvironment,
|
|
@@ -134,7 +135,9 @@ function aggregateQuality(
|
|
|
134
135
|
async function measureQuality(
|
|
135
136
|
dbPath: string,
|
|
136
137
|
groundTruth: GroundTruthQuery[],
|
|
137
|
-
ollamaUrl: string | null
|
|
138
|
+
ollamaUrl: string | null,
|
|
139
|
+
qdrantVecStore: QdrantVecStore | null,
|
|
140
|
+
hashToPath: Map<string, string>
|
|
138
141
|
): Promise<{ quality: ScaleQuality; latency: Omit<ScaleLatency, 'insert'> }> {
|
|
139
142
|
const store = createStore(dbPath);
|
|
140
143
|
|
|
@@ -165,12 +168,15 @@ async function measureQuality(
|
|
|
165
168
|
const ftsIds = ftsResults.map(r => docIdFromPath(r.path));
|
|
166
169
|
ftsPerQuery.push({ query: gt.query, ...computeQueryMetrics(ftsIds, gt.relevant_doc_ids) });
|
|
167
170
|
|
|
168
|
-
if (embedder) {
|
|
171
|
+
if (embedder && qdrantVecStore) {
|
|
169
172
|
const t0vec = Date.now();
|
|
170
173
|
const { embedding } = await embedder.embed(gt.query);
|
|
171
|
-
const
|
|
174
|
+
const qdrantResults = await qdrantVecStore.search(embedding, { limit: 10 });
|
|
172
175
|
vecQueryTimes.push(Date.now() - t0vec);
|
|
173
|
-
const vecIds =
|
|
176
|
+
const vecIds = qdrantResults
|
|
177
|
+
.map(r => hashToPath.get(r.hash))
|
|
178
|
+
.filter((p): p is string => p !== undefined)
|
|
179
|
+
.map(p => docIdFromPath(p));
|
|
174
180
|
vecPerQuery.push({ query: gt.query, ...computeQueryMetrics(vecIds, gt.relevant_doc_ids) });
|
|
175
181
|
|
|
176
182
|
const t0hyb = Date.now();
|
|
@@ -213,10 +219,10 @@ async function measureQuality(
|
|
|
213
219
|
async function insertDocs(
|
|
214
220
|
dbPath: string,
|
|
215
221
|
fixturesDir: string,
|
|
216
|
-
ollamaUrl: string | null
|
|
217
|
-
|
|
222
|
+
ollamaUrl: string | null,
|
|
223
|
+
qdrantVecStore: QdrantVecStore | null
|
|
224
|
+
): Promise<{ latency: LatencyStats; hashToPath: Map<string, string> }> {
|
|
218
225
|
const store = createStore(dbPath);
|
|
219
|
-
store.ensureVecTable(768);
|
|
220
226
|
|
|
221
227
|
let embedder: { embed(text: string): Promise<{ embedding: number[] }>; dispose(): void } | null = null;
|
|
222
228
|
if (ollamaUrl) {
|
|
@@ -230,6 +236,7 @@ async function insertDocs(
|
|
|
230
236
|
const docsDir = path.join(fixturesDir, 'docs');
|
|
231
237
|
const docFiles = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
232
238
|
const insertTimes: number[] = [];
|
|
239
|
+
const hashToPath = new Map<string, string>();
|
|
233
240
|
const workspaceRoot = process.cwd();
|
|
234
241
|
const projectHash = crypto.createHash('sha256').update(workspaceRoot).digest('hex').substring(0, 12);
|
|
235
242
|
|
|
@@ -252,9 +259,14 @@ async function insertDocs(
|
|
|
252
259
|
active: true,
|
|
253
260
|
projectHash,
|
|
254
261
|
});
|
|
255
|
-
|
|
262
|
+
hashToPath.set(hash, docPath);
|
|
263
|
+
if (embedder && qdrantVecStore) {
|
|
256
264
|
const { embedding } = await embedder.embed(content);
|
|
257
|
-
|
|
265
|
+
await qdrantVecStore.upsert({
|
|
266
|
+
id: `${hash}:0`,
|
|
267
|
+
embedding,
|
|
268
|
+
metadata: { hash, seq: 0, pos: 0, model: 'nomic-embed-text' },
|
|
269
|
+
});
|
|
258
270
|
}
|
|
259
271
|
insertTimes.push(Date.now() - t0);
|
|
260
272
|
}
|
|
@@ -263,7 +275,7 @@ async function insertDocs(
|
|
|
263
275
|
store.close();
|
|
264
276
|
}
|
|
265
277
|
|
|
266
|
-
return computeLatencyStats(insertTimes);
|
|
278
|
+
return { latency: computeLatencyStats(insertTimes), hashToPath };
|
|
267
279
|
}
|
|
268
280
|
|
|
269
281
|
async function runCombinationTests(
|
|
@@ -380,6 +392,21 @@ async function detectOllamaUrl(): Promise<string | null> {
|
|
|
380
392
|
return null;
|
|
381
393
|
}
|
|
382
394
|
|
|
395
|
+
async function detectQdrantUrl(): Promise<string | null> {
|
|
396
|
+
const candidates = [
|
|
397
|
+
process.env['QDRANT_URL'],
|
|
398
|
+
'http://localhost:6333',
|
|
399
|
+
'http://host.docker.internal:6333',
|
|
400
|
+
].filter(Boolean) as string[];
|
|
401
|
+
for (const url of candidates) {
|
|
402
|
+
try {
|
|
403
|
+
const resp = await fetch(`${url}/healthz`, { signal: AbortSignal.timeout(3000) });
|
|
404
|
+
if (resp.ok) return url;
|
|
405
|
+
} catch {}
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
383
410
|
export interface RunOptions {
|
|
384
411
|
scales: number[];
|
|
385
412
|
noCleanup: boolean;
|
|
@@ -396,6 +423,20 @@ export async function runBenchmarkSuite(opts: RunOptions): Promise<BenchResult>
|
|
|
396
423
|
console.warn('Warning: Ollama not reachable — skipping vector and hybrid quality tests');
|
|
397
424
|
}
|
|
398
425
|
|
|
426
|
+
const qdrantUrl = await detectQdrantUrl();
|
|
427
|
+
if (!qdrantUrl) {
|
|
428
|
+
console.warn('[bench] Qdrant unreachable — skipping vector and hybrid test suites');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const benchCollectionBaseName = `bench-${Date.now()}`;
|
|
432
|
+
const qdrantVecStore: QdrantVecStore | null = qdrantUrl && ollamaUrl
|
|
433
|
+
? new QdrantVecStore({ url: qdrantUrl, collection: benchCollectionBaseName, dimensions: 768 })
|
|
434
|
+
: null;
|
|
435
|
+
|
|
436
|
+
if (qdrantVecStore) {
|
|
437
|
+
await qdrantVecStore.ensureCollection();
|
|
438
|
+
}
|
|
439
|
+
|
|
399
440
|
const ollamaInfo = await getOllamaInfo(ollamaUrl);
|
|
400
441
|
const env: BenchEnvironment = {
|
|
401
442
|
ollama_model: ollamaInfo?.model ?? 'none',
|
|
@@ -444,10 +485,10 @@ export async function runBenchmarkSuite(opts: RunOptions): Promise<BenchResult>
|
|
|
444
485
|
}
|
|
445
486
|
|
|
446
487
|
console.log(' Inserting docs...');
|
|
447
|
-
const insertLatency = await insertDocs(testDbPath, fixturesDir, ollamaUrl);
|
|
488
|
+
const { latency: insertLatency, hashToPath } = await insertDocs(testDbPath, fixturesDir, ollamaUrl, qdrantVecStore);
|
|
448
489
|
|
|
449
490
|
console.log(' Running quality metrics...');
|
|
450
|
-
const { quality, latency: queryLatency } = await measureQuality(testDbPath, groundTruth, ollamaUrl);
|
|
491
|
+
const { quality, latency: queryLatency } = await measureQuality(testDbPath, groundTruth, ollamaUrl, qdrantVecStore, hashToPath);
|
|
451
492
|
|
|
452
493
|
const scaleLatency: ScaleLatency = {
|
|
453
494
|
insert: insertLatency,
|
|
@@ -479,6 +520,9 @@ export async function runBenchmarkSuite(opts: RunOptions): Promise<BenchResult>
|
|
|
479
520
|
};
|
|
480
521
|
}
|
|
481
522
|
} finally {
|
|
523
|
+
if (qdrantVecStore) {
|
|
524
|
+
try { await qdrantVecStore.deleteCollection(); } catch {}
|
|
525
|
+
}
|
|
482
526
|
if (!noCleanup) {
|
|
483
527
|
try { fs.unlinkSync(testDbPath); } catch {}
|
|
484
528
|
try { fs.unlinkSync(testDbPath + '-wal'); } catch {}
|
package/src/bench.ts
CHANGED
|
@@ -111,7 +111,7 @@ async function runSearchSuite(
|
|
|
111
111
|
const result = await embedder.embed('error handling async');
|
|
112
112
|
cachedEmbedding = result.embedding;
|
|
113
113
|
}
|
|
114
|
-
store.
|
|
114
|
+
await store.searchVecAsync('error handling async', cachedEmbedding, { limit: 10 });
|
|
115
115
|
}, iterations)
|
|
116
116
|
);
|
|
117
117
|
|
|
@@ -71,7 +71,6 @@ export async function handleEmbed(globalOpts: GlobalOptions, commandArgs: string
|
|
|
71
71
|
if (qdrantStore) store.setVectorStore(qdrantStore);
|
|
72
72
|
const hashes = store.getHashesNeedingEmbedding();
|
|
73
73
|
if (hashes.length > 0) {
|
|
74
|
-
store.ensureVecTable(provider.getDimensions());
|
|
75
74
|
cliOutput(`[${path.basename(process.cwd())}] ${hashes.length} chunks pending...`);
|
|
76
75
|
const embedded = await embedPendingCodebase(store, provider, 50);
|
|
77
76
|
totalEmbedded += embedded;
|
|
@@ -97,7 +96,6 @@ export async function handleEmbed(globalOpts: GlobalOptions, commandArgs: string
|
|
|
97
96
|
if (qdrantStore) wsStore.setVectorStore(qdrantStore);
|
|
98
97
|
const wsHashes = wsStore.getHashesNeedingEmbedding();
|
|
99
98
|
if (wsHashes.length > 0) {
|
|
100
|
-
wsStore.ensureVecTable(provider.getDimensions());
|
|
101
99
|
cliOutput(`[${path.basename(wsPath)}] ${wsHashes.length} chunks pending...`);
|
|
102
100
|
const embedded = await embedPendingCodebase(wsStore, provider, 50);
|
|
103
101
|
totalEmbedded += embedded;
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -221,7 +221,6 @@ export async function handleInit(globalOpts: GlobalOptions, commandArgs: string[
|
|
|
221
221
|
const provider = await createEmbeddingProvider({ embeddingConfig });
|
|
222
222
|
const INIT_EMBED_CAP = 50;
|
|
223
223
|
if (provider) {
|
|
224
|
-
store.ensureVecTable(provider.getDimensions());
|
|
225
224
|
let embedded = 0;
|
|
226
225
|
while (embedded < INIT_EMBED_CAP) {
|
|
227
226
|
const row = store.getNextHashNeedingEmbedding(projectHash);
|