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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
15
src/tools/vault-tools-factory.ts
Normal file
15
src/tools/vault-tools-factory.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -46,27 +51,11 @@ 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 === '.';
|
||||||
|
|
||||||
if (isRootPath) {
|
let targetFolder: TFolder;
|
||||||
// List direct children of the root
|
|
||||||
const allFiles = this.app.vault.getAllLoadedFiles();
|
|
||||||
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
|
if (isRootPath) {
|
||||||
// Root items have parent === null or parent.path === '' or parent.path === '/'
|
// Get the root folder using adapter
|
||||||
const itemParent = item.parent?.path || '';
|
targetFolder = this.vault.getRoot();
|
||||||
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) {
|
|
||||||
// Check if it's a file instead
|
|
||||||
if (PathUtils.fileExists(this.app, normalizedPath)) {
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: ErrorMessages.notAFolder(normalizedPath) }],
|
|
||||||
isError: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!folderObj) {
|
||||||
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,6 +235,48 @@ 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
|
||||||
@@ -307,7 +289,7 @@ export class VaultTools {
|
|||||||
|
|
||||||
// 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 = {};
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user