Release v1.1.0: Phase 1.1 - Path Normalization & Error Handling
- Add PathUtils for cross-platform path normalization and validation - Add ErrorMessages with context-aware, actionable error messages - Update all tool implementations with enhanced path handling - Improve tool descriptions for AI agents with detailed guidance - Add Jest testing infrastructure with 43 passing tests - Add comprehensive documentation (Tool Selection Guide, error improvements) - Fix cross-platform path issues (Windows backslashes, case sensitivity) - Fix delete folder error message (clear 'cannot delete folders' message) - Fix parent folder detection with specific error messages - All changes backward compatible with v1.0.0 New files: - src/utils/path-utils.ts - Path normalization utilities - src/utils/error-messages.ts - Enhanced error messages - tests/__mocks__/obsidian.ts - Mock Obsidian API - tests/path-utils.test.ts - 43 unit tests - tests/README.md - Testing guide - jest.config.js - Jest configuration - docs/TOOL_SELECTION_GUIDE.md - Comprehensive tool guide - docs/ERROR_MESSAGE_IMPROVEMENTS.md - Error message documentation - docs/TOOL_DESCRIPTION_IMPROVEMENTS.md - AI agent improvements - PHASE_1.1_IMPLEMENTATION.md - Implementation summary - RELEASE_NOTES_v1.1.0.md - Release notes Updated: - CHANGELOG.md - Add v1.1.0 entry - ROADMAP.md - Mark Phase 1.1 complete, add Phase 1.5 proposal - manifest.json - Bump to v1.1.0 - package.json - Bump to v1.1.0, add test scripts - src/tools/index.ts - Enhanced tool descriptions - src/tools/note-tools.ts - Use PathUtils and ErrorMessages - src/tools/vault-tools.ts - Use PathUtils and ErrorMessages
This commit is contained in:
@@ -1,68 +1,208 @@
|
||||
import { App, TFile } from 'obsidian';
|
||||
import { CallToolResult } from '../types/mcp-types';
|
||||
import { PathUtils } from '../utils/path-utils';
|
||||
import { ErrorMessages } from '../utils/error-messages';
|
||||
|
||||
export class NoteTools {
|
||||
constructor(private app: App) {}
|
||||
|
||||
async readNote(path: string): Promise<CallToolResult> {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
// Validate path
|
||||
if (!path || path.trim() === '') {
|
||||
return {
|
||||
content: [{ type: "text", text: `Note not found: ${path}` }],
|
||||
content: [{ type: "text", text: ErrorMessages.emptyPath() }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
return {
|
||||
content: [{ type: "text", text: content }]
|
||||
};
|
||||
if (!PathUtils.isValidVaultPath(path)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.invalidPath(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve file using path utilities
|
||||
const file = PathUtils.resolveFile(this.app, path);
|
||||
|
||||
if (!file) {
|
||||
// Check if it's a folder instead
|
||||
if (PathUtils.folderExists(this.app, path)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.notAFile(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.fileNotFound(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await this.app.vault.read(file);
|
||||
return {
|
||||
content: [{ type: "text", text: content }]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.operationFailed('read note', path, (error as Error).message) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async createNote(path: string, content: string): Promise<CallToolResult> {
|
||||
// Validate path
|
||||
if (!path || path.trim() === '') {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.emptyPath() }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
if (!PathUtils.isValidVaultPath(path)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.invalidPath(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Normalize the path
|
||||
const normalizedPath = PathUtils.normalizePath(path);
|
||||
|
||||
// Check if file already exists
|
||||
if (PathUtils.fileExists(this.app, normalizedPath)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.pathAlreadyExists(normalizedPath, 'file') }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's a folder
|
||||
if (PathUtils.folderExists(this.app, normalizedPath)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.notAFile(normalizedPath) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await this.app.vault.create(path, content);
|
||||
const file = await this.app.vault.create(normalizedPath, content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note created successfully: ${file.path}` }]
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMsg = (error as Error).message;
|
||||
|
||||
// Check for parent folder not found error
|
||||
if (errorMsg.includes('parent folder')) {
|
||||
const parentPath = PathUtils.getParentPath(normalizedPath);
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.parentFolderNotFound(normalizedPath, parentPath) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed to create note: ${(error as Error).message}` }],
|
||||
content: [{ type: "text", text: ErrorMessages.operationFailed('create note', normalizedPath, errorMsg) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async updateNote(path: string, content: string): Promise<CallToolResult> {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
// Validate path
|
||||
if (!path || path.trim() === '') {
|
||||
return {
|
||||
content: [{ type: "text", text: `Note not found: ${path}` }],
|
||||
content: [{ type: "text", text: ErrorMessages.emptyPath() }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
await this.app.vault.modify(file, content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note updated successfully: ${path}` }]
|
||||
};
|
||||
if (!PathUtils.isValidVaultPath(path)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.invalidPath(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve file using path utilities
|
||||
const file = PathUtils.resolveFile(this.app, path);
|
||||
|
||||
if (!file) {
|
||||
// Check if it's a folder instead
|
||||
if (PathUtils.folderExists(this.app, path)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.notAFile(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.fileNotFound(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.vault.modify(file, content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note updated successfully: ${file.path}` }]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.operationFailed('update note', path, (error as Error).message) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async deleteNote(path: string): Promise<CallToolResult> {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
// Validate path
|
||||
if (!path || path.trim() === '') {
|
||||
return {
|
||||
content: [{ type: "text", text: `Note not found: ${path}` }],
|
||||
content: [{ type: "text", text: ErrorMessages.emptyPath() }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
await this.app.vault.delete(file);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note deleted successfully: ${path}` }]
|
||||
};
|
||||
if (!PathUtils.isValidVaultPath(path)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.invalidPath(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve file using path utilities
|
||||
const file = PathUtils.resolveFile(this.app, path);
|
||||
|
||||
if (!file) {
|
||||
// Check if it's a folder instead
|
||||
if (PathUtils.folderExists(this.app, path)) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.cannotDeleteFolder(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.fileNotFound(path) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.vault.delete(file);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note deleted successfully: ${file.path}` }]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: ErrorMessages.operationFailed('delete note', path, (error as Error).message) }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user