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:
2025-10-17 00:38:45 -04:00
parent 4e399e00f8
commit 99e2ade3ca
7 changed files with 1115 additions and 64 deletions

View File

@@ -0,0 +1,58 @@
import { TFile } from 'obsidian';
import * as crypto from 'crypto';
/**
* Version control utilities for concurrency management
* Implements ETag-based optimistic locking for write operations
*/
export class VersionUtils {
/**
* Generate a version ID (ETag) for a file based on its modification time and size
* Format: base64(sha256(mtime + size))
*/
static generateVersionId(file: TFile): string {
const data = `${file.stat.mtime}-${file.stat.size}`;
const hash = crypto.createHash('sha256').update(data).digest('base64');
// Use URL-safe base64 and truncate to reasonable length
return hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').substring(0, 22);
}
/**
* Validate that the provided version ID matches the current file version
* Returns true if versions match (safe to proceed with write)
* Returns false if versions don't match (conflict detected)
*/
static validateVersion(file: TFile, providedVersionId: string): boolean {
const currentVersionId = this.generateVersionId(file);
return currentVersionId === providedVersionId;
}
/**
* Create a version mismatch error message
*/
static versionMismatchError(path: string, providedVersion: string, currentVersion: string): string {
return JSON.stringify({
error: 'Version mismatch (412 Precondition Failed)',
path,
message: 'The file has been modified since you last read it. Please re-read the file and try again.',
providedVersion,
currentVersion,
troubleshooting: [
'Re-read the file to get the latest versionId',
'Merge your changes with the current content',
'Retry the operation with the new versionId'
]
}, null, 2);
}
/**
* Create a response with version information
*/
static createVersionedResponse(file: TFile, data: any): any {
return {
...data,
versionId: this.generateVersionId(file),
modified: file.stat.mtime
};
}
}