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.
250 lines • 10.2 kB
JavaScript
;
/**
* Org-mode parser for extracting structure from .org files
*/
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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ORGParser = void 0;
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const types_1 = require("../models/types");
class ORGParser {
async parse(filePath, content, notesRoot) {
// Convert Buffer to string if needed
const textContent = typeof content === 'string' ? content : content.toString('utf-8');
// Extract title from #+TITLE directive or filename
const title = this.extractTitle(textContent) || path.basename(filePath, path.extname(filePath));
// Extract headings
const headings = this.extractHeadings(textContent);
// Extract links
const outgoingLinks = this.extractLinks(textContent, filePath);
// Extract tags
const tags = this.extractTags(textContent);
// Calculate word count (excluding org syntax)
const cleanContent = this.cleanOrgSyntax(textContent);
const wordCount = cleanContent.split(/\s+/).filter(word => word.length > 0).length;
// Get file modification time
const stats = await fs.promises.stat(filePath);
const lastModified = stats.mtime;
// Calculate relative path
const relativePath = path.relative(notesRoot, filePath);
// Extract metadata from org directives
const frontmatter = this.extractMetadata(textContent);
return {
path: filePath,
relativePath,
title,
headings,
outgoingLinks,
tags,
frontmatter,
lastModified,
wordCount
};
}
extractTitle(content) {
const titleMatch = content.match(/^#\+TITLE:\s*(.+)$/im);
return titleMatch ? titleMatch[1].trim() : null;
}
extractHeadings(content) {
const headings = [];
const lines = content.split('\n');
let lineNumber = 0;
for (const line of lines) {
lineNumber++;
// Match org headings: *, **, ***, etc.
const headingMatch = line.match(/^(\*+)\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
let text = headingMatch[2].trim();
// Remove org-specific syntax from heading text
// Remove TODO/DONE keywords
text = text.replace(/^(TODO|DONE|NEXT|WAITING|SOMEDAY)\s+/, '');
// Remove priority indicators [#A], [#B], [#C]
text = text.replace(/\[#[ABC]\]\s*/, '');
// Remove trailing tags :tag1:tag2:
text = text.replace(/\s+:[\w:]+:$/, '').trim();
headings.push({
level: Math.min(level, 6),
text,
lineNumber,
slug: this.createSlug(text)
});
}
}
return headings;
}
extractLinks(content, sourcePath) {
const links = [];
const lines = content.split('\n');
lines.forEach((line, lineIndex) => {
// Pattern 1: [[link][description]] or [[link]]
const orgLinkPattern = /\[\[([^\]]+)\](?:\[([^\]]+)\])?\]/g;
let match;
while ((match = orgLinkPattern.exec(line)) !== null) {
const target = match[1];
const description = match[2] || target;
// Skip external URLs for now
if (!target.startsWith('http://') && !target.startsWith('https://') &&
!target.startsWith('file://') && !target.startsWith('mailto:')) {
links.push({
sourcePath,
targetPath: null,
linkType: types_1.LinkType.WIKI,
linkText: target,
context: this.extractContext(line, match.index, match.index + match[0].length),
lineNumber: lineIndex + 1,
isBroken: false
});
}
}
// Pattern 2: Plain URLs
const urlPattern = /https?:\/\/[^\s<>"\{\}\|\^\[\]`]+/g;
while ((match = urlPattern.exec(line)) !== null) {
let url = match[0];
// Remove trailing punctuation
url = url.replace(/[.,;!?]+$/, '');
links.push({
sourcePath,
targetPath: null,
linkType: types_1.LinkType.MARKDOWN,
linkText: url,
context: this.extractContext(line, match.index, match.index + url.length),
lineNumber: lineIndex + 1,
isBroken: false
});
}
});
return links;
}
extractTags(content) {
const tags = new Set();
// Pattern 1: Heading tags :tag1:tag2:
const headingTagPattern = /^\*+\s+.*?:([\w:]+):$/gm;
let match;
while ((match = headingTagPattern.exec(content)) !== null) {
const tagString = match[1];
const headingTags = tagString.split(':').filter(tag => tag.length > 0);
headingTags.forEach(tag => tags.add(tag));
}
// Pattern 2: File-level tags #+TAGS: tag1 tag2 tag3
const fileTagPattern = /^#\+TAGS:\s*(.+)$/im;
const fileTagMatch = content.match(fileTagPattern);
if (fileTagMatch) {
const fileTags = fileTagMatch[1].split(/\s+/).filter(tag => tag.length > 0);
fileTags.forEach(tag => tags.add(tag));
}
// Pattern 3: Hashtags in text #tag
const hashtagPattern = /(?:^|(?<=\s))#([a-zA-Z0-9_-]+)/g;
while ((match = hashtagPattern.exec(content)) !== null) {
tags.add(match[1]);
}
return tags;
}
extractMetadata(content) {
const frontmatter = {};
// Extract org directives
const directivePattern = /^#\+(\w+):\s*(.+)$/gm;
let match;
while ((match = directivePattern.exec(content)) !== null) {
const key = match[1].toLowerCase();
const value = match[2].trim();
// Handle special cases
switch (key) {
case 'tags':
frontmatter.tags = value.split(/\s+/);
break;
case 'date':
case 'created':
case 'modified':
// Try to parse as date
try {
frontmatter[key] = new Date(value);
}
catch {
frontmatter[key] = value;
}
break;
default:
frontmatter[key] = value;
}
}
return frontmatter;
}
cleanOrgSyntax(content) {
let cleaned = content;
// Remove org directives
cleaned = cleaned.replace(/^#\+\w+:.*$/gm, '');
// Remove drawer content (:PROPERTIES: ... :END:)
cleaned = cleaned.replace(/:PROPERTIES:\s*\n(?:.*\n)*?:END:\s*\n/g, '');
// Convert org links to plain text
cleaned = cleaned.replace(/\[\[([^\]]+)\](?:\[([^\]]+)\])?\]/g, (match, target, description) => {
return description || target;
});
// Remove org markup
cleaned = cleaned.replace(/\*([^*]+)\*/g, '$1'); // *bold*
cleaned = cleaned.replace(/\/([^/]+)\//g, '$1'); // /italic/
cleaned = cleaned.replace(/_([^_]+)_/g, '$1'); // _underline_
cleaned = cleaned.replace(/=([^=]+)=/g, '$1'); // =code=
cleaned = cleaned.replace(/~([^~]+)~/g, '$1'); // ~verbatim~
// Remove heading stars
cleaned = cleaned.replace(/^\*+\s+/gm, '');
return cleaned;
}
extractContext(line, matchStart, matchEnd, contextChars = 50) {
const start = Math.max(0, matchStart - contextChars);
const end = Math.min(line.length, matchEnd + contextChars);
let context = line.slice(start, end);
context = context.replace(/\s+/g, ' ').trim();
if (start > 0)
context = '...' + context;
if (end < line.length)
context = context + '...';
return context;
}
createSlug(text) {
let slug = text.toLowerCase().replace(/[^\w\s-]/g, '');
slug = slug.replace(/[-\s]+/g, '-');
return slug.replace(/^-+|-+$/g, '');
}
supports(extension) {
return this.getSupportedExtensions().includes(extension.toLowerCase());
}
getSupportedExtensions() {
return ['.org'];
}
}
exports.ORGParser = ORGParser;
//# sourceMappingURL=ORGParser.js.map