nano-brain 2026.3.1 → 2026.3.3
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/index.ts +300 -44
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { startServer } from './server.js';
|
|
2
2
|
import { createStore, computeHash, indexDocument, extractProjectHashFromPath } from './store.js';
|
|
3
|
-
import { loadCollectionConfig, addCollection, removeCollection, renameCollection, listCollections, getCollections, scanCollectionFiles, saveCollectionConfig } from './collections.js';
|
|
3
|
+
import { loadCollectionConfig, addCollection, removeCollection, renameCollection, listCollections, getCollections, scanCollectionFiles, saveCollectionConfig, getWorkspaceConfig } from './collections.js';
|
|
4
4
|
import { harvestSessions } from './harvester.js';
|
|
5
5
|
import { createEmbeddingProvider, detectOllamaUrl, checkOllamaHealth, checkOpenAIHealth } from './embeddings.js';
|
|
6
6
|
import { hybridSearch, parseSearchConfig } from './search.js';
|
|
7
|
-
import { indexCodebase, embedPendingCodebase } from './codebase.js';
|
|
7
|
+
import { indexCodebase, embedPendingCodebase, getCodebaseStats } from './codebase.js';
|
|
8
8
|
import { findCycles } from './graph.js';
|
|
9
9
|
import { handleBench } from './bench.js';
|
|
10
10
|
import { resolveHostUrl } from './host.js';
|
|
@@ -12,6 +12,7 @@ import { QdrantVecStore } from './providers/qdrant.js';
|
|
|
12
12
|
import { createVectorStore } from './vector-store.js';
|
|
13
13
|
import type { SearchResult } from './types.js';
|
|
14
14
|
import type { VectorPoint } from './vector-store.js';
|
|
15
|
+
import Database from 'better-sqlite3';
|
|
15
16
|
import * as fs from 'fs';
|
|
16
17
|
import * as path from 'path';
|
|
17
18
|
import * as os from 'os';
|
|
@@ -102,6 +103,7 @@ nano-brain - Memory system with hybrid search
|
|
|
102
103
|
--daemon Run as background daemon
|
|
103
104
|
stop Stop running daemon
|
|
104
105
|
status Show index health, embedding server status, and stats
|
|
106
|
+
--all Show status for all workspaces
|
|
105
107
|
collection Manage collections
|
|
106
108
|
add <name> <path> [--pattern=<glob>]
|
|
107
109
|
remove <name>
|
|
@@ -164,6 +166,7 @@ nano-brain - Memory system with hybrid search
|
|
|
164
166
|
--batch-size=<n> Vectors per batch (default: 500)
|
|
165
167
|
--dry-run Show counts without migrating
|
|
166
168
|
--activate Switch to Qdrant provider after migration
|
|
169
|
+
verify Compare SQLite vector counts against Qdrant
|
|
167
170
|
activate Switch config to use Qdrant as vector provider
|
|
168
171
|
cleanup Drop SQLite vector tables (requires Qdrant active with vectors)
|
|
169
172
|
Logging Config (~/.nano-brain/config.yml):
|
|
@@ -328,69 +331,181 @@ async function handleCollection(globalOpts: GlobalOptions, commandArgs: string[]
|
|
|
328
331
|
}
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
console.log('nano-brain Status');
|
|
337
|
-
console.log('═══════════════════════════════════════════════════');
|
|
338
|
-
console.log('');
|
|
339
|
-
console.log('Index:');
|
|
340
|
-
console.log(` Documents: ${health.documentCount}`);
|
|
341
|
-
console.log(` Embedded: ${health.embeddedCount}`);
|
|
342
|
-
console.log(` Pending embeddings: ${health.pendingEmbeddings}`);
|
|
343
|
-
console.log(` Database size: ${(health.databaseSize / 1024 / 1024).toFixed(2)} MB`);
|
|
344
|
-
console.log('');
|
|
345
|
-
|
|
346
|
-
if (health.collections.length > 0) {
|
|
347
|
-
console.log('Collections:');
|
|
348
|
-
for (const coll of health.collections) {
|
|
349
|
-
console.log(` ${coll.name}: ${coll.documentCount} documents`);
|
|
350
|
-
}
|
|
351
|
-
console.log('');
|
|
334
|
+
function extractWorkspaceName(dbFilename: string): string {
|
|
335
|
+
const base = path.basename(dbFilename, '.sqlite');
|
|
336
|
+
const parts = base.split('-');
|
|
337
|
+
if (parts.length > 1 && parts[parts.length - 1].length === 12) {
|
|
338
|
+
return parts.slice(0, -1).join('-');
|
|
352
339
|
}
|
|
353
|
-
|
|
340
|
+
return base;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function formatBytes(bytes: number): string {
|
|
344
|
+
const mb = bytes / 1024 / 1024;
|
|
345
|
+
return `${mb.toFixed(1)} MB`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function printEmbeddingServerStatus(config: ReturnType<typeof loadCollectionConfig>): Promise<void> {
|
|
354
349
|
const embeddingConfig = config?.embedding;
|
|
355
|
-
const
|
|
356
|
-
const
|
|
350
|
+
const url = embeddingConfig?.url || detectOllamaUrl();
|
|
351
|
+
const model = embeddingConfig?.model || 'nomic-embed-text';
|
|
357
352
|
const provider = embeddingConfig?.provider || 'ollama';
|
|
358
|
-
|
|
353
|
+
|
|
359
354
|
console.log('Embedding Server:');
|
|
360
355
|
console.log(` Provider: ${provider}`);
|
|
361
|
-
console.log(` URL: ${
|
|
362
|
-
console.log(` Model: ${
|
|
363
|
-
|
|
356
|
+
console.log(` URL: ${url}`);
|
|
357
|
+
console.log(` Model: ${model}`);
|
|
358
|
+
|
|
364
359
|
if (provider === 'openai') {
|
|
365
|
-
const openAiHealth = await checkOpenAIHealth(
|
|
360
|
+
const openAiHealth = await checkOpenAIHealth(url, embeddingConfig?.apiKey || '', model);
|
|
366
361
|
if (openAiHealth.reachable) {
|
|
367
362
|
console.log(` Status: ✅ connected`);
|
|
368
|
-
console.log(` Model: ✅ ${openAiHealth.model}`);
|
|
369
363
|
} else {
|
|
370
364
|
console.log(` Status: ❌ unreachable (${openAiHealth.error})`);
|
|
371
365
|
}
|
|
372
366
|
} else if (provider !== 'local') {
|
|
373
|
-
const ollamaHealth = await checkOllamaHealth(
|
|
367
|
+
const ollamaHealth = await checkOllamaHealth(url);
|
|
374
368
|
if (ollamaHealth.reachable) {
|
|
375
|
-
const hasModel = ollamaHealth.models?.some(m => m.startsWith(ollamaModel));
|
|
376
369
|
console.log(` Status: ✅ connected`);
|
|
377
|
-
console.log(` Model: ${hasModel ? '✅ available' : '❌ not found — run: ollama pull ' + ollamaModel}`);
|
|
378
|
-
if (ollamaHealth.models && ollamaHealth.models.length > 0) {
|
|
379
|
-
console.log(` Available: ${ollamaHealth.models.join(', ')}`);
|
|
380
|
-
}
|
|
381
370
|
} else {
|
|
382
371
|
console.log(` Status: ❌ unreachable (${ollamaHealth.error})`);
|
|
383
|
-
console.log(` Fallback: local GGUF (node-llama-cpp)`);
|
|
384
372
|
}
|
|
385
373
|
} else {
|
|
386
374
|
console.log(` Status: local GGUF mode`);
|
|
387
375
|
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function handleStatus(globalOpts: GlobalOptions, commandArgs: string[]): Promise<void> {
|
|
379
|
+
log('cli', 'status command invoked');
|
|
380
|
+
const showAll = commandArgs.includes('--all');
|
|
381
|
+
const config = loadCollectionConfig(globalOpts.configPath);
|
|
382
|
+
const dataDir = path.dirname(globalOpts.dbPath);
|
|
383
|
+
|
|
384
|
+
if (showAll) {
|
|
385
|
+
let dbFiles: string[] = [];
|
|
386
|
+
try {
|
|
387
|
+
const files = fs.readdirSync(dataDir);
|
|
388
|
+
dbFiles = files.filter(f => f.endsWith('.sqlite')).map(f => path.join(dataDir, f));
|
|
389
|
+
} catch {
|
|
390
|
+
console.error(`Cannot read data directory: ${dataDir}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (dbFiles.length === 0) {
|
|
395
|
+
console.log('No workspaces found.');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('nano-brain Status — All Workspaces');
|
|
400
|
+
console.log('═══════════════════════════════════════════════════');
|
|
401
|
+
console.log('');
|
|
402
|
+
|
|
403
|
+
const header = ' Workspace Documents Embedded Pending DB Size';
|
|
404
|
+
const divider = ' ───────────────────── ───────── ──────── ─────── ───────';
|
|
405
|
+
console.log(header);
|
|
406
|
+
console.log(divider);
|
|
407
|
+
|
|
408
|
+
let totalDocs = 0;
|
|
409
|
+
let totalEmbedded = 0;
|
|
410
|
+
let totalPending = 0;
|
|
411
|
+
let totalSize = 0;
|
|
412
|
+
|
|
413
|
+
for (const dbFile of dbFiles) {
|
|
414
|
+
const workspaceName = extractWorkspaceName(dbFile);
|
|
415
|
+
let fileSize = 0;
|
|
416
|
+
try {
|
|
417
|
+
fileSize = fs.statSync(dbFile).size;
|
|
418
|
+
} catch { /* ignore */ }
|
|
419
|
+
|
|
420
|
+
let docs = 0;
|
|
421
|
+
let embedded = 0;
|
|
422
|
+
let pending = 0;
|
|
423
|
+
try {
|
|
424
|
+
const readDb = new Database(dbFile, { readonly: true });
|
|
425
|
+
try {
|
|
426
|
+
docs = (readDb.prepare('SELECT COUNT(*) as count FROM documents WHERE active = 1').get() as { count: number }).count;
|
|
427
|
+
embedded = (readDb.prepare('SELECT COUNT(*) as count FROM content_vectors').get() as { count: number }).count;
|
|
428
|
+
pending = docs - embedded;
|
|
429
|
+
if (pending < 0) pending = 0;
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
readDb.close();
|
|
433
|
+
} catch { /* ignore */ }
|
|
434
|
+
|
|
435
|
+
totalDocs += docs;
|
|
436
|
+
totalEmbedded += embedded;
|
|
437
|
+
totalPending += pending;
|
|
438
|
+
totalSize += fileSize;
|
|
439
|
+
|
|
440
|
+
const name = workspaceName.padEnd(21);
|
|
441
|
+
const docsStr = docs.toLocaleString().padStart(9);
|
|
442
|
+
const embeddedStr = embedded.toLocaleString().padStart(8);
|
|
443
|
+
const pendingStr = pending.toLocaleString().padStart(7);
|
|
444
|
+
const sizeStr = formatBytes(fileSize).padStart(9);
|
|
445
|
+
console.log(` ${name} ${docsStr} ${embeddedStr} ${pendingStr} ${sizeStr}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.log('');
|
|
449
|
+
console.log(` Total: ${dbFiles.length} workspaces, ${totalDocs.toLocaleString()} documents, ${totalPending.toLocaleString()} pending embeddings, ${formatBytes(totalSize)}`);
|
|
450
|
+
console.log('');
|
|
451
|
+
|
|
452
|
+
await printEmbeddingServerStatus(config);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const workspaceRoot = process.cwd();
|
|
457
|
+
const resolvedDbPath = resolveDbPath(globalOpts.dbPath, workspaceRoot);
|
|
458
|
+
const workspaceName = extractWorkspaceName(resolvedDbPath);
|
|
459
|
+
|
|
460
|
+
let dbSize = 0;
|
|
461
|
+
try {
|
|
462
|
+
dbSize = fs.statSync(resolvedDbPath).size;
|
|
463
|
+
} catch { /* ignore */ }
|
|
464
|
+
|
|
465
|
+
const store = createStore(resolvedDbPath);
|
|
466
|
+
const health = store.getIndexHealth();
|
|
467
|
+
|
|
468
|
+
console.log(`nano-brain Status — ${workspaceName}`);
|
|
469
|
+
console.log('═══════════════════════════════════════════════════');
|
|
388
470
|
console.log('');
|
|
389
|
-
|
|
471
|
+
|
|
472
|
+
console.log('Database:');
|
|
473
|
+
console.log(` Path: ${resolvedDbPath.replace(os.homedir(), '~')}`);
|
|
474
|
+
console.log(` Size: ${formatBytes(dbSize)} (on disk)`);
|
|
475
|
+
console.log('');
|
|
476
|
+
|
|
477
|
+
console.log('Index:');
|
|
478
|
+
console.log(` Documents: ${health.documentCount.toLocaleString()}`);
|
|
479
|
+
console.log(` Embedded: ${health.embeddedCount.toLocaleString()}`);
|
|
480
|
+
console.log(` Pending embeddings: ${health.pendingEmbeddings.toLocaleString()}`);
|
|
481
|
+
console.log('');
|
|
482
|
+
|
|
483
|
+
if (health.collections.length > 0) {
|
|
484
|
+
console.log('Collections:');
|
|
485
|
+
for (const coll of health.collections) {
|
|
486
|
+
console.log(` ${coll.name.padEnd(10)} ${coll.documentCount.toLocaleString()} documents`);
|
|
487
|
+
}
|
|
488
|
+
console.log('');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const wsConfig = getWorkspaceConfig(config, workspaceRoot);
|
|
492
|
+
const codebaseStats = getCodebaseStats(store, wsConfig?.codebase, workspaceRoot);
|
|
493
|
+
if (codebaseStats) {
|
|
494
|
+
console.log('Codebase:');
|
|
495
|
+
console.log(` Enabled: ${codebaseStats.enabled}`);
|
|
496
|
+
console.log(` Storage: ${formatBytes(codebaseStats.storageUsed)} / ${formatBytes(codebaseStats.maxSize)}`);
|
|
497
|
+
console.log(` Extensions: ${codebaseStats.extensions.join(', ') || 'auto-detect'}`);
|
|
498
|
+
console.log(` Excludes: ${codebaseStats.excludeCount} patterns`);
|
|
499
|
+
console.log('');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
await printEmbeddingServerStatus(config);
|
|
503
|
+
console.log('');
|
|
504
|
+
|
|
390
505
|
console.log('Models:');
|
|
391
506
|
console.log(` Embedding: ${health.modelStatus.embedding}`);
|
|
392
507
|
console.log(` Reranker: ${health.modelStatus.reranker}`);
|
|
393
|
-
console.log(` Expander:
|
|
508
|
+
console.log(` Expander: ${health.modelStatus.expander}`);
|
|
394
509
|
store.close();
|
|
395
510
|
}
|
|
396
511
|
|
|
@@ -1336,7 +1451,7 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
|
|
|
1336
1451
|
const subcommand = commandArgs[0];
|
|
1337
1452
|
|
|
1338
1453
|
if (!subcommand) {
|
|
1339
|
-
console.error('Missing qdrant subcommand (up, down, status, migrate, activate, cleanup)');
|
|
1454
|
+
console.error('Missing qdrant subcommand (up, down, status, migrate, verify, activate, cleanup)');
|
|
1340
1455
|
process.exit(1);
|
|
1341
1456
|
}
|
|
1342
1457
|
|
|
@@ -1688,6 +1803,147 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
|
|
|
1688
1803
|
break;
|
|
1689
1804
|
}
|
|
1690
1805
|
|
|
1806
|
+
case 'verify': {
|
|
1807
|
+
const config = loadCollectionConfig(globalOpts.configPath);
|
|
1808
|
+
const vectorConfig = config?.vector;
|
|
1809
|
+
const qdrantUrl = vectorConfig?.url || 'http://localhost:6333';
|
|
1810
|
+
const resolvedUrl = resolveHostUrl(qdrantUrl);
|
|
1811
|
+
|
|
1812
|
+
try {
|
|
1813
|
+
const healthRes = await fetch(`${resolvedUrl}/healthz`);
|
|
1814
|
+
if (!healthRes.ok) {
|
|
1815
|
+
throw new Error(`HTTP ${healthRes.status}`);
|
|
1816
|
+
}
|
|
1817
|
+
} catch {
|
|
1818
|
+
console.error(`❌ Qdrant is not reachable at ${resolvedUrl}.`);
|
|
1819
|
+
console.error(' Run `npx nano-brain qdrant up` first.');
|
|
1820
|
+
console.error(' If running inside a container, Qdrant must be accessible at host.docker.internal:6333.');
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
const dataDir = DEFAULT_DB_DIR;
|
|
1825
|
+
if (!fs.existsSync(dataDir)) {
|
|
1826
|
+
console.log('No databases found in ' + dataDir);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
const sqliteFiles = fs.readdirSync(dataDir).filter(f => f.endsWith('.sqlite'));
|
|
1831
|
+
if (sqliteFiles.length === 0) {
|
|
1832
|
+
console.log('No SQLite databases found');
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
console.log('Verifying migration...');
|
|
1837
|
+
console.log('═══════════════════════════════════════════════════');
|
|
1838
|
+
|
|
1839
|
+
const Database = (await import('better-sqlite3')).default;
|
|
1840
|
+
const sqliteVec = await import('sqlite-vec');
|
|
1841
|
+
|
|
1842
|
+
let totalVectors = 0;
|
|
1843
|
+
let dbCount = 0;
|
|
1844
|
+
const uniqueKeys = new Set<string>();
|
|
1845
|
+
let sawVectorTables = false;
|
|
1846
|
+
|
|
1847
|
+
for (const sqliteFile of sqliteFiles) {
|
|
1848
|
+
const dbPath = path.join(dataDir, sqliteFile);
|
|
1849
|
+
const db = new Database(dbPath);
|
|
1850
|
+
|
|
1851
|
+
try {
|
|
1852
|
+
sqliteVec.load(db);
|
|
1853
|
+
} catch {
|
|
1854
|
+
console.log(`[${sqliteFile}] sqlite-vec not available, skipping`);
|
|
1855
|
+
db.close();
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
let vectorCount = 0;
|
|
1860
|
+
try {
|
|
1861
|
+
const countStmt = db.prepare(`
|
|
1862
|
+
SELECT COUNT(*) as cnt FROM content_vectors cv
|
|
1863
|
+
JOIN vectors_vec vv ON cv.hash || ':' || cv.seq = vv.hash_seq
|
|
1864
|
+
`);
|
|
1865
|
+
const countRow = countStmt.get() as { cnt: number };
|
|
1866
|
+
vectorCount = countRow.cnt;
|
|
1867
|
+
} catch {
|
|
1868
|
+
console.log(`[${sqliteFile}] no vector tables, skipping`);
|
|
1869
|
+
db.close();
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
sawVectorTables = true;
|
|
1874
|
+
|
|
1875
|
+
if (vectorCount === 0) {
|
|
1876
|
+
console.log(`[${sqliteFile}] 0 vectors`);
|
|
1877
|
+
db.close();
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const keyStmt = db.prepare(`
|
|
1882
|
+
SELECT DISTINCT cv.hash || ':' || cv.seq as key FROM content_vectors cv
|
|
1883
|
+
JOIN vectors_vec vv ON cv.hash || ':' || cv.seq = vv.hash_seq
|
|
1884
|
+
`);
|
|
1885
|
+
const rows = keyStmt.all() as Array<{ key: string }>;
|
|
1886
|
+
for (const row of rows) {
|
|
1887
|
+
uniqueKeys.add(row.key);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
console.log(`[${sqliteFile}] ${vectorCount.toLocaleString()} vectors in SQLite`);
|
|
1891
|
+
totalVectors += vectorCount;
|
|
1892
|
+
dbCount++;
|
|
1893
|
+
db.close();
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
if (!sawVectorTables || totalVectors === 0) {
|
|
1897
|
+
let pointsCount = 0;
|
|
1898
|
+
try {
|
|
1899
|
+
const collectionRes = await fetch(`${resolvedUrl}/collections/nano-brain`);
|
|
1900
|
+
if (collectionRes.ok) {
|
|
1901
|
+
const collectionData = await collectionRes.json();
|
|
1902
|
+
const result = collectionData.result || collectionData;
|
|
1903
|
+
pointsCount = result.points_count ?? result.vectors_count ?? 0;
|
|
1904
|
+
}
|
|
1905
|
+
} catch {
|
|
1906
|
+
console.error('❌ Failed to check Qdrant collection');
|
|
1907
|
+
process.exit(1);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
console.log('SQLite: no vector data (already cleaned up)');
|
|
1911
|
+
console.log(`Qdrant: ${pointsCount.toLocaleString()} vectors`);
|
|
1912
|
+
console.log(`ℹ️ Cannot verify — SQLite vectors already cleaned. Qdrant has ${pointsCount.toLocaleString()} vectors.`);
|
|
1913
|
+
break;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
console.log('───────────────────────────────────────────────────');
|
|
1917
|
+
console.log(`SQLite total: ${totalVectors.toLocaleString()} vectors (across ${dbCount} databases)`);
|
|
1918
|
+
|
|
1919
|
+
let pointsCount = 0;
|
|
1920
|
+
try {
|
|
1921
|
+
const collectionRes = await fetch(`${resolvedUrl}/collections/nano-brain`);
|
|
1922
|
+
if (collectionRes.ok) {
|
|
1923
|
+
const collectionData = await collectionRes.json();
|
|
1924
|
+
const result = collectionData.result || collectionData;
|
|
1925
|
+
pointsCount = result.points_count ?? result.vectors_count ?? 0;
|
|
1926
|
+
}
|
|
1927
|
+
} catch {
|
|
1928
|
+
console.error('❌ Failed to check Qdrant collection');
|
|
1929
|
+
process.exit(1);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
const uniqueCount = uniqueKeys.size;
|
|
1933
|
+
console.log(`Qdrant total: ${pointsCount.toLocaleString()} unique vectors`);
|
|
1934
|
+
const difference = totalVectors - pointsCount;
|
|
1935
|
+
console.log(`Difference: ${difference.toLocaleString()} (expected — cross-workspace duplicates share the same hash:seq key)`);
|
|
1936
|
+
console.log('');
|
|
1937
|
+
|
|
1938
|
+
if (uniqueCount > pointsCount) {
|
|
1939
|
+
const missing = uniqueCount - pointsCount;
|
|
1940
|
+
console.log(`⚠️ Found ${missing.toLocaleString()} vectors in SQLite not present in Qdrant. Run \`npx nano-brain qdrant migrate\` to sync.`);
|
|
1941
|
+
} else {
|
|
1942
|
+
console.log('✅ Migration verified: Qdrant has all unique vectors');
|
|
1943
|
+
}
|
|
1944
|
+
break;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1691
1947
|
case 'activate': {
|
|
1692
1948
|
const config = loadCollectionConfig(globalOpts.configPath);
|
|
1693
1949
|
const vectorConfig = config?.vector;
|
|
@@ -1841,7 +2097,7 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
|
|
|
1841
2097
|
|
|
1842
2098
|
default:
|
|
1843
2099
|
console.error(`Unknown qdrant subcommand: ${subcommand}`);
|
|
1844
|
-
console.error('Available: up, down, status, migrate, activate, cleanup');
|
|
2100
|
+
console.error('Available: up, down, status, migrate, verify, activate, cleanup');
|
|
1845
2101
|
process.exit(1);
|
|
1846
2102
|
}
|
|
1847
2103
|
}
|
|
@@ -1872,7 +2128,7 @@ async function main() {
|
|
|
1872
2128
|
case 'collection':
|
|
1873
2129
|
return handleCollection(globalOpts, commandArgs);
|
|
1874
2130
|
case 'status':
|
|
1875
|
-
return handleStatus(globalOpts);
|
|
2131
|
+
return handleStatus(globalOpts, commandArgs);
|
|
1876
2132
|
case 'update':
|
|
1877
2133
|
return handleUpdate(globalOpts);
|
|
1878
2134
|
case 'embed':
|