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) {