mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
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.
This commit is contained in:
parent
540b76e0ea
commit
16c9107606
7 changed files with 100 additions and 11 deletions
|
|
@ -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<typeof PatchVaultFileArgsSchema>;
|
|||
export type DeleteVaultFilesArgs = z.infer<typeof DeleteVaultFilesArgsSchema>;
|
||||
export type MoveVaultFilesArgs = z.infer<typeof MoveVaultFilesArgsSchema>;
|
||||
export type CreateVaultFolder = z.infer<typeof CreateVaultFolderSchema>;
|
||||
export type DeleteVaultFolder = z.infer<typeof DeleteVaultFolderSchema>;
|
||||
export type ListVaultFilesArgs = z.infer<typeof ListVaultFilesArgsSchema>;
|
||||
export type GetWebViewerContent = z.infer<typeof GetWebViewerContentSchema>;
|
||||
export type ExecuteWorkflowArgs = z.infer<typeof ExecuteWorkflowArgsSchema>;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
40
AIClasses/ToolDefinitions/Tools/DeleteVaultFolder.ts
Normal file
40
AIClasses/ToolDefinitions/Tools/DeleteVaultFolder.ts
Normal file
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<AIToolResponsePayload> {
|
||||
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<AIToolResponsePayload> {
|
||||
const files: TAbstractFile[] = await this.fileSystemService.listDirectoryContents(path, recursive);
|
||||
return new AIToolResponsePayload(files.map(file => ({
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class FileSystemService {
|
|||
public async deleteFile(filePath: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<Error | void> {
|
||||
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<void | Error> {
|
||||
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<TFile[]> {
|
||||
return await this.vaultService.listFilesInDirectory(dirPath, recursive, allowAccessToPluginRoot);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string | Error> {
|
||||
|
|
@ -199,13 +199,16 @@ export class VaultService {
|
|||
|
||||
public async delete(file: TAbstractFile, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<void | Error> {
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue