feat: Phase 8 - Write Operations & Concurrency
Implement safe write operations with concurrency control, partial updates, conflict resolution, and file rename/move with automatic link updates. New Tools: - update_frontmatter: Partial frontmatter updates with concurrency control - update_sections: Line-based section edits to reduce race conditions - rename_file: File rename/move with automatic wikilink updates Enhanced Tools: - create_note: Added onConflict strategies (error, overwrite, rename) - delete_note: Added soft delete, dryRun, and concurrency control Key Features: - ETag-based optimistic locking via ifMatch parameter - Version tracking on all write operations - Conflict resolution strategies - Link integrity maintenance during file operations - Safe operations with preview and recovery options Files Created: - src/utils/version-utils.ts Files Modified: - src/tools/note-tools.ts - src/utils/frontmatter-utils.ts - src/tools/index.ts - src/types/mcp-types.ts - ROADMAP.md - CHANGELOG.md Fixes: - Fixed rename_file backlinks API issue (not available in Obsidian API) - Fixed update_frontmatter null-object error when patch is undefined
This commit is contained in:
@@ -133,6 +133,69 @@ export class FrontmatterUtils {
|
||||
return content.startsWith('---\n') || content.startsWith('---\r\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize frontmatter object to YAML string with delimiters
|
||||
* Returns the complete frontmatter block including --- delimiters
|
||||
*/
|
||||
static serializeFrontmatter(data: Record<string, any>): string {
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines: string[] = ['---'];
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle different value types
|
||||
if (Array.isArray(value)) {
|
||||
// Array format
|
||||
if (value.length === 0) {
|
||||
lines.push(`${key}: []`);
|
||||
} else {
|
||||
lines.push(`${key}:`);
|
||||
for (const item of value) {
|
||||
const itemStr = typeof item === 'string' ? item : JSON.stringify(item);
|
||||
lines.push(` - ${itemStr}`);
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
// Object format (nested)
|
||||
lines.push(`${key}:`);
|
||||
for (const [subKey, subValue] of Object.entries(value)) {
|
||||
const subValueStr = typeof subValue === 'string' ? subValue : JSON.stringify(subValue);
|
||||
lines.push(` ${subKey}: ${subValueStr}`);
|
||||
}
|
||||
} else if (typeof value === 'string') {
|
||||
// String - check if needs quoting
|
||||
const needsQuotes = value.includes(':') || value.includes('#') ||
|
||||
value.includes('[') || value.includes(']') ||
|
||||
value.includes('{') || value.includes('}') ||
|
||||
value.includes('|') || value.includes('>') ||
|
||||
value.startsWith(' ') || value.endsWith(' ');
|
||||
|
||||
if (needsQuotes) {
|
||||
// Escape quotes in the string
|
||||
const escaped = value.replace(/"/g, '\\"');
|
||||
lines.push(`${key}: "${escaped}"`);
|
||||
} else {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
// Number or boolean - direct serialization
|
||||
lines.push(`${key}: ${value}`);
|
||||
} else {
|
||||
// Fallback to JSON stringification
|
||||
lines.push(`${key}: ${JSON.stringify(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Excalidraw file metadata
|
||||
* Excalidraw files are JSON with special structure
|
||||
|
||||
Reference in New Issue
Block a user