@mauricio.wolff/mcp-obsidian 0.4.1 → 0.5.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/README.md +29 -28
- package/dist/server.js +489 -0
- package/dist/src/filesystem.js +525 -0
- package/dist/src/frontmatter.js +104 -0
- package/dist/src/pathfilter.js +57 -0
- package/dist/src/search.js +105 -0
- package/dist/src/types.js +1 -0
- package/package.json +18 -13
- package/server.ts +3 -3
- package/src/filesystem.ts +0 -602
- package/src/frontmatter.ts +0 -124
- package/src/pathfilter.ts +0 -72
- package/src/search.ts +0 -97
- package/src/types.ts +0 -119
package/src/frontmatter.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import matter from 'gray-matter';
|
|
2
|
-
import type { ParsedNote, FrontmatterValidationResult } from './types.js';
|
|
3
|
-
|
|
4
|
-
export class FrontmatterHandler {
|
|
5
|
-
parse(content: string): ParsedNote {
|
|
6
|
-
try {
|
|
7
|
-
const parsed = matter(content);
|
|
8
|
-
return {
|
|
9
|
-
frontmatter: parsed.data,
|
|
10
|
-
content: parsed.content,
|
|
11
|
-
originalContent: content
|
|
12
|
-
};
|
|
13
|
-
} catch (error) {
|
|
14
|
-
// If parsing fails, treat as content without frontmatter
|
|
15
|
-
return {
|
|
16
|
-
frontmatter: {},
|
|
17
|
-
content: content,
|
|
18
|
-
originalContent: content
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
stringify(frontmatterData: Record<string, any>, content: string): string {
|
|
24
|
-
try {
|
|
25
|
-
// If no frontmatter, return content as-is
|
|
26
|
-
if (!frontmatterData || Object.keys(frontmatterData).length === 0) {
|
|
27
|
-
return content;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return matter.stringify(content, frontmatterData);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
throw new Error(`Failed to stringify frontmatter: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
validate(frontmatterData: Record<string, any>): FrontmatterValidationResult {
|
|
37
|
-
const result: FrontmatterValidationResult = {
|
|
38
|
-
isValid: true,
|
|
39
|
-
errors: [],
|
|
40
|
-
warnings: []
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
// Test if the frontmatter can be serialized to valid YAML using Bun's YAML
|
|
45
|
-
Bun.YAML.stringify(frontmatterData);
|
|
46
|
-
} catch (error) {
|
|
47
|
-
result.isValid = false;
|
|
48
|
-
result.errors.push(`Invalid YAML structure: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check for problematic values
|
|
52
|
-
this.checkForProblematicValues(frontmatterData, result, '');
|
|
53
|
-
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private checkForProblematicValues(
|
|
58
|
-
obj: any,
|
|
59
|
-
result: FrontmatterValidationResult,
|
|
60
|
-
path: string
|
|
61
|
-
): void {
|
|
62
|
-
if (obj === null || obj === undefined) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (typeof obj === 'function') {
|
|
67
|
-
result.errors.push(`Functions are not allowed in frontmatter at path: ${path}`);
|
|
68
|
-
result.isValid = false;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (typeof obj === 'symbol') {
|
|
73
|
-
result.errors.push(`Symbols are not allowed in frontmatter at path: ${path}`);
|
|
74
|
-
result.isValid = false;
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (obj instanceof Date) {
|
|
79
|
-
// Dates are fine, but warn if they're invalid
|
|
80
|
-
if (isNaN(obj.getTime())) {
|
|
81
|
-
result.warnings.push(`Invalid date at path: ${path}`);
|
|
82
|
-
}
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (Array.isArray(obj)) {
|
|
87
|
-
obj.forEach((item, index) => {
|
|
88
|
-
this.checkForProblematicValues(item, result, `${path}[${index}]`);
|
|
89
|
-
});
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (typeof obj === 'object' && obj !== null) {
|
|
94
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
95
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
96
|
-
|
|
97
|
-
// Check for problematic keys
|
|
98
|
-
if (typeof key !== 'string') {
|
|
99
|
-
result.errors.push(`Non-string keys are not allowed: ${key}`);
|
|
100
|
-
result.isValid = false;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this.checkForProblematicValues(value, result, currentPath);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
extractFrontmatter(content: string): Record<string, any> {
|
|
109
|
-
const parsed = this.parse(content);
|
|
110
|
-
return parsed.frontmatter;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
updateFrontmatter(content: string, updates: Record<string, any>): string {
|
|
114
|
-
const parsed = this.parse(content);
|
|
115
|
-
const updatedFrontmatter = { ...parsed.frontmatter, ...updates };
|
|
116
|
-
|
|
117
|
-
const validation = this.validate(updatedFrontmatter);
|
|
118
|
-
if (!validation.isValid) {
|
|
119
|
-
throw new Error(`Invalid frontmatter: ${validation.errors.join(', ')}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return this.stringify(updatedFrontmatter, parsed.content);
|
|
123
|
-
}
|
|
124
|
-
}
|
package/src/pathfilter.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { PathFilterConfig } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export class PathFilter {
|
|
4
|
-
private ignoredPatterns: string[];
|
|
5
|
-
private allowedExtensions: string[];
|
|
6
|
-
|
|
7
|
-
constructor(config?: Partial<PathFilterConfig>) {
|
|
8
|
-
this.ignoredPatterns = [
|
|
9
|
-
'.obsidian/**',
|
|
10
|
-
'.git/**',
|
|
11
|
-
'node_modules/**',
|
|
12
|
-
'.DS_Store',
|
|
13
|
-
'Thumbs.db',
|
|
14
|
-
...config?.ignoredPatterns || []
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
this.allowedExtensions = [
|
|
18
|
-
'.md',
|
|
19
|
-
'.markdown',
|
|
20
|
-
'.txt',
|
|
21
|
-
...config?.allowedExtensions || []
|
|
22
|
-
];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private simpleGlobMatch(pattern: string, path: string): boolean {
|
|
26
|
-
// Convert glob pattern to regex
|
|
27
|
-
// Handle ** (any number of directories)
|
|
28
|
-
let regexPattern = pattern
|
|
29
|
-
.replace(/\*\*/g, '.*') // ** matches any number of directories
|
|
30
|
-
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
31
|
-
.replace(/\?/g, '[^/]') // ? matches single character except /
|
|
32
|
-
.replace(/\./g, '\\.'); // Escape dots
|
|
33
|
-
|
|
34
|
-
// Ensure we match the full path
|
|
35
|
-
regexPattern = '^' + regexPattern + '$';
|
|
36
|
-
|
|
37
|
-
const regex = new RegExp(regexPattern);
|
|
38
|
-
return regex.test(path);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
isAllowed(path: string): boolean {
|
|
42
|
-
// Normalize path separators
|
|
43
|
-
const normalizedPath = path.replace(/\\/g, '/');
|
|
44
|
-
|
|
45
|
-
// Check if path matches any ignored pattern
|
|
46
|
-
for (const pattern of this.ignoredPatterns) {
|
|
47
|
-
if (this.simpleGlobMatch(pattern, normalizedPath)) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// For files, check extension if allowedExtensions is configured
|
|
53
|
-
if (this.allowedExtensions.length > 0 && this.isFile(normalizedPath)) {
|
|
54
|
-
const hasAllowedExtension = this.allowedExtensions.some(ext =>
|
|
55
|
-
normalizedPath.toLowerCase().endsWith(ext.toLowerCase())
|
|
56
|
-
);
|
|
57
|
-
if (!hasAllowedExtension) {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private isFile(path: string): boolean {
|
|
66
|
-
return path.includes('.') && !path.endsWith('/');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
filterPaths(paths: string[]): string[] {
|
|
70
|
-
return paths.filter(path => this.isAllowed(path));
|
|
71
|
-
}
|
|
72
|
-
}
|
package/src/search.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { join } from 'path';
|
|
2
|
-
import type { PathFilter } from './pathfilter.js';
|
|
3
|
-
import type { SearchParams, SearchResult } from './types.js';
|
|
4
|
-
|
|
5
|
-
export class SearchService {
|
|
6
|
-
constructor(
|
|
7
|
-
private vaultPath: string,
|
|
8
|
-
private pathFilter: PathFilter
|
|
9
|
-
) {}
|
|
10
|
-
|
|
11
|
-
async search(params: SearchParams): Promise<SearchResult[]> {
|
|
12
|
-
const {
|
|
13
|
-
query,
|
|
14
|
-
limit = 5,
|
|
15
|
-
searchContent = true,
|
|
16
|
-
searchFrontmatter = false,
|
|
17
|
-
caseSensitive = false
|
|
18
|
-
} = params;
|
|
19
|
-
|
|
20
|
-
if (!query || query.trim().length === 0) {
|
|
21
|
-
throw new Error('Search query cannot be empty');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const results: SearchResult[] = [];
|
|
25
|
-
const glob = new Bun.Glob("**/*.md");
|
|
26
|
-
const maxLimit = Math.min(limit, 20);
|
|
27
|
-
|
|
28
|
-
for await (const relativePath of glob.scan(this.vaultPath)) {
|
|
29
|
-
if (!this.pathFilter.isAllowed(relativePath)) continue;
|
|
30
|
-
if (results.length >= maxLimit) break;
|
|
31
|
-
|
|
32
|
-
const fullPath = join(this.vaultPath, relativePath);
|
|
33
|
-
const file = Bun.file(fullPath);
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const content = await file.text();
|
|
37
|
-
let searchableText = '';
|
|
38
|
-
|
|
39
|
-
// Prepare search text based on options
|
|
40
|
-
if (searchContent && searchFrontmatter) {
|
|
41
|
-
searchableText = content;
|
|
42
|
-
} else if (searchContent) {
|
|
43
|
-
// Remove frontmatter from search
|
|
44
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
45
|
-
searchableText = frontmatterMatch ? content.slice(frontmatterMatch[0].length) : content;
|
|
46
|
-
} else if (searchFrontmatter) {
|
|
47
|
-
// Search only frontmatter
|
|
48
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
49
|
-
searchableText = frontmatterMatch ? frontmatterMatch[1] : '';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const searchIn = caseSensitive ? searchableText : searchableText.toLowerCase();
|
|
53
|
-
const searchQuery = caseSensitive ? query : query.toLowerCase();
|
|
54
|
-
|
|
55
|
-
const index = searchIn.indexOf(searchQuery);
|
|
56
|
-
if (index !== -1) {
|
|
57
|
-
// Extract excerpt around first match
|
|
58
|
-
const excerptStart = Math.max(0, index - 50);
|
|
59
|
-
const excerptEnd = Math.min(searchableText.length, index + searchQuery.length + 50);
|
|
60
|
-
let excerpt = searchableText.slice(excerptStart, excerptEnd).trim();
|
|
61
|
-
|
|
62
|
-
// Add ellipsis if excerpt is truncated
|
|
63
|
-
if (excerptStart > 0) excerpt = '...' + excerpt;
|
|
64
|
-
if (excerptEnd < searchableText.length) excerpt = excerpt + '...';
|
|
65
|
-
|
|
66
|
-
// Count total matches
|
|
67
|
-
let matchCount = 0;
|
|
68
|
-
let searchIndex = 0;
|
|
69
|
-
while ((searchIndex = searchIn.indexOf(searchQuery, searchIndex)) !== -1) {
|
|
70
|
-
matchCount++;
|
|
71
|
-
searchIndex += searchQuery.length;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Find line number of first match
|
|
75
|
-
const lines = searchableText.slice(0, index).split('\n');
|
|
76
|
-
const lineNumber = lines.length;
|
|
77
|
-
|
|
78
|
-
// Extract title from filename
|
|
79
|
-
const title = relativePath.split('/').pop()?.replace(/\.md$/, '') || relativePath;
|
|
80
|
-
|
|
81
|
-
results.push({
|
|
82
|
-
path: relativePath,
|
|
83
|
-
title: title,
|
|
84
|
-
excerpt: excerpt,
|
|
85
|
-
matchCount: matchCount,
|
|
86
|
-
lineNumber: lineNumber
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
} catch (error) {
|
|
90
|
-
// Skip files that can't be read
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return results;
|
|
96
|
-
}
|
|
97
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
export interface ParsedNote {
|
|
2
|
-
frontmatter: Record<string, any>;
|
|
3
|
-
content: string;
|
|
4
|
-
originalContent: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface NoteWriteParams {
|
|
8
|
-
path: string;
|
|
9
|
-
content: string;
|
|
10
|
-
frontmatter?: Record<string, any>;
|
|
11
|
-
mode?: 'overwrite' | 'append' | 'prepend';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface DeleteNoteParams {
|
|
15
|
-
path: string;
|
|
16
|
-
confirmPath: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface DeleteResult {
|
|
20
|
-
success: boolean;
|
|
21
|
-
path: string;
|
|
22
|
-
message: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface DirectoryListing {
|
|
26
|
-
files: string[];
|
|
27
|
-
directories: string[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface FrontmatterValidationResult {
|
|
31
|
-
isValid: boolean;
|
|
32
|
-
errors: string[];
|
|
33
|
-
warnings: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface PathFilterConfig {
|
|
37
|
-
ignoredPatterns: string[];
|
|
38
|
-
allowedExtensions: string[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Search types
|
|
42
|
-
export interface SearchParams {
|
|
43
|
-
query: string;
|
|
44
|
-
limit?: number;
|
|
45
|
-
searchContent?: boolean;
|
|
46
|
-
searchFrontmatter?: boolean;
|
|
47
|
-
caseSensitive?: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SearchResult {
|
|
51
|
-
path: string;
|
|
52
|
-
title: string;
|
|
53
|
-
excerpt: string;
|
|
54
|
-
matchCount: number;
|
|
55
|
-
lineNumber?: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Move types
|
|
59
|
-
export interface MoveNoteParams {
|
|
60
|
-
oldPath: string;
|
|
61
|
-
newPath: string;
|
|
62
|
-
overwrite?: boolean;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface MoveResult {
|
|
66
|
-
success: boolean;
|
|
67
|
-
oldPath: string;
|
|
68
|
-
newPath: string;
|
|
69
|
-
message: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Batch read types
|
|
73
|
-
export interface BatchReadParams {
|
|
74
|
-
paths: string[];
|
|
75
|
-
includeContent?: boolean;
|
|
76
|
-
includeFrontmatter?: boolean;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface BatchReadResult {
|
|
80
|
-
successful: Array<{
|
|
81
|
-
path: string;
|
|
82
|
-
frontmatter?: Record<string, any>;
|
|
83
|
-
content?: string;
|
|
84
|
-
}>;
|
|
85
|
-
failed: Array<{
|
|
86
|
-
path: string;
|
|
87
|
-
error: string;
|
|
88
|
-
}>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Update frontmatter types
|
|
92
|
-
export interface UpdateFrontmatterParams {
|
|
93
|
-
path: string;
|
|
94
|
-
frontmatter: Record<string, any>;
|
|
95
|
-
merge?: boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Note info types
|
|
99
|
-
export interface NoteInfo {
|
|
100
|
-
path: string;
|
|
101
|
-
size: number;
|
|
102
|
-
modified: number; // timestamp
|
|
103
|
-
hasFrontmatter: boolean;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Tag management types
|
|
107
|
-
export interface TagManagementParams {
|
|
108
|
-
path: string;
|
|
109
|
-
operation: 'add' | 'remove' | 'list';
|
|
110
|
-
tags?: string[];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface TagManagementResult {
|
|
114
|
-
path: string;
|
|
115
|
-
operation: string;
|
|
116
|
-
tags: string[];
|
|
117
|
-
success: boolean;
|
|
118
|
-
message?: string;
|
|
119
|
-
}
|