Files
obsidian-mcp-server/src/utils/glob-utils.ts
Bill aff7c6bd0a feat: Phase 4 - Enhanced List Operations (v3.0.0)
- Replace list_notes with powerful new list tool
- Add recursive directory traversal
- Implement glob pattern filtering (*, **, ?, [abc], {a,b})
- Add cursor-based pagination for large result sets
- Support frontmatter summary extraction using metadata cache
- Add type filtering (files, directories, any)
- Create GlobUtils for pattern matching
- Add new types: FrontmatterSummary, FileMetadataWithFrontmatter, ListResult
- Update version to 3.0.0 (breaking change)
- Add comprehensive documentation and changelog
- Add Phase 10: UI Notifications to roadmap

BREAKING CHANGE: list_notes tool removed, replaced with list tool.
Migration: Replace list_notes({ path }) with list({ path }).
Response structure now wrapped in ListResult object.
2025-10-16 23:10:31 -04:00

155 lines
3.5 KiB
TypeScript

/**
* Glob pattern matching utilities for filtering files and folders
* Supports *, **, ?, and negation patterns
*/
export class GlobUtils {
/**
* Convert a glob pattern to a regular expression
* Supports:
* - * matches any characters except /
* - ** matches any characters including /
* - ? matches a single character except /
* - [abc] matches any character in the set
* - {a,b} matches any of the alternatives
*/
private static globToRegex(pattern: string): RegExp {
let regexStr = '^';
let i = 0;
while (i < pattern.length) {
const char = pattern[i];
switch (char) {
case '*':
// Check for **
if (pattern[i + 1] === '*') {
// ** matches everything including /
regexStr += '.*';
i += 2;
// Skip optional trailing /
if (pattern[i] === '/') {
regexStr += '/?';
i++;
}
} else {
// * matches anything except /
regexStr += '[^/]*';
i++;
}
break;
case '?':
// ? matches a single character except /
regexStr += '[^/]';
i++;
break;
case '[':
// Character class
const closeIdx = pattern.indexOf(']', i);
if (closeIdx === -1) {
// No closing bracket, treat as literal
regexStr += '\\[';
i++;
} else {
regexStr += '[' + pattern.substring(i + 1, closeIdx) + ']';
i = closeIdx + 1;
}
break;
case '{':
// Alternatives {a,b,c}
const closeIdx2 = pattern.indexOf('}', i);
if (closeIdx2 === -1) {
// No closing brace, treat as literal
regexStr += '\\{';
i++;
} else {
const alternatives = pattern.substring(i + 1, closeIdx2).split(',');
regexStr += '(' + alternatives.map(alt =>
this.escapeRegex(alt)
).join('|') + ')';
i = closeIdx2 + 1;
}
break;
case '/':
case '.':
case '(':
case ')':
case '+':
case '^':
case '$':
case '|':
case '\\':
// Escape special regex characters
regexStr += '\\' + char;
i++;
break;
default:
regexStr += char;
i++;
}
}
regexStr += '$';
return new RegExp(regexStr);
}
private static escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Check if a path matches a glob pattern
*/
static matches(path: string, pattern: string): boolean {
const regex = this.globToRegex(pattern);
return regex.test(path);
}
/**
* Check if a path matches any of the include patterns
* If no includes are specified, returns true
*/
static matchesIncludes(path: string, includes?: string[]): boolean {
if (!includes || includes.length === 0) {
return true;
}
return includes.some(pattern => this.matches(path, pattern));
}
/**
* Check if a path matches any of the exclude patterns
* If no excludes are specified, returns false
*/
static matchesExcludes(path: string, excludes?: string[]): boolean {
if (!excludes || excludes.length === 0) {
return false;
}
return excludes.some(pattern => this.matches(path, pattern));
}
/**
* Check if a path should be included based on include and exclude patterns
* Returns true if the path matches includes and doesn't match excludes
*/
static shouldInclude(path: string, includes?: string[], excludes?: string[]): boolean {
// Must match includes (if specified)
if (!this.matchesIncludes(path, includes)) {
return false;
}
// Must not match excludes (if specified)
if (this.matchesExcludes(path, excludes)) {
return false;
}
return true;
}
}