From 16c9107606322425d048df6ec241ec30e1536a01 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Wed, 8 Apr 2026 23:08:21 +0100 Subject: [PATCH] feat: add delete_vault_folder tool with confirmation parameter Implement DeleteVaultFolder schema, tool definition, and service method with confirmation guard. Update FileSystemService.deleteFolder and VaultService.delete to handle folder deletion with exclusion checks. Fix deleteFile type guard to ensure TFile instance. --- AIClasses/Schemas/AIToolSchemas.ts | 9 ++++- .../ToolDefinitions/AIToolDefinitions.ts | 6 ++- .../Tools/DeleteVaultFolder.ts | 40 +++++++++++++++++++ Enums/AITool.ts | 1 + Services/AIServices/AIToolService.ts | 25 +++++++++++- Services/FileSystemService.ts | 17 +++++++- Services/VaultService.ts | 13 +++--- 7 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 AIClasses/ToolDefinitions/Tools/DeleteVaultFolder.ts diff --git a/AIClasses/Schemas/AIToolSchemas.ts b/AIClasses/Schemas/AIToolSchemas.ts index b65acfa..5197ac1 100644 --- a/AIClasses/Schemas/AIToolSchemas.ts +++ b/AIClasses/Schemas/AIToolSchemas.ts @@ -1,5 +1,3 @@ -/** Zod schemas for runtime validation of AI function arguments **/ - import { z } from "zod"; // Zod schemas for AI function arguments @@ -45,6 +43,12 @@ export const CreateVaultFolderSchema = z.object({ user_message: z.string() }); +export const DeleteVaultFolderSchema = z.object({ + path: z.string(), + user_message: z.string(), + confirm_deletion: z.boolean() +}); + export const ListVaultFilesArgsSchema = z.object({ path: z.string(), recursive: z.boolean(), @@ -142,6 +146,7 @@ export type PatchVaultFileArgs = z.infer; export type DeleteVaultFilesArgs = z.infer; export type MoveVaultFilesArgs = z.infer; export type CreateVaultFolder = z.infer; +export type DeleteVaultFolder = z.infer; export type ListVaultFilesArgs = z.infer; export type GetWebViewerContent = z.infer; export type ExecuteWorkflowArgs = z.infer; diff --git a/AIClasses/ToolDefinitions/AIToolDefinitions.ts b/AIClasses/ToolDefinitions/AIToolDefinitions.ts index c0ed398..4795000 100644 --- a/AIClasses/ToolDefinitions/AIToolDefinitions.ts +++ b/AIClasses/ToolDefinitions/AIToolDefinitions.ts @@ -20,6 +20,7 @@ import { ReadMemories } from "./Tools/ReadMemories"; import { UpdateMemories } from "./Tools/UpdateMemories"; import { CreateVaultFolder } from "./Tools/CreateVaultFolder"; import { GetWebViewerContent } from "./Tools/GetWebViewerContent"; +import { DeleteVaultFolder } from "./Tools/DeleteVaultFolder"; export abstract class AIToolDefinitions { @@ -27,7 +28,7 @@ export abstract class AIToolDefinitions { // Definitions list provides a list of function definitions that does not include any planning functions (used as reference in planning agent prompt) private static readonly definitionsList = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, GetWebViewerContent, - WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder]; + WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder, DeleteVaultFolder]; public static agentDefinitions(destructive: boolean, planningMode: boolean, memories: boolean, updateMemories: boolean): IAIToolDefinition[] { this.isGated = false; @@ -57,7 +58,8 @@ export abstract class AIToolDefinitions { PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, - CreateVaultFolder + CreateVaultFolder, + DeleteVaultFolder ]); } diff --git a/AIClasses/ToolDefinitions/Tools/DeleteVaultFolder.ts b/AIClasses/ToolDefinitions/Tools/DeleteVaultFolder.ts new file mode 100644 index 0000000..c0af4f5 --- /dev/null +++ b/AIClasses/ToolDefinitions/Tools/DeleteVaultFolder.ts @@ -0,0 +1,40 @@ +import { AITool } from "Enums/AITool"; +import type { IAIToolDefinition } from "../IAIToolDefinition"; + +export const DeleteVaultFolder: IAIToolDefinition = { + name: AITool.DeleteVaultFolder, + description: `Permanently removes a folder (directory) and all of its contents from the vault. + +IMPORTANT: This action is irreversible - deleting a folder will also delete all files and subfolders within it. + +Call this function: +- When the user explicitly requests to delete a folder or directory +- When an entire folder structure is no longer needed and should be permanently removed +- When removing an outdated or empty directory that has been confirmed for deletion +- After verifying the folder exists and confirming the user's intent to delete it and its contents + +Do NOT use this function: +- When uncertain about whether to delete +- Before confirming the folder's contents and the user's intent to remove them all +- When the user only wants to delete specific files within the folder (delete only those files) +- When the user might want to recover the folder later - archive it instead`, + parameters: { + type: "object", + properties: { + path: { + type: "string", + description: "The full path to the folder to delete within the vault. All contents, including subfolders and files, will be permanently removed. Example: 'projects/2024/notes'" + }, + user_message: { + type: "string", + description: "A short message to be displayed to the user explaining what folder is being deleted and why. Example: 'Deleting the 2024 projects folder as it is no longer needed'" + }, + confirm_deletion: { + type: "boolean", + description: "Safety flag that must be explicitly set to true to confirm the deletion is intentional. This prevents accidental removal of entire folder structures.", + default: false + } + }, + required: ["path", "user_message", "confirm_deletion"] + } +} \ No newline at end of file diff --git a/Enums/AITool.ts b/Enums/AITool.ts index 5344128..5eef44e 100644 --- a/Enums/AITool.ts +++ b/Enums/AITool.ts @@ -6,6 +6,7 @@ export enum AITool { DeleteVaultFiles = "delete_vault_files", MoveVaultFiles = "move_vault_files", CreateVaultFolder = "create_vault_folder", + DeleteVaultFolder = "delete_vault_folder", ListVaultFiles = "list_vault_files", ReadMemories = "read_memories", UpdateMemories = "update_memories", diff --git a/Services/AIServices/AIToolService.ts b/Services/AIServices/AIToolService.ts index 355dec1..12fff86 100644 --- a/Services/AIServices/AIToolService.ts +++ b/Services/AIServices/AIToolService.ts @@ -30,7 +30,8 @@ import { ReadMemoriesArgsSchema, UpdateMemoriesArgsSchema, CreateVaultFolderSchema, - GetWebViewerContentSchema + GetWebViewerContentSchema, + DeleteVaultFolderSchema } from "AIClasses/Schemas/AIToolSchemas"; export class AIToolService { @@ -142,6 +143,18 @@ export class AIToolService { } return new AIToolResponse(toolCall.name, await this.createVaultFolder(parseResult.data.path), toolCall.toolId); } + + case AITool.DeleteVaultFolder: { + const parseResult = DeleteVaultFolderSchema.safeParse(toolCall.arguments); + if (!parseResult.success) { + return new AIToolResponse( + toolCall.name, + new AIToolResponsePayload({ error: `Invalid arguments for ${AITool.DeleteVaultFolder}: ${parseResult.error.message}` }), + toolCall.toolId + ); + } + return new AIToolResponse(toolCall.name, await this.deleteVaultFolder(parseResult.data.path, parseResult.data.confirm_deletion), toolCall.toolId); + } case AITool.ListVaultFiles: { const parseResult = ListVaultFilesArgsSchema.safeParse(toolCall.arguments); @@ -354,6 +367,16 @@ export class AIToolService { return new AIToolResponsePayload({ path: path, success: true }); } + private async deleteVaultFolder(path: string, confirmation: boolean): Promise { + if (!confirmation) { + return new AIToolResponsePayload({ error: "Confirmation was false, no action taken" }); + } + const result = await this.fileSystemService.deleteFolder(path); + return result instanceof Error + ? new AIToolResponsePayload({ path: path, success: false, error: result.message }) + : new AIToolResponsePayload({ path: path, success: true }); + } + private async listVaultFiles(path: string, recursive: boolean): Promise { const files: TAbstractFile[] = await this.fileSystemService.listDirectoryContents(path, recursive); return new AIToolResponsePayload(files.map(file => ({ diff --git a/Services/FileSystemService.ts b/Services/FileSystemService.ts index 25d70c4..6f1bd5d 100644 --- a/Services/FileSystemService.ts +++ b/Services/FileSystemService.ts @@ -80,7 +80,7 @@ export class FileSystemService { public async deleteFile(filePath: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise { const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot); - if (!file) { + if (!file || !(file instanceof TFile)) { return Exception.new(`File does not exist: ${filePath}`); } @@ -95,6 +95,21 @@ export class FileSystemService { return await this.vaultService.createDirectories(path, allowAccessToPluginRoot); } + public async deleteFolder(path: string, allowAccessToPluginRoot: boolean = false): Promise { + if (this.vaultService.isExclusion(path, allowAccessToPluginRoot)) { + return Exception.new(`Deletion failed. The folder or its contents may be protected: ${path}`); + } + + const folder: TAbstractFile | null = this.vaultService.getAbstractFileByPath(path, allowAccessToPluginRoot); + + if (!folder || !(folder instanceof TFolder)) { + return Exception.new(`Folder does not exist: ${path}`); + } + + return await this.vaultService.delete(folder, allowAccessToPluginRoot); + } + + public async listFilesInDirectory(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise { return await this.vaultService.listFilesInDirectory(dirPath, recursive, allowAccessToPluginRoot); } diff --git a/Services/VaultService.ts b/Services/VaultService.ts index 18cd467..0ad958e 100644 --- a/Services/VaultService.ts +++ b/Services/VaultService.ts @@ -76,7 +76,7 @@ export class VaultService { return false; } - return await this.vault.adapter.exists(filePath); + return await this.vault.adapter.exists(filePath, true); } public async read(file: TFile, allowAccessToPluginRoot: boolean = false): Promise { @@ -199,13 +199,16 @@ export class VaultService { public async delete(file: TAbstractFile, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise { const filePath = this.sanitiserService.sanitize(file.path); - if (this.isExclusion(file.path, allowAccessToPluginRoot)) { - Exception.log(`Plugin attempted to delete a file that is in the exclusions list: ${filePath}`) - return Exception.new(`File does not exist: ${filePath}`); + const isFile = file instanceof TFile; + + if (this.isExclusion(filePath, allowAccessToPluginRoot)) { + Exception.log(`Plugin attempted to delete a ${isFile ? "file" : "folder"} that is in the exclusions list: ${filePath}`) + return isFile ? Exception.new(`File does not exist: ${filePath} GOT`) + : Exception.new(`Deletion failed. The folder or its contents may be protected: ${filePath}`); } // handle file deletion - if (file instanceof TFile) { + if (isFile) { const currentContent = await this.read(file, allowAccessToPluginRoot) if (currentContent instanceof Error) {