mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add create_vault_folder tool for directory creation
Add new AI tool to create vault folders with proper permission checks and error handling. Refactor createDirectories to be public and handle both file and directory paths. Update WriteVaultFile description formatting and fix method naming consistency.
This commit is contained in:
parent
a32f153087
commit
ae321f382a
9 changed files with 132 additions and 39 deletions
|
|
@ -40,6 +40,11 @@ export const MoveVaultFilesArgsSchema = z.object({
|
|||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const CreateVaultFolderSchema = z.object({
|
||||
path: z.string(),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
export const ListVaultFilesArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
recursive: z.boolean(),
|
||||
|
|
@ -131,6 +136,7 @@ export type PatchVaultFileArgs = z.infer<typeof PatchVaultFileArgsSchema>;
|
|||
export type DeleteVaultFilesArgs = z.infer<typeof DeleteVaultFilesArgsSchema>;
|
||||
export type MoveVaultFilesArgs = z.infer<typeof MoveVaultFilesArgsSchema>;
|
||||
export type ListVaultFilesArgs = z.infer<typeof ListVaultFilesArgsSchema>;
|
||||
export type CreateVaultFolder = z.infer<typeof CreateVaultFolderSchema>;
|
||||
export type ExecuteWorkflowArgs = z.infer<typeof ExecuteWorkflowArgsSchema>;
|
||||
export type AskUserQuestionPlanningArgs = z.infer<typeof AskUserQuestionPlanningArgsSchema>;
|
||||
export type AskUserQuestionExecutionArgs = z.infer<typeof AskUserQuestionExecutionArgsSchema>;
|
||||
|
|
|
|||
|
|
@ -18,13 +18,14 @@ import { RevisePlan } from "./Tools/RevisePlan";
|
|||
import { SkipStep } from "./Tools/SkipStep";
|
||||
import { ReadMemories } from "./Tools/ReadMemories";
|
||||
import { UpdateMemories } from "./Tools/UpdateMemories";
|
||||
import { CreateVaultFolder } from "./Tools/CreateVaultFolder";
|
||||
|
||||
export abstract class AIToolDefinitions {
|
||||
|
||||
public static isGated: boolean = false;
|
||||
|
||||
// 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, WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles];
|
||||
private static readonly definitionsList = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder];
|
||||
|
||||
public static agentDefinitions(destructive: boolean, planningMode: boolean, memories: boolean, updateMemories: boolean): IAIToolDefinition[] {
|
||||
this.isGated = false;
|
||||
|
|
@ -52,7 +53,8 @@ export abstract class AIToolDefinitions {
|
|||
WriteVaultFile,
|
||||
PatchVaultFile,
|
||||
DeleteVaultFiles,
|
||||
MoveVaultFiles
|
||||
MoveVaultFiles,
|
||||
CreateVaultFolder
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
31
AIClasses/ToolDefinitions/Tools/CreateVaultFolder.ts
Normal file
31
AIClasses/ToolDefinitions/Tools/CreateVaultFolder.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { AITool } from "Enums/AITool";
|
||||
import type { IAIToolDefinition } from "../IAIToolDefinition";
|
||||
|
||||
export const CreateVaultFolder: IAIToolDefinition = {
|
||||
name: AITool.CreateVaultFolder,
|
||||
description: `Creates a new folder (directory) in the vault, including any necessary parent folders.
|
||||
|
||||
Call this function:
|
||||
- When setting up a new folder structure or organizational hierarchy
|
||||
- When a file needs to be written to a folder that does not yet exist
|
||||
- When the user explicitly asks to create a folder or directory
|
||||
|
||||
Do NOT use this function:
|
||||
- When the target folder already exists
|
||||
- When writing a file to an existing folder (use WriteVaultFile directly instead)
|
||||
- When renaming or moving an existing folder`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "The full path to the folder to create within the vault. Intermediate parent folders will be created automatically if they do not exist. Example: 'projects/2024/notes'"
|
||||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
description: "A short message to be displayed to the user explaining what folder you're creating and why. Example: 'Creating a new Projects folder to organise your notes'"
|
||||
}
|
||||
},
|
||||
required: ["path", "user_message"]
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ Do NOT use this function:
|
|||
properties: {
|
||||
file_path: {
|
||||
type: "string",
|
||||
description: "The full path to the file within the vault (e.g., 'folder/note.md')"
|
||||
description: "The full path to the file within the vault. Example: 'folder/note.md'"
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
|
|
@ -26,7 +26,7 @@ Do NOT use this function:
|
|||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
description: "A short message to be displayed to the user explaining what you're writing and why (e.g., 'Creating your daily note for today')"
|
||||
description: "A short message to be displayed to the user explaining what you're writing and why. Example: 'Creating your daily note for today'"
|
||||
}
|
||||
},
|
||||
required: ["file_path", "content", "user_message"]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export enum AITool {
|
|||
PatchVaultFile = "patch_vault_file",
|
||||
DeleteVaultFiles = "delete_vault_files",
|
||||
MoveVaultFiles = "move_vault_files",
|
||||
CreateVaultFolder = "create_vault_folder",
|
||||
ListVaultFiles = "list_vault_files",
|
||||
ReadMemories = "read_memories",
|
||||
UpdateMemories = "update_memories",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import {
|
|||
ListVaultFilesArgsSchema,
|
||||
PatchVaultFileArgsSchema,
|
||||
ReadMemoriesArgsSchema,
|
||||
UpdateMemoriesArgsSchema
|
||||
UpdateMemoriesArgsSchema,
|
||||
CreateVaultFolderSchema
|
||||
} from "AIClasses/Schemas/AIToolSchemas";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
|
||||
|
|
@ -119,6 +120,18 @@ export class AIToolService {
|
|||
}
|
||||
return new AIToolResponse(toolCall.name, await this.moveVaultFiles(parseResult.data.source_paths, parseResult.data.destination_paths), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.CreateVaultFolder: {
|
||||
const parseResult = CreateVaultFolderSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.CreateVaultFolder}: ${parseResult.error.message}` },
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(toolCall.name, await this.createVaultFolder(parseResult.data.path), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.ListVaultFiles: {
|
||||
const parseResult = ListVaultFilesArgsSchema.safeParse(toolCall.arguments);
|
||||
|
|
@ -129,7 +142,7 @@ export class AIToolService {
|
|||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(toolCall.name, await this.ListVaultFiles(parseResult.data.path, parseResult.data.recursive), toolCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.listVaultFiles(parseResult.data.path, parseResult.data.recursive), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.ReadMemories: {
|
||||
|
|
@ -222,13 +235,13 @@ export class AIToolService {
|
|||
filePaths.map(async (filePath) => {
|
||||
const result = await this.fileSystemService.readFile(filePath);
|
||||
if (result instanceof Error) {
|
||||
return { path: filePath, error: result.message }
|
||||
return { path: filePath, error: result.message };
|
||||
}
|
||||
return {
|
||||
type: pathExtname(filePath),
|
||||
path: filePath,
|
||||
contents: result
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
return { results };
|
||||
|
|
@ -258,7 +271,7 @@ export class AIToolService {
|
|||
const results = await Promise.all(filePaths.map(async filePath => {
|
||||
const result = await this.fileSystemService.deleteFile(filePath);
|
||||
if (result instanceof Error) {
|
||||
return { path: filePath, success: false, error: result.message }
|
||||
return { path: filePath, success: false, error: result.message };
|
||||
}
|
||||
return { path: filePath, success: true };
|
||||
}));
|
||||
|
|
@ -275,7 +288,7 @@ export class AIToolService {
|
|||
const destinationPath = destinationPaths[index];
|
||||
const result = await this.fileSystemService.moveFile(sourcePath, destinationPath);
|
||||
if (result instanceof Error) {
|
||||
return { path: destinationPath, success: false, error: result.message }
|
||||
return { path: destinationPath, success: false, error: result.message };
|
||||
}
|
||||
return { path: destinationPath, success: true };
|
||||
}));
|
||||
|
|
@ -283,7 +296,15 @@ export class AIToolService {
|
|||
return { results };
|
||||
}
|
||||
|
||||
private async ListVaultFiles(path: string, recursive: boolean): Promise<object> {
|
||||
private async createVaultFolder(path: string): Promise<object> {
|
||||
const result = await this.fileSystemService.createFolder(path);
|
||||
if (result instanceof Error) {
|
||||
return { path: path, success: false, error: result.message };
|
||||
}
|
||||
return { path: path, success: true };
|
||||
}
|
||||
|
||||
private async listVaultFiles(path: string, recursive: boolean): Promise<object> {
|
||||
const files: TAbstractFile[] = await this.fileSystemService.listDirectoryContents(path, recursive);
|
||||
return files.map(file => ({
|
||||
type: file instanceof TFile ? "file" : "directory",
|
||||
|
|
@ -301,7 +322,7 @@ export class AIToolService {
|
|||
|
||||
private async updateMemories(content: string): Promise<object> {
|
||||
if (!this.settingsService.settings.allowUpdatingMemories) {
|
||||
return { error: Copy.MemoriesUpdatingDisabledError }
|
||||
return { error: Copy.MemoriesUpdatingDisabledError };
|
||||
}
|
||||
|
||||
if (!this.lastToolReadMemories) {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ export class FileSystemService {
|
|||
return await this.vaultService.move(sourcePath, destinationPath, allowAccessToPluginRoot);
|
||||
}
|
||||
|
||||
public async createFolder(path: string, allowAccessToPluginRoot: boolean = false): Promise<void | Error> {
|
||||
return await this.vaultService.createDirectories(path, allowAccessToPluginRoot);
|
||||
}
|
||||
|
||||
public async listFilesInDirectory(dirPath: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TFile[]> {
|
||||
return await this.vaultService.listFilesInDirectory(dirPath, recursive, allowAccessToPluginRoot);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,15 +284,6 @@ export class VaultService {
|
|||
}
|
||||
}
|
||||
|
||||
public async createFolder(path: string, allowAccessToPluginRoot: boolean = false): Promise<TFolder | Error> {
|
||||
path = this.sanitiserService.sanitize(path);
|
||||
if (this.isExclusion(path, allowAccessToPluginRoot)) {
|
||||
Exception.log(`Plugin attempted to create a folder that is in the exclusion list: ${path}`);
|
||||
return Exception.new(`Failed to create folder, permission denied: ${path}`);
|
||||
}
|
||||
return await this.vault.createFolder(path);
|
||||
}
|
||||
|
||||
public async listDirectoryContents(path: string, recursive: boolean = true, allowAccessToPluginRoot: boolean = false): Promise<TAbstractFile[]> {
|
||||
path = this.sanitiserService.sanitize(path);
|
||||
|
||||
|
|
@ -468,8 +459,10 @@ export class VaultService {
|
|||
});
|
||||
}
|
||||
|
||||
private async createDirectories(filePath: string, allowAccessToPluginRoot: boolean = false): Promise<void | Error> {
|
||||
const dirPath: string = filePath.substring(0, filePath.lastIndexOf("/"));
|
||||
public async createDirectories(filePath: string, allowAccessToPluginRoot: boolean = false): Promise<void | Error> {
|
||||
const dirPath: string = path.extname(filePath)
|
||||
? filePath.substring(0, filePath.lastIndexOf("/"))
|
||||
: filePath;
|
||||
|
||||
const dirs: string[] = dirPath.split("/");
|
||||
|
||||
|
|
@ -478,13 +471,11 @@ export class VaultService {
|
|||
for (const dir of dirs) {
|
||||
if (dir) {
|
||||
currentPath = currentPath ? `${currentPath}/${dir}` : dir;
|
||||
try {
|
||||
if (!(await this.exists(currentPath, allowAccessToPluginRoot))) {
|
||||
await this.createFolder(currentPath, allowAccessToPluginRoot);
|
||||
if (!(await this.exists(currentPath, allowAccessToPluginRoot))) {
|
||||
const result = await this.createDirectory(currentPath, allowAccessToPluginRoot);
|
||||
if (result instanceof Error) {
|
||||
failures.push(currentPath);
|
||||
}
|
||||
} catch (error) {
|
||||
failures.push(currentPath);
|
||||
Exception.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -493,6 +484,15 @@ export class VaultService {
|
|||
}
|
||||
}
|
||||
|
||||
private async createDirectory(path: string, allowAccessToPluginRoot: boolean = false): Promise<TFolder | Error> {
|
||||
path = this.sanitiserService.sanitize(path);
|
||||
if (this.isExclusion(path, allowAccessToPluginRoot)) {
|
||||
Exception.log(`Plugin attempted to create a folder that is in the exclusion list: ${path}`);
|
||||
return Exception.new(`Failed to create folder, permission denied: ${path}`);
|
||||
}
|
||||
return await this.vault.createFolder(path);
|
||||
}
|
||||
|
||||
private extractSnippets(pages: IPageText[], regex: RegExp): ISearchSnippet[] {
|
||||
const allSnippets: ISearchSnippet[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -705,22 +705,50 @@ describe('VaultService - Integration Tests', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createFolder', () => {
|
||||
it('should create folder with sanitized path', async () => {
|
||||
const mockFolder = createMockFolder('folder');
|
||||
mockVault.createFolder.mockResolvedValue(mockFolder);
|
||||
describe('createDirectories', () => {
|
||||
it('should create a single directory with no slashes', async () => {
|
||||
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
||||
mockVault.createFolder.mockResolvedValue(createMockFolder('Zap'));
|
||||
|
||||
const result = await vaultService.createFolder('folder');
|
||||
await vaultService.createDirectories('Zap');
|
||||
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
||||
expect(result).toBe(mockFolder);
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('Zap');
|
||||
});
|
||||
|
||||
it('should return error when trying to create folder in excluded path', async () => {
|
||||
const result = await vaultService.createFolder('Vaultkeeper AI/subfolder', false);
|
||||
it('should create all intermediate directories for a directory path', async () => {
|
||||
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
||||
mockVault.createFolder.mockResolvedValue(createMockFolder(''));
|
||||
|
||||
await vaultService.createDirectories('Zap/Test');
|
||||
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('Zap');
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('Zap/Test');
|
||||
});
|
||||
|
||||
it('should create parent directories for a file path', async () => {
|
||||
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
||||
mockVault.createFolder.mockResolvedValue(createMockFolder(''));
|
||||
|
||||
await vaultService.createDirectories('Zap/Test/file.md');
|
||||
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('Zap');
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('Zap/Test');
|
||||
expect(mockVault.createFolder).not.toHaveBeenCalledWith('Zap/Test/file.md');
|
||||
});
|
||||
|
||||
it('should skip directories that already exist', async () => {
|
||||
mockVault.adapter.exists.mockResolvedValue(true);
|
||||
|
||||
await vaultService.createDirectories('Zap');
|
||||
|
||||
expect(mockVault.createFolder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return error when trying to create directory in excluded path', async () => {
|
||||
const result = await vaultService.createDirectories('Vaultkeeper AI/subfolder', false);
|
||||
|
||||
expect(result).toBeInstanceOf(Error);
|
||||
expect((result as Error).message).toContain('Failed to create folder, permission denied');
|
||||
expect((result as Error).message).toContain('Failed to create the following directories');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue