/** * Copyright 2026 Reto Meier * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { MultiAgentToolContext, MultiAgentToolResult, ToolParsingResult } from "../../momoa_core/types.js"; import { addDynamicallyRelevantFile, getFileAnalysis, removeFileEntry, updateFileEntry } from "../../utils/fileAnalysis.js"; import { MultiAgentTool } from "../multiAgentTool.js"; /** * Simulates renaming a directory by modifying a file map in place. * It marks all files in the source path for deletion (by setting their content to an empty string) * and creates new entries for those files at the destination path with their original content. */ export const moveFolderTool: MultiAgentTool = { displayName: "Move File or Folder", name: 'MOVE_FILE_OR_FOLDER{SOURCE: ', endToken: '}', /** * Executes the file reader tool. * @param params The parameters for the tool's execution, expecting a 'filename' property. * @param context The ToolContext object containing necessary runtime information. * @returns A promise that resolves to the file's content or an error message. */ async execute(params: Record, context: MultiAgentToolContext): Promise { const source = params.source; const destination = params.destination; addDynamicallyRelevantFile(source); addDynamicallyRelevantFile(destination); let sourceExists = false; const allFileKeys = [...context.fileMap.keys(), ...context.binaryFileMap.keys()]; // Check if the source path exists as a direct file entry if (context.fileMap.has(source) || context.binaryFileMap.has(source)) { sourceExists = true; } else { // If not a file, check if it exists as a folder by looking for any file paths that start with it const normalizedSourcePrefix = source.endsWith('/') ? source : source + '/'; for (const key of allFileKeys) { if (key.startsWith(normalizedSourcePrefix)) { sourceExists = true; break; } } } if (!sourceExists) { const errorMessage = `No file or folder '${source}' exists, so it can't be moved or renamed. Please check the file or folder name carefully before retrying.`; return { result: errorMessage }; } let resultString; // Case 1: Renaming a single file. if (context.fileMap.has(source) || context.binaryFileMap.has(source)) { // Check if destination is already taken by a file. if (context.fileMap.has(destination) || context.binaryFileMap.has(destination)) { return { result: `Couldn't rename ${source} because '${destination}' already exists.` }; } const normalizedDestPrefix = destination.endsWith('/') ? destination : destination + '/'; for (const key of allFileKeys) { if (key.startsWith(normalizedDestPrefix)) { return { result: `Invalid destination -- a folder named '${destination}' already exists.` }; } } await updateFileInMetadata(source, destination, context); resultString = `Renamed file '${source}' to '${destination}'`; } else { // Case 2: Moving a folder if (destination.startsWith(source + '/')) { return { result: `Error: Cannot move a directory ('${source}') into a subdirectory of itself.` }; } // Check if destination path is blocked by an existing file or folder. if (context.fileMap.has(destination) || context.binaryFileMap.has(destination)) { return { result: `Invalid destination folder name -- a file already exists with that name.` }; } for (const filePath of allFileKeys) { if (filePath.startsWith(destination + '/')) { return { result: `Folder move failed. The directory '${destination}' already exists and is not empty. The destination directory must not exist or be empty.` }; } } const eachMove = new Set<{ sourceFilePath: string, destinationFilePath: string }>(); const normalizedSourcePrefix = source.endsWith('/') ? source : source + '/'; for (const sourceFilePath of allFileKeys) { if (sourceFilePath.startsWith(normalizedSourcePrefix)) { const destinationFilePath = sourceFilePath.replace(source, destination); eachMove.add({ sourceFilePath, destinationFilePath }); } } if (eachMove.size === 0) { return { result: `Source '${source}' appears to be an empty or non-existent folder. No files were moved.` }; } resultString = `Renamed directory '${source}' to '${destination}':`; for (const { sourceFilePath, destinationFilePath } of eachMove) { await updateFileInMetadata(sourceFilePath, destinationFilePath, context); resultString += `\n* Moved ${sourceFilePath} to ${destinationFilePath}`; } } return { result: resultString }; }, /** * Extract parameters from the tool invocation string. * @param invocation The string used to invoke the tool. * @returns The parameter names and corresponding values. */ async extractParameters(invocation: string, _context: MultiAgentToolContext): Promise { const pattern = /^(.*?)\s*DESTINATION:\s*(.*)/; const match = invocation.match(pattern); if (match && match[1] && match[2]) { const source = match[1].trim(); const destination = match[2].trim().replace(/}$/, '').trim(); if (source && destination) { return { success: true, params: { source, destination } }; } } return { success: false, error: `Invalid syntax for the ${this.displayName} Tool. Expected format: MOVE_FILE_OR_FOLDER{SOURCE: DESTINATION: }` }; } }; async function updateFileInMetadata(source: string, destination: string, context: MultiAgentToolContext) { context.editedFilesSet.add(source); context.editedFilesSet.add(destination); if (context.binaryFileMap.has(source)) { const sourceContent = context.binaryFileMap.get(source); context.binaryFileMap.set(destination, sourceContent ?? ''); context.binaryFileMap.delete(source); } else { const sourceContent = context.fileMap.get(source); const sourceAnalysis = getFileAnalysis(source); if (sourceAnalysis) { sourceAnalysis.filename = destination; sourceAnalysis.relatedFiles = ''; sourceAnalysis.description = `[Moved from ${source} to ${destination}] ${sourceAnalysis.description}`; } context.fileMap.set(destination, sourceContent ?? ''); await updateFileEntry(destination, context.fileMap, undefined, sourceAnalysis); context.fileMap.delete(source); removeFileEntry(source); } }