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.
- package/package.json +1 -1
- package/src/index.ts +194 -9
package/package.json
CHANGED
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
|
}
|