brain-mcp
Version:
Brain MCP Server - Semantic knowledge base access for Claude Code via Model Context Protocol. Provides intelligent search and navigation of files from multiple locations through native MCP tools.
344 lines • 13.6 kB
JavaScript
;
/**
* Brain CLI V2 - Multi-location file support
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = require("commander");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const FileRegistry_1 = require("../storage/FileRegistry");
const VectorStore_1 = require("../embedding/VectorStore");
const EmbeddingService_1 = require("../embedding/EmbeddingService");
const ParserFactory_1 = require("../parser/ParserFactory");
const ChunkingService_1 = require("../parser/ChunkingService");
const fast_glob_1 = __importDefault(require("fast-glob"));
const program = new commander_1.Command();
async function loadConfig() {
const configPath = path.join(process.env.HOME || '~', '.brain', 'config.json');
if (!fs.existsSync(configPath)) {
// Create minimal config if it doesn't exist
return {};
}
try {
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
catch {
return {};
}
}
async function ensureConfigDir() {
const configDir = path.join(process.env.HOME || '~', '.brain');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
return configDir;
}
/**
* Add files command with multi-location support
*/
async function addFiles(targetPath, options) {
const config = await loadConfig();
const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
if (!apiKey) {
console.error('❌ OpenAI API key not found. Set OPENAI_API_KEY environment variable or add to ~/.brain/config.json');
process.exit(1);
}
const configDir = await ensureConfigDir();
// Resolve absolute path
const absolutePath = path.resolve(targetPath);
if (!fs.existsSync(absolutePath)) {
console.error(`❌ Path not found: ${absolutePath}`);
process.exit(1);
}
console.log('🧠 Brain Add Files');
console.log(`📂 Target: ${absolutePath}`);
console.log('');
// Initialize services
const fileRegistry = new FileRegistry_1.FileRegistry(configDir);
await fileRegistry.initialize();
const vectorStore = new VectorStore_1.VectorStore(configDir, fileRegistry);
const embeddingService = new EmbeddingService_1.EmbeddingService(apiKey);
const parserFactory = new ParserFactory_1.ParserFactory();
let supportedExtensions = parserFactory.getSupportedExtensions();
// Filter by types if specified
if (options.types) {
const requestedTypes = options.types.split(',').map(t => t.trim().startsWith('.') ? t : '.' + t);
supportedExtensions = supportedExtensions.filter(ext => requestedTypes.includes(ext));
console.log(`🔍 File types: ${supportedExtensions.join(', ')}`);
}
// Find files
let files;
if (fs.statSync(absolutePath).isDirectory()) {
const patterns = supportedExtensions.map(ext => `**/*${ext}`);
files = await (0, fast_glob_1.default)(patterns, {
cwd: absolutePath,
absolute: true,
ignore: ['**/node_modules/**', '**/.*/**']
});
}
else {
// Single file
const ext = path.extname(absolutePath).toLowerCase();
if (!supportedExtensions.includes(ext)) {
console.error(`❌ Unsupported file type: ${ext}`);
console.log(`Supported types: ${supportedExtensions.join(', ')}`);
process.exit(1);
}
files = [absolutePath];
}
if (files.length === 0) {
console.log('❌ No supported files found.');
return;
}
console.log(`📁 Found ${files.length} file(s) to add:`);
files.slice(0, 10).forEach(file => {
console.log(` 📄 ${path.relative(process.cwd(), file)}`);
});
if (files.length > 10) {
console.log(` ... and ${files.length - 10} more`);
}
console.log('');
// Process files
console.log('📊 Processing files...');
let processed = 0;
let added = 0;
for (const filePath of files) {
try {
// Check if file already exists
const existingFile = await fileRegistry.getFileByPath(filePath);
const stats = fs.statSync(filePath);
if (existingFile && existingFile.lastModified >= stats.mtime) {
console.log(` ⏭️ Skipping ${path.basename(filePath)} (not modified)`);
continue;
}
// Parse file
const parser = parserFactory.getParser(filePath);
if (!parser) {
console.log(` ⚠️ No parser for ${path.basename(filePath)}`);
continue;
}
const content = filePath.endsWith('.pdf')
? fs.readFileSync(filePath)
: fs.readFileSync(filePath, 'utf-8');
const parseResult = await parser.parse(filePath, content, configDir);
if (!parseResult) {
console.log(` ⚠️ Skipping ${path.basename(filePath)} (no content)`);
continue;
}
// Create display name (relative to current directory or absolute path)
const displayName = path.relative(process.cwd(), filePath) || path.basename(filePath);
const fileType = path.extname(filePath).substring(1).toUpperCase();
// Add or update file in registry
let fileRecord;
if (existingFile) {
await fileRegistry.updateFileModified(existingFile.id, stats.mtime);
fileRecord = existingFile;
fileRecord.lastModified = stats.mtime;
}
else {
fileRecord = await fileRegistry.addFile(filePath, displayName, fileType);
}
// Create chunks using the chunking service
// For PDFs, we need to re-extract the text since we can't pass binary data
let textContent;
if (filePath.endsWith('.pdf')) {
// Re-extract text from PDF using pdftotext
const { execSync } = require('child_process');
textContent = execSync(`pdftotext -layout "${filePath}" -`, {
encoding: 'utf-8',
maxBuffer: 50 * 1024 * 1024
});
}
else {
textContent = content;
}
const chunks = ChunkingService_1.ChunkingService.createChunks(textContent, parseResult.title, parseResult.headings, filePath);
// Add chunks to vector store
await vectorStore.addFileChunks(fileRecord, chunks, embeddingService);
processed++;
added++;
if (processed % 5 === 0) {
console.log(` 📝 Processed ${processed} files...`);
}
}
catch (error) {
console.error(`❌ Failed to add ${path.basename(filePath)}: ${error.message}`);
}
}
// Save vector store
await vectorStore.saveToDisk();
console.log('');
console.log('✅ Files added successfully!');
console.log(`📈 Added: ${added} files`);
const stats = await vectorStore.getStats();
console.log(`📊 Total: ${stats.totalFiles} files, ${stats.totalDocuments} chunks`);
await fileRegistry.close();
}
/**
* Remove files command
*/
async function removeFiles(targetPath) {
const configDir = await ensureConfigDir();
// Initialize services
const fileRegistry = new FileRegistry_1.FileRegistry(configDir);
await fileRegistry.initialize();
const vectorStore = new VectorStore_1.VectorStore(configDir, fileRegistry);
console.log('🧠 Brain Remove Files');
console.log(`📂 Target: ${targetPath}`);
console.log('');
// Try to find file by display name or path
let fileRecord = await fileRegistry.getFileByDisplayName(targetPath);
if (!fileRecord) {
const absolutePath = path.resolve(targetPath);
fileRecord = await fileRegistry.getFileByPath(absolutePath);
}
if (!fileRecord) {
console.error(`❌ File not found: ${targetPath}`);
process.exit(1);
}
console.log(`🗑️ Removing: ${fileRecord.displayName}`);
// Remove from vector store and registry
await vectorStore.removeFile(fileRecord.id);
await vectorStore.saveToDisk();
console.log('✅ File removed successfully!');
const stats = await vectorStore.getStats();
console.log(`📊 Remaining: ${stats.totalFiles} files, ${stats.totalDocuments} chunks`);
await fileRegistry.close();
}
/**
* Status command
*/
async function showStatus() {
const configDir = await ensureConfigDir();
const config = await loadConfig();
const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
// Initialize services
const fileRegistry = new FileRegistry_1.FileRegistry(configDir);
await fileRegistry.initialize();
const vectorStore = new VectorStore_1.VectorStore(configDir, fileRegistry);
const stats = await vectorStore.getStats();
const files = await fileRegistry.getAllFiles();
console.log('🧠 Brain Status');
console.log('');
console.log(`📂 Config: ${configDir}`);
console.log(`✅ Database: ${files.length > 0 ? 'Connected' : 'Empty'}`);
console.log(`🔑 OpenAI API: ${apiKey ? '✅ Configured' : '❌ Not configured'}`);
console.log(`📊 Vector Store: ${stats.totalFiles} files, ${stats.totalDocuments} chunks`);
if (files.length > 0) {
console.log('');
console.log('📁 File Types:');
const typeCount = files.reduce((acc, file) => {
acc[file.fileType] = (acc[file.fileType] || 0) + 1;
return acc;
}, {});
Object.entries(typeCount).forEach(([type, count]) => {
console.log(` • ${type}: ${count}`);
});
}
await fileRegistry.close();
}
/**
* Clear command
*/
async function clearAll() {
const configDir = await ensureConfigDir();
console.log('🧠 Brain Clear');
console.log('⚠️ This will remove all indexed files and rebuild the vector store.');
console.log('');
// Remove database and vector files
const dbPath = path.join(configDir, 'brain-registry.db');
const vectorPath = path.join(configDir, '.brain-vectors-v2.json');
try {
if (fs.existsSync(dbPath)) {
fs.unlinkSync(dbPath);
console.log('✅ Removed file registry');
}
if (fs.existsSync(vectorPath)) {
fs.unlinkSync(vectorPath);
console.log('✅ Removed vector store');
}
console.log('');
console.log('🎉 Brain cleared successfully!');
console.log('Run "brain add <path>" to start adding files.');
}
catch (error) {
console.error('❌ Failed to clear:', error.message);
process.exit(1);
}
}
// Set up CLI commands
program
.name('brain')
.description('Brain MCP - Semantic knowledge base for your files')
.version('2.0.0');
program
.command('setup')
.description('Interactive setup for Brain MCP configuration')
.action(async () => {
try {
const setupPath = path.join(__dirname, '..', 'setup.js');
const { setupBrain } = require(setupPath);
await setupBrain();
}
catch (error) {
console.error('❌ Setup failed:', error.message);
process.exit(1);
}
});
program
.command('add <path>')
.description('Add files to the Brain knowledge base')
.option('-t, --types <types>', 'Comma-separated list of file types to include (e.g., pdf,txt,org)')
.action(addFiles);
program
.command('remove <path>')
.description('Remove files from the Brain knowledge base')
.action(removeFiles);
program
.command('status')
.description('Show Brain configuration and status')
.action(showStatus);
program
.command('clear')
.description('Clear and rebuild all vector embeddings')
.action(clearAll);
// Parse command line
program.parse(process.argv);
//# sourceMappingURL=brain.js.map