refactor: migrate VaultTools to use adapter interfaces

Update VaultTools constructor to accept IVaultAdapter and
IMetadataCacheAdapter. Add factory function for production usage.
Update stat, exists, and createFileMetadataWithFrontmatter methods.
This commit is contained in:
2025-10-19 23:25:22 -04:00
parent 248b3924fe
commit 25755661f7
3 changed files with 162 additions and 132 deletions

View File

@@ -2,6 +2,7 @@ import { App } from 'obsidian';
import { Tool, CallToolResult } from '../types/mcp-types'; import { Tool, CallToolResult } from '../types/mcp-types';
import { NoteTools } from './note-tools'; import { NoteTools } from './note-tools';
import { VaultTools } from './vault-tools'; import { VaultTools } from './vault-tools';
import { createVaultTools } from './vault-tools-factory';
import { NotificationManager } from '../ui/notifications'; import { NotificationManager } from '../ui/notifications';
export class ToolRegistry { export class ToolRegistry {
@@ -11,7 +12,7 @@ export class ToolRegistry {
constructor(app: App) { constructor(app: App) {
this.noteTools = new NoteTools(app); this.noteTools = new NoteTools(app);
this.vaultTools = new VaultTools(app); this.vaultTools = createVaultTools(app);
} }
/** /**

View File

@@ -0,0 +1,15 @@
import { App } from 'obsidian';
import { VaultTools } from './vault-tools';
import { VaultAdapter } from '../adapters/vault-adapter';
import { MetadataCacheAdapter } from '../adapters/metadata-adapter';
/**
* Factory function to create VaultTools with concrete adapters
*/
export function createVaultTools(app: App): VaultTools {
return new VaultTools(
new VaultAdapter(app.vault),
new MetadataCacheAdapter(app.metadataCache),
app
);
}

View File

@@ -6,9 +6,14 @@ import { GlobUtils } from '../utils/glob-utils';
import { SearchUtils } from '../utils/search-utils'; import { SearchUtils } from '../utils/search-utils';
import { WaypointUtils } from '../utils/waypoint-utils'; import { WaypointUtils } from '../utils/waypoint-utils';
import { LinkUtils } from '../utils/link-utils'; import { LinkUtils } from '../utils/link-utils';
import { IVaultAdapter, IMetadataCacheAdapter } from '../adapters/interfaces';
export class VaultTools { export class VaultTools {
constructor(private app: App) {} constructor(
private vault: IVaultAdapter,
private metadata: IMetadataCacheAdapter,
private app: App // Keep temporarily for methods not yet migrated
) {}
async getVaultInfo(): Promise<CallToolResult> { async getVaultInfo(): Promise<CallToolResult> {
const files = this.app.vault.getFiles(); const files = this.app.vault.getFiles();
@@ -45,28 +50,12 @@ export class VaultTools {
// Normalize root path: undefined, empty string "", or "." all mean root // Normalize root path: undefined, empty string "", or "." all mean root
const isRootPath = !path || path === '' || path === '.'; const isRootPath = !path || path === '' || path === '.';
let targetFolder: TFolder;
if (isRootPath) { if (isRootPath) {
// List direct children of the root // Get the root folder using adapter
const allFiles = this.app.vault.getAllLoadedFiles(); targetFolder = this.vault.getRoot();
for (const item of allFiles) {
// Skip the vault root itself
// The vault root can have path === '' or path === '/' depending on Obsidian version
if (item.path === '' || item.path === '/' || (item instanceof TFolder && item.isRoot())) {
continue;
}
// Check if this item is a direct child of root
// Root items have parent === null or parent.path === '' or parent.path === '/'
const itemParent = item.parent?.path || '';
if (itemParent === '' || itemParent === '/') {
if (item instanceof TFile) {
items.push(this.createFileMetadata(item));
} else if (item instanceof TFolder) {
items.push(this.createDirectoryMetadata(item));
}
}
}
} else { } else {
// Validate non-root path // Validate non-root path
if (!PathUtils.isValidVaultPath(path)) { if (!PathUtils.isValidVaultPath(path)) {
@@ -79,35 +68,38 @@ export class VaultTools {
// Normalize the path // Normalize the path
const normalizedPath = PathUtils.normalizePath(path); const normalizedPath = PathUtils.normalizePath(path);
// Check if it's a folder // Get folder using adapter
const folderObj = PathUtils.resolveFolder(this.app, normalizedPath); const folderObj = this.vault.getAbstractFileByPath(normalizedPath);
if (!folderObj) { if (!folderObj) {
// Check if it's a file instead
if (PathUtils.fileExists(this.app, normalizedPath)) {
return {
content: [{ type: "text", text: ErrorMessages.notAFolder(normalizedPath) }],
isError: true
};
}
return { return {
content: [{ type: "text", text: ErrorMessages.folderNotFound(normalizedPath) }], content: [{ type: "text", text: ErrorMessages.folderNotFound(normalizedPath) }],
isError: true isError: true
}; };
} }
// Get direct children of the folder (non-recursive) // Check if it's a folder
const allFiles = this.app.vault.getAllLoadedFiles(); if (!(folderObj instanceof TFolder)) {
for (const item of allFiles) { return {
// Check if this item is a direct child of the target folder content: [{ type: "text", text: ErrorMessages.notAFolder(normalizedPath) }],
const itemParent = item.parent?.path || ''; isError: true
if (itemParent === normalizedPath) { };
if (item instanceof TFile) { }
items.push(this.createFileMetadata(item));
} else if (item instanceof TFolder) { targetFolder = folderObj;
items.push(this.createDirectoryMetadata(item)); }
}
} // Iterate over direct children of the folder
for (const item of targetFolder.children) {
// Skip the vault root itself
if (item.path === '' || item.path === '/' || (item instanceof TFolder && item.isRoot())) {
continue;
}
if (item instanceof TFile) {
items.push(this.createFileMetadata(item));
} else if (item instanceof TFolder) {
items.push(this.createDirectoryMetadata(item));
} }
} }
@@ -155,9 +147,13 @@ export class VaultTools {
// Normalize root path: undefined, empty string "", or "." all mean root // Normalize root path: undefined, empty string "", or "." all mean root
const isRootPath = !path || path === '' || path === '.'; const isRootPath = !path || path === '' || path === '.';
let normalizedPath = '';
if (!isRootPath) { let targetFolder: TFolder;
if (isRootPath) {
// Get the root folder using adapter
targetFolder = this.vault.getRoot();
} else {
// Validate non-root path // Validate non-root path
if (!PathUtils.isValidVaultPath(path)) { if (!PathUtils.isValidVaultPath(path)) {
return { return {
@@ -167,87 +163,31 @@ export class VaultTools {
} }
// Normalize the path // Normalize the path
normalizedPath = PathUtils.normalizePath(path); const normalizedPath = PathUtils.normalizePath(path);
// Get folder using adapter
const folderObj = this.vault.getAbstractFileByPath(normalizedPath);
// Check if it's a folder
const folderObj = PathUtils.resolveFolder(this.app, normalizedPath);
if (!folderObj) { if (!folderObj) {
// Check if it's a file instead
if (PathUtils.fileExists(this.app, normalizedPath)) {
return {
content: [{ type: "text", text: ErrorMessages.notAFolder(normalizedPath) }],
isError: true
};
}
return { return {
content: [{ type: "text", text: ErrorMessages.folderNotFound(normalizedPath) }], content: [{ type: "text", text: ErrorMessages.folderNotFound(normalizedPath) }],
isError: true isError: true
}; };
} }
// Check if it's a folder
if (!(folderObj instanceof TFolder)) {
return {
content: [{ type: "text", text: ErrorMessages.notAFolder(normalizedPath) }],
isError: true
};
}
targetFolder = folderObj;
} }
// Collect items based on recursive flag // Collect items based on recursive flag
const allFiles = this.app.vault.getAllLoadedFiles(); await this.collectItems(targetFolder, items, recursive, includes, excludes, only, withFrontmatterSummary);
for (const item of allFiles) {
// Skip the vault root itself
if (item.path === '' || item.path === '/' || (item instanceof TFolder && item.isRoot())) {
continue;
}
// Determine if this item should be included based on path
let shouldIncludeItem = false;
if (isRootPath) {
if (recursive) {
// Include all items in the vault
shouldIncludeItem = true;
} else {
// Include only direct children of root
const itemParent = item.parent?.path || '';
shouldIncludeItem = (itemParent === '' || itemParent === '/');
}
} else {
if (recursive) {
// Include items that are descendants of the target folder
shouldIncludeItem = item.path.startsWith(normalizedPath + '/') || item.path === normalizedPath;
// Exclude the folder itself
if (item.path === normalizedPath) {
shouldIncludeItem = false;
}
} else {
// Include only direct children of the target folder
const itemParent = item.parent?.path || '';
shouldIncludeItem = (itemParent === normalizedPath);
}
}
if (!shouldIncludeItem) {
continue;
}
// Apply glob filtering
if (!GlobUtils.shouldInclude(item.path, includes, excludes)) {
continue;
}
// Apply type filtering
if (item instanceof TFile) {
if (only === 'directories') {
continue;
}
const fileMetadata = await this.createFileMetadataWithFrontmatter(item, withFrontmatterSummary);
items.push(fileMetadata);
} else if (item instanceof TFolder) {
if (only === 'files') {
continue;
}
items.push(this.createDirectoryMetadata(item));
}
}
// Sort: directories first, then files, alphabetically within each group // Sort: directories first, then files, alphabetically within each group
items.sort((a, b) => { items.sort((a, b) => {
@@ -295,22 +235,64 @@ export class VaultTools {
}; };
} }
/**
* Helper method to recursively collect items from a folder
*/
private async collectItems(
folder: TFolder,
items: Array<FileMetadataWithFrontmatter | DirectoryMetadata>,
recursive: boolean,
includes?: string[],
excludes?: string[],
only?: 'files' | 'directories' | 'any',
withFrontmatterSummary?: boolean
): Promise<void> {
for (const item of folder.children) {
// Skip the vault root itself
if (item.path === '' || item.path === '/' || (item instanceof TFolder && item.isRoot())) {
continue;
}
// Apply glob filtering
if (!GlobUtils.shouldInclude(item.path, includes, excludes)) {
continue;
}
// Apply type filtering and add items
if (item instanceof TFile) {
if (only !== 'directories') {
const fileMetadata = await this.createFileMetadataWithFrontmatter(item, withFrontmatterSummary || false);
items.push(fileMetadata);
}
} else if (item instanceof TFolder) {
if (only !== 'files') {
items.push(this.createDirectoryMetadata(item));
}
// Recursively collect from subfolders if needed
if (recursive) {
await this.collectItems(item, items, recursive, includes, excludes, only, withFrontmatterSummary);
}
}
}
}
private async createFileMetadataWithFrontmatter( private async createFileMetadataWithFrontmatter(
file: TFile, file: TFile,
withFrontmatterSummary: boolean withFrontmatterSummary: boolean
): Promise<FileMetadataWithFrontmatter> { ): Promise<FileMetadataWithFrontmatter> {
const baseMetadata = this.createFileMetadata(file); const baseMetadata = this.createFileMetadata(file);
if (!withFrontmatterSummary || file.extension !== 'md') { if (!withFrontmatterSummary || file.extension !== 'md') {
return baseMetadata; return baseMetadata;
} }
// Extract frontmatter without reading full content // Extract frontmatter without reading full content
try { try {
const cache = this.app.metadataCache.getFileCache(file); const cache = this.metadata.getFileCache(file);
if (cache?.frontmatter) { if (cache?.frontmatter) {
const summary: FrontmatterSummary = {}; const summary: FrontmatterSummary = {};
// Extract common frontmatter fields // Extract common frontmatter fields
if (cache.frontmatter.title) { if (cache.frontmatter.title) {
summary.title = cache.frontmatter.title; summary.title = cache.frontmatter.title;
@@ -403,14 +385,30 @@ export class VaultTools {
// Normalize the path // Normalize the path
const normalizedPath = PathUtils.normalizePath(path); const normalizedPath = PathUtils.normalizePath(path);
// Get file or folder using adapter
const item = this.vault.getAbstractFileByPath(normalizedPath);
if (!item) {
// Path doesn't exist
const result: StatResult = {
path: normalizedPath,
exists: false
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
// Check if it's a file // Check if it's a file
const file = PathUtils.resolveFile(this.app, normalizedPath); if (item instanceof TFile) {
if (file) {
const result: StatResult = { const result: StatResult = {
path: normalizedPath, path: normalizedPath,
exists: true, exists: true,
kind: "file", kind: "file",
metadata: this.createFileMetadata(file) metadata: this.createFileMetadata(item)
}; };
return { return {
content: [{ content: [{
@@ -421,13 +419,12 @@ export class VaultTools {
} }
// Check if it's a folder // Check if it's a folder
const folder = PathUtils.resolveFolder(this.app, normalizedPath); if (item instanceof TFolder) {
if (folder) {
const result: StatResult = { const result: StatResult = {
path: normalizedPath, path: normalizedPath,
exists: true, exists: true,
kind: "directory", kind: "directory",
metadata: this.createDirectoryMetadata(folder) metadata: this.createDirectoryMetadata(item)
}; };
return { return {
content: [{ content: [{
@@ -437,7 +434,7 @@ export class VaultTools {
}; };
} }
// Path doesn't exist // Path doesn't exist (shouldn't reach here)
const result: StatResult = { const result: StatResult = {
path: normalizedPath, path: normalizedPath,
exists: false exists: false
@@ -462,8 +459,25 @@ export class VaultTools {
// Normalize the path // Normalize the path
const normalizedPath = PathUtils.normalizePath(path); const normalizedPath = PathUtils.normalizePath(path);
// Get file or folder using adapter
const item = this.vault.getAbstractFileByPath(normalizedPath);
if (!item) {
// Path doesn't exist
const result: ExistsResult = {
path: normalizedPath,
exists: false
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
// Check if it's a file // Check if it's a file
if (PathUtils.fileExists(this.app, normalizedPath)) { if (item instanceof TFile) {
const result: ExistsResult = { const result: ExistsResult = {
path: normalizedPath, path: normalizedPath,
exists: true, exists: true,
@@ -478,7 +492,7 @@ export class VaultTools {
} }
// Check if it's a folder // Check if it's a folder
if (PathUtils.folderExists(this.app, normalizedPath)) { if (item instanceof TFolder) {
const result: ExistsResult = { const result: ExistsResult = {
path: normalizedPath, path: normalizedPath,
exists: true, exists: true,
@@ -492,7 +506,7 @@ export class VaultTools {
}; };
} }
// Path doesn't exist // Path doesn't exist (shouldn't reach here)
const result: ExistsResult = { const result: ExistsResult = {
path: normalizedPath, path: normalizedPath,
exists: false exists: false