nano-brain 2026.3.0 → 2026.3.1

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 +194 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-brain",
3
- "version": "2026.3.0",
3
+ "version": "2026.3.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "nano-brain": "./bin/cli.js"
package/src/index.ts CHANGED
@@ -163,6 +163,9 @@ nano-brain - Memory system with hybrid search
163
163
  --workspace=<path> Migrate specific workspace only
164
164
  --batch-size=<n> Vectors per batch (default: 500)
165
165
  --dry-run Show counts without migrating
166
+ --activate Switch to Qdrant provider after migration
167
+ activate Switch config to use Qdrant as vector provider
168
+ cleanup Drop SQLite vector tables (requires Qdrant active with vectors)
166
169
  Logging Config (~/.nano-brain/config.yml):
167
170
  logging:
168
171
  enabled: true # enable file logging (or use NANO_BRAIN_LOG=1 env)
@@ -1333,7 +1336,7 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1333
1336
  const subcommand = commandArgs[0];
1334
1337
 
1335
1338
  if (!subcommand) {
1336
- console.error('Missing qdrant subcommand (up, down, status, migrate)');
1339
+ console.error('Missing qdrant subcommand (up, down, status, migrate, activate, cleanup)');
1337
1340
  process.exit(1);
1338
1341
  }
1339
1342
 
@@ -1418,6 +1421,19 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1418
1421
  }
1419
1422
 
1420
1423
  case 'status': {
1424
+ const config = loadCollectionConfig(globalOpts.configPath);
1425
+ const vectorConfig = config?.vector;
1426
+ const currentProvider = vectorConfig?.provider || 'sqlite-vec';
1427
+
1428
+ console.log('Qdrant Status');
1429
+ console.log('═══════════════════════════════════════════════════');
1430
+ if (currentProvider === 'qdrant') {
1431
+ console.log(`Active provider: qdrant ✓`);
1432
+ } else {
1433
+ console.log(`Active provider: sqlite-vec (default)`);
1434
+ }
1435
+ console.log('');
1436
+
1421
1437
  let containerStatus = 'unknown';
1422
1438
  try {
1423
1439
  const output = execSync(`docker compose -f "${composeTarget}" ps --format json`, { encoding: 'utf-8' });
@@ -1436,12 +1452,8 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1436
1452
  containerStatus = 'not running';
1437
1453
  }
1438
1454
 
1439
- console.log('Qdrant Status');
1440
- console.log('═══════════════════════════════════════════════════');
1441
1455
  console.log(`Container: ${containerStatus}`);
1442
1456
 
1443
- const config = loadCollectionConfig(globalOpts.configPath);
1444
- const vectorConfig = config?.vector;
1445
1457
  const qdrantUrl = vectorConfig?.url || 'http://localhost:6333';
1446
1458
  const resolvedUrl = resolveHostUrl(qdrantUrl);
1447
1459
 
@@ -1473,9 +1485,6 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1473
1485
  }
1474
1486
  console.log(' Run `npx nano-brain qdrant up` to start.');
1475
1487
  }
1476
-
1477
- console.log('');
1478
- console.log(`Config provider: ${vectorConfig?.provider || 'sqlite-vec (default)'}`);
1479
1488
  break;
1480
1489
  }
1481
1490
 
@@ -1483,6 +1492,7 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1483
1492
  let workspaceFilter: string | undefined;
1484
1493
  let batchSize = 500;
1485
1494
  let dryRun = false;
1495
+ let activateAfter = false;
1486
1496
 
1487
1497
  for (const arg of commandArgs.slice(1)) {
1488
1498
  if (arg.startsWith('--workspace=')) {
@@ -1491,6 +1501,8 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1491
1501
  batchSize = parseInt(arg.substring(13), 10);
1492
1502
  } else if (arg === '--dry-run') {
1493
1503
  dryRun = true;
1504
+ } else if (arg === '--activate') {
1505
+ activateAfter = true;
1494
1506
  }
1495
1507
  }
1496
1508
 
@@ -1650,13 +1662,186 @@ async function handleQdrant(globalOpts: GlobalOptions, commandArgs: string[]): P
1650
1662
  console.log(`\n📊 Dry-run complete: ${totalVectors} vectors in ${dbCount} database(s)`);
1651
1663
  } else {
1652
1664
  console.log(`\n✅ Migrated ${totalVectors} vectors from ${dbCount} database(s) in ${elapsed}s`);
1665
+
1666
+ const currentProvider = config?.vector?.provider || 'sqlite-vec';
1667
+ if (currentProvider !== 'qdrant') {
1668
+ if (activateAfter) {
1669
+ let updatedConfig = loadCollectionConfig(globalOpts.configPath);
1670
+ if (!updatedConfig) {
1671
+ updatedConfig = { collections: {} };
1672
+ }
1673
+ const newVectorConfig: VectorConfigSection = {
1674
+ provider: 'qdrant',
1675
+ url: vectorConfig?.url || 'http://localhost:6333',
1676
+ collection: vectorConfig?.collection || 'nano-brain',
1677
+ };
1678
+ updatedConfig.vector = newVectorConfig;
1679
+ saveCollectionConfig(globalOpts.configPath, updatedConfig);
1680
+ console.log('\n✅ Switched to Qdrant provider');
1681
+ } else {
1682
+ console.log(`\nProvider is currently: ${currentProvider}`);
1683
+ console.log('To use Qdrant for searches, run: npx nano-brain qdrant activate');
1684
+ console.log('Or re-run with: npx nano-brain qdrant migrate --activate');
1685
+ }
1686
+ }
1687
+ }
1688
+ break;
1689
+ }
1690
+
1691
+ case 'activate': {
1692
+ const config = loadCollectionConfig(globalOpts.configPath);
1693
+ const vectorConfig = config?.vector;
1694
+ const qdrantUrl = vectorConfig?.url || 'http://localhost:6333';
1695
+ const resolvedUrl = resolveHostUrl(qdrantUrl);
1696
+
1697
+ try {
1698
+ const healthRes = await fetch(`${resolvedUrl}/healthz`);
1699
+ if (!healthRes.ok) {
1700
+ throw new Error(`HTTP ${healthRes.status}`);
1701
+ }
1702
+ } catch {
1703
+ console.error(`❌ Qdrant is not reachable at ${resolvedUrl}.`);
1704
+ console.error(' Run `npx nano-brain qdrant up` first.');
1705
+ process.exit(1);
1706
+ }
1707
+
1708
+ let updatedConfig = loadCollectionConfig(globalOpts.configPath);
1709
+ if (!updatedConfig) {
1710
+ updatedConfig = { collections: {} };
1711
+ }
1712
+ const newVectorConfig: VectorConfigSection = {
1713
+ provider: 'qdrant',
1714
+ url: qdrantUrl,
1715
+ collection: vectorConfig?.collection || 'nano-brain',
1716
+ };
1717
+ updatedConfig.vector = newVectorConfig;
1718
+ saveCollectionConfig(globalOpts.configPath, updatedConfig);
1719
+
1720
+ console.log('✅ Switched to Qdrant provider');
1721
+ console.log(` URL: ${qdrantUrl}`);
1722
+ console.log(` Collection: ${newVectorConfig.collection}`);
1723
+ break;
1724
+ }
1725
+
1726
+ case 'cleanup': {
1727
+ const config = loadCollectionConfig(globalOpts.configPath);
1728
+ const vectorConfig = config?.vector;
1729
+ const currentProvider = vectorConfig?.provider || 'sqlite-vec';
1730
+
1731
+ if (currentProvider !== 'qdrant') {
1732
+ console.error('❌ Cannot cleanup: provider is not set to qdrant');
1733
+ console.error(` Current provider: ${currentProvider}`);
1734
+ console.error(' Run `npx nano-brain qdrant activate` first.');
1735
+ process.exit(1);
1736
+ }
1737
+
1738
+ const qdrantUrl = vectorConfig?.url || 'http://localhost:6333';
1739
+ const resolvedUrl = resolveHostUrl(qdrantUrl);
1740
+
1741
+ try {
1742
+ const healthRes = await fetch(`${resolvedUrl}/healthz`);
1743
+ if (!healthRes.ok) {
1744
+ throw new Error(`HTTP ${healthRes.status}`);
1745
+ }
1746
+ } catch {
1747
+ console.error(`❌ Qdrant is not reachable at ${resolvedUrl}.`);
1748
+ console.error(' Cannot cleanup without verifying Qdrant has vectors.');
1749
+ process.exit(1);
1750
+ }
1751
+
1752
+ let pointsCount = 0;
1753
+ try {
1754
+ const collectionRes = await fetch(`${resolvedUrl}/collections/nano-brain`);
1755
+ if (collectionRes.ok) {
1756
+ const collectionData = await collectionRes.json();
1757
+ const result = collectionData.result || collectionData;
1758
+ pointsCount = result.points_count ?? result.vectors_count ?? 0;
1759
+ }
1760
+ } catch {
1761
+ console.error('❌ Failed to check Qdrant collection');
1762
+ process.exit(1);
1763
+ }
1764
+
1765
+ if (pointsCount === 0) {
1766
+ console.error('❌ Cannot cleanup: Qdrant collection has no vectors');
1767
+ console.error(' Run `npx nano-brain qdrant migrate` first to migrate vectors.');
1768
+ process.exit(1);
1769
+ }
1770
+
1771
+ console.log(`Qdrant has ${pointsCount} vectors. Proceeding with SQLite cleanup...`);
1772
+
1773
+ const dataDir = DEFAULT_DB_DIR;
1774
+ if (!fs.existsSync(dataDir)) {
1775
+ console.log('No databases found in ' + dataDir);
1776
+ return;
1653
1777
  }
1778
+
1779
+ const sqliteFiles = fs.readdirSync(dataDir).filter(f => f.endsWith('.sqlite'));
1780
+ if (sqliteFiles.length === 0) {
1781
+ console.log('No SQLite databases found');
1782
+ return;
1783
+ }
1784
+
1785
+ const Database = (await import('better-sqlite3')).default;
1786
+ const sqliteVec = await import('sqlite-vec');
1787
+
1788
+ let cleanedCount = 0;
1789
+ let totalSpaceSaved = 0;
1790
+
1791
+ for (const sqliteFile of sqliteFiles) {
1792
+ const dbPath = path.join(dataDir, sqliteFile);
1793
+ const statBefore = fs.statSync(dbPath);
1794
+ const db = new Database(dbPath);
1795
+
1796
+ try {
1797
+ sqliteVec.load(db);
1798
+ } catch {
1799
+ console.log(`[${sqliteFile}] sqlite-vec not available, skipping`);
1800
+ db.close();
1801
+ continue;
1802
+ }
1803
+
1804
+ let hasVectorTables = false;
1805
+ try {
1806
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('vectors_vec', 'content_vectors')").all() as Array<{ name: string }>;
1807
+ hasVectorTables = tables.length > 0;
1808
+ } catch {
1809
+ db.close();
1810
+ continue;
1811
+ }
1812
+
1813
+ if (!hasVectorTables) {
1814
+ console.log(`[${sqliteFile}] no vector tables, skipping`);
1815
+ db.close();
1816
+ continue;
1817
+ }
1818
+
1819
+ try {
1820
+ db.exec('DROP TABLE IF EXISTS vectors_vec');
1821
+ db.exec('DELETE FROM content_vectors');
1822
+ db.exec('VACUUM');
1823
+ cleanedCount++;
1824
+
1825
+ const statAfter = fs.statSync(dbPath);
1826
+ const spaceSaved = statBefore.size - statAfter.size;
1827
+ totalSpaceSaved += Math.max(0, spaceSaved);
1828
+
1829
+ console.log(`[${sqliteFile}] cleaned`);
1830
+ } catch (err) {
1831
+ console.error(`[${sqliteFile}] cleanup failed:`, err);
1832
+ }
1833
+
1834
+ db.close();
1835
+ }
1836
+
1837
+ const spaceMB = (totalSpaceSaved / (1024 * 1024)).toFixed(2);
1838
+ console.log(`\n✅ Cleaned ${cleanedCount} database(s), ~${spaceMB} MB freed`);
1654
1839
  break;
1655
1840
  }
1656
1841
 
1657
1842
  default:
1658
1843
  console.error(`Unknown qdrant subcommand: ${subcommand}`);
1659
- console.error('Available: up, down, status, migrate');
1844
+ console.error('Available: up, down, status, migrate, activate, cleanup');
1660
1845
  process.exit(1);
1661
1846
  }
1662
1847
  }