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:
2025-10-16 21:27:23 -04:00
parent 08cc6e9ea6
commit 7524271eaa
15 changed files with 5638 additions and 119 deletions

View File

@@ -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
};
}
}
}