nano-brain 2026.3.1 → 2026.3.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +144 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-brain",
3
- "version": "2026.3.1",
3
+ "version": "2026.3.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "nano-brain": "./bin/cli.js"
package/src/index.ts CHANGED
@@ -164,6 +164,7 @@ nano-brain - Memory system with hybrid search
164
164
  --batch-size=<n> Vectors per batch (default: 500)
165
165
  --dry-run Show counts without migrating
166
166
  --activate Switch to Qdrant provider after migration
167
+ verify Compare SQLite vector counts against Qdrant
167
168
  activate Switch config to use Qdrant as vector provider
168
169
  cleanup Drop SQLite vector tables (requires Qdrant active with vectors)
169
170
  Logging Config (~/.nano-brain/config.yml):
@@ -1336,7 +1337,7 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1336
1337
  const subcommand = commandArgs[0];
1337
1338
 
1338
1339
  if (!subcommand) {
1339
- console.error('Missing qdrant subcommand (up, down, status, migrate, activate, cleanup)');
1340
+ console.error('Missing qdrant subcommand (up, down, status, migrate, verify, activate, cleanup)');
1340
1341
  process.exit(1);
1341
1342
  }
1342
1343
 
@@ -1688,6 +1689,147 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1688
1689
  break;
1689
1690
  }
1690
1691
 
1692
+ case 'verify': {
1693
+ const config = loadCollectionConfig(globalOpts.configPath);
1694
+ const vectorConfig = config?.vector;
1695
+ const qdrantUrl = vectorConfig?.url || 'http://localhost:6333';
1696
+ const resolvedUrl = resolveHostUrl(qdrantUrl);
1697
+
1698
+ try {
1699
+ const healthRes = await fetch(`${resolvedUrl}/healthz`);
1700
+ if (!healthRes.ok) {
1701
+ throw new Error(`HTTP ${healthRes.status}`);
1702
+ }
1703
+ } catch {
1704
+ console.error(`❌ Qdrant is not reachable at ${resolvedUrl}.`);
1705
+ console.error(' Run `npx nano-brain qdrant up` first.');
1706
+ console.error(' If running inside a container, Qdrant must be accessible at host.docker.internal:6333.');
1707
+ process.exit(1);
1708
+ }
1709
+
1710
+ const dataDir = DEFAULT_DB_DIR;
1711
+ if (!fs.existsSync(dataDir)) {
1712
+ console.log('No databases found in ' + dataDir);
1713
+ return;
1714
+ }
1715
+
1716
+ const sqliteFiles = fs.readdirSync(dataDir).filter(f => f.endsWith('.sqlite'));
1717
+ if (sqliteFiles.length === 0) {
1718
+ console.log('No SQLite databases found');
1719
+ return;
1720
+ }
1721
+
1722
+ console.log('Verifying migration...');
1723
+ console.log('═══════════════════════════════════════════════════');
1724
+
1725
+ const Database = (await import('better-sqlite3')).default;
1726
+ const sqliteVec = await import('sqlite-vec');
1727
+
1728
+ let totalVectors = 0;
1729
+ let dbCount = 0;
1730
+ const uniqueKeys = new Set<string>();
1731
+ let sawVectorTables = false;
1732
+
1733
+ for (const sqliteFile of sqliteFiles) {
1734
+ const dbPath = path.join(dataDir, sqliteFile);
1735
+ const db = new Database(dbPath);
1736
+
1737
+ try {
1738
+ sqliteVec.load(db);
1739
+ } catch {
1740
+ console.log(`[${sqliteFile}] sqlite-vec not available, skipping`);
1741
+ db.close();
1742
+ continue;
1743
+ }
1744
+
1745
+ let vectorCount = 0;
1746
+ try {
1747
+ const countStmt = db.prepare(`
1748
+ SELECT COUNT(*) as cnt FROM content_vectors cv
1749
+ JOIN vectors_vec vv ON cv.hash || ':' || cv.seq = vv.hash_seq
1750
+ `);
1751
+ const countRow = countStmt.get() as { cnt: number };
1752
+ vectorCount = countRow.cnt;
1753
+ } catch {
1754
+ console.log(`[${sqliteFile}] no vector tables, skipping`);
1755
+ db.close();
1756
+ continue;
1757
+ }
1758
+
1759
+ sawVectorTables = true;
1760
+
1761
+ if (vectorCount === 0) {
1762
+ console.log(`[${sqliteFile}] 0 vectors`);
1763
+ db.close();
1764
+ continue;
1765
+ }
1766
+
1767
+ const keyStmt = db.prepare(`
1768
+ SELECT DISTINCT cv.hash || ':' || cv.seq as key FROM content_vectors cv
1769
+ JOIN vectors_vec vv ON cv.hash || ':' || cv.seq = vv.hash_seq
1770
+ `);
1771
+ const rows = keyStmt.all() as Array<{ key: string }>;
1772
+ for (const row of rows) {
1773
+ uniqueKeys.add(row.key);
1774
+ }
1775
+
1776
+ console.log(`[${sqliteFile}] ${vectorCount.toLocaleString()} vectors in SQLite`);
1777
+ totalVectors += vectorCount;
1778
+ dbCount++;
1779
+ db.close();
1780
+ }
1781
+
1782
+ if (!sawVectorTables || totalVectors === 0) {
1783
+ let pointsCount = 0;
1784
+ try {
1785
+ const collectionRes = await fetch(`${resolvedUrl}/collections/nano-brain`);
1786
+ if (collectionRes.ok) {
1787
+ const collectionData = await collectionRes.json();
1788
+ const result = collectionData.result || collectionData;
1789
+ pointsCount = result.points_count ?? result.vectors_count ?? 0;
1790
+ }
1791
+ } catch {
1792
+ console.error('❌ Failed to check Qdrant collection');
1793
+ process.exit(1);
1794
+ }
1795
+
1796
+ console.log('SQLite: no vector data (already cleaned up)');
1797
+ console.log(`Qdrant: ${pointsCount.toLocaleString()} vectors`);
1798
+ console.log(`ℹ️ Cannot verify — SQLite vectors already cleaned. Qdrant has ${pointsCount.toLocaleString()} vectors.`);
1799
+ break;
1800
+ }
1801
+
1802
+ console.log('───────────────────────────────────────────────────');
1803
+ console.log(`SQLite total: ${totalVectors.toLocaleString()} vectors (across ${dbCount} databases)`);
1804
+
1805
+ let pointsCount = 0;
1806
+ try {
1807
+ const collectionRes = await fetch(`${resolvedUrl}/collections/nano-brain`);
1808
+ if (collectionRes.ok) {
1809
+ const collectionData = await collectionRes.json();
1810
+ const result = collectionData.result || collectionData;
1811
+ pointsCount = result.points_count ?? result.vectors_count ?? 0;
1812
+ }
1813
+ } catch {
1814
+ console.error('❌ Failed to check Qdrant collection');
1815
+ process.exit(1);
1816
+ }
1817
+
1818
+ const uniqueCount = uniqueKeys.size;
1819
+ console.log(`Qdrant total: ${pointsCount.toLocaleString()} unique vectors`);
1820
+ const difference = totalVectors - pointsCount;
1821
+ console.log(`Difference: ${difference.toLocaleString()} (expected — cross-workspace duplicates share the same hash:seq key)`);
1822
+ console.log('');
1823
+
1824
+ if (uniqueCount > pointsCount) {
1825
+ const missing = uniqueCount - pointsCount;
1826
+ console.log(`⚠️ Found ${missing.toLocaleString()} vectors in SQLite not present in Qdrant. Run \`npx nano-brain qdrant migrate\` to sync.`);
1827
+ } else {
1828
+ console.log('✅ Migration verified: Qdrant has all unique vectors');
1829
+ }
1830
+ break;
1831
+ }
1832
+
1691
1833
  case 'activate': {
1692
1834
  const config = loadCollectionConfig(globalOpts.configPath);
1693
1835
  const vectorConfig = config?.vector;
@@ -1841,7 +1983,7 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1841
1983
 
1842
1984
  default:
1843
1985
  console.error(`Unknown qdrant subcommand: ${subcommand}`);
1844
- console.error('Available: up, down, status, migrate, activate, cleanup');
1986
+ console.error('Available: up, down, status, migrate, verify, activate, cleanup');
1845
1987
  process.exit(1);
1846
1988
  }
1847
1989
  }