@aj-archipelago/cortex 1.4.6 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  2. package/helper-apps/cortex-file-handler/package.json +1 -1
  3. package/helper-apps/cortex-file-handler/src/index.js +27 -4
  4. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
  5. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
  6. package/helper-apps/cortex-file-handler/src/start.js +2 -0
  7. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
  8. package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
  9. package/lib/entityConstants.js +1 -1
  10. package/lib/fileUtils.js +1481 -0
  11. package/lib/pathwayTools.js +7 -1
  12. package/lib/util.js +2 -313
  13. package/package.json +4 -3
  14. package/pathways/image_qwen.js +1 -1
  15. package/pathways/system/entity/memory/sys_read_memory.js +17 -3
  16. package/pathways/system/entity/memory/sys_save_memory.js +22 -6
  17. package/pathways/system/entity/sys_entity_agent.js +21 -4
  18. package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
  19. package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
  20. package/pathways/system/entity/tools/sys_tool_editfile.js +403 -0
  21. package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
  22. package/pathways/system/entity/tools/sys_tool_image.js +172 -10
  23. package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
  24. package/pathways/system/entity/tools/sys_tool_readfile.js +217 -124
  25. package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
  26. package/pathways/system/entity/tools/sys_tool_writefile.js +211 -0
  27. package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
  28. package/pathways/transcribe_gemini.js +2 -1
  29. package/server/executeWorkspace.js +1 -1
  30. package/server/plugins/neuralSpacePlugin.js +2 -6
  31. package/server/plugins/openAiWhisperPlugin.js +2 -1
  32. package/server/plugins/replicateApiPlugin.js +4 -14
  33. package/server/typeDef.js +10 -1
  34. package/tests/integration/features/tools/fileCollection.test.js +858 -0
  35. package/tests/integration/features/tools/fileOperations.test.js +851 -0
  36. package/tests/integration/features/tools/writefile.test.js +350 -0
  37. package/tests/unit/core/fileCollection.test.js +259 -0
  38. package/tests/unit/core/util.test.js +320 -1
@@ -0,0 +1,171 @@
1
+ // sys_tool_analyzefile.js
2
+ // Entity tool that analyzes one or more files and answers questions about them
3
+
4
+ import { Prompt } from '../../../../server/prompt.js';
5
+ import { generateFileMessageContent, injectFileIntoChatHistory } from '../../../../lib/fileUtils.js';
6
+
7
+ export default {
8
+ prompt:
9
+ [
10
+ new Prompt({ messages: [
11
+ {"role": "system", "content": `You are the part of an AI entity named {{aiName}} that can view, hear, and understand files of all sorts (images, videos, audio, pdfs, text, etc.) - you provide the capability to view and analyze files that the user provides.\nThe user has provided you with one or more files in this conversation - you should consider them for context when you respond.\nIf you don't see any files, something has gone wrong in the upload and you should inform the user and have them try again.\n{{renderTemplate AI_DATETIME}}`},
12
+ "{{chatHistory}}",
13
+ ]}),
14
+ ],
15
+ inputParameters: {
16
+ chatHistory: [{role: '', content: []}],
17
+ contextId: ``,
18
+ contextKey: ``,
19
+ aiName: "Jarvis",
20
+ language: "English",
21
+ },
22
+ max_tokens: 8192,
23
+ model: 'gemini-flash-20-vision',
24
+ useInputChunking: false,
25
+ enableDuplicateRequests: false,
26
+ timeout: 600,
27
+ geminiSafetySettings: [{category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH'},
28
+ {category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH'},
29
+ {category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH'},
30
+ {category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH'}],
31
+ toolDefinition: [{
32
+ type: "function",
33
+ icon: "📄",
34
+ function: {
35
+ name: "AnalyzePDF",
36
+ description: "Use specifically for reading, analyzing, and answering questions about PDF file content. Do not use this tool for analyzing and answering questions about other file types.",
37
+ parameters: {
38
+ type: "object",
39
+ properties: {
40
+ detailedInstructions: {
41
+ type: "string",
42
+ description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
43
+ },
44
+ file: {
45
+ type: "string",
46
+ description: "Optional: The file to analyze (from ListFileCollection or SearchFileCollection): can be the hash, the filename, the URL, or the GCS URL. You can find available files in the availableFiles section."
47
+ },
48
+ userMessage: {
49
+ type: "string",
50
+ description: "A user-friendly message that describes what you're doing with this tool"
51
+ }
52
+ },
53
+ required: ["detailedInstructions", "userMessage"]
54
+ }
55
+ }
56
+ },
57
+ {
58
+ type: "function",
59
+ icon: "📝",
60
+ function: {
61
+ name: "AnalyzeText",
62
+ description: "Use specifically for reading, analyzing, and answering questions about text files (including csv, json, html, etc.).",
63
+ parameters: {
64
+ type: "object",
65
+ properties: {
66
+ detailedInstructions: {
67
+ type: "string",
68
+ description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
69
+ },
70
+ userMessage: {
71
+ type: "string",
72
+ description: "A user-friendly message that describes what you're doing with this tool"
73
+ }
74
+ },
75
+ required: ["detailedInstructions", "userMessage"]
76
+ }
77
+ }
78
+ },
79
+ {
80
+ type: "function",
81
+ icon: "📝",
82
+ function: {
83
+ name: "AnalyzeMarkdown",
84
+ description: "Use specifically for reading, analyzing, and answering questions about markdown files.",
85
+ parameters: {
86
+ type: "object",
87
+ properties: {
88
+ detailedInstructions: {
89
+ type: "string",
90
+ description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
91
+ },
92
+ userMessage: {
93
+ type: "string",
94
+ description: "A user-friendly message that describes what you're doing with this tool"
95
+ }
96
+ },
97
+ required: ["detailedInstructions", "userMessage"]
98
+ }
99
+ }
100
+ },
101
+ {
102
+ type: "function",
103
+ icon: "🖼️",
104
+ function: {
105
+ name: "AnalyzeImage",
106
+ description: "Use specifically for reading, analyzing, and answering questions about image files (jpg, gif, bmp, png, etc). This cannot be used for creating or transforming images.",
107
+ parameters: {
108
+ type: "object",
109
+ properties: {
110
+ detailedInstructions: {
111
+ type: "string",
112
+ description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
113
+ },
114
+ file: {
115
+ type: "string",
116
+ description: "Optional: The file to analyze (from ListFileCollection or SearchFileCollection): can be the hash, the filename, the URL, or the GCS URL. You can find available files in the availableFiles section."
117
+ },
118
+ userMessage: {
119
+ type: "string",
120
+ description: "A user-friendly message that describes what you're doing with this tool"
121
+ }
122
+ },
123
+ required: ["detailedInstructions", "userMessage"]
124
+ }
125
+ }
126
+ },
127
+ {
128
+ type: "function",
129
+ icon: "🎥",
130
+ function: {
131
+ name: "AnalyzeVideo",
132
+ description: "Use specifically for reading, analyzing, and answering questions about video or audio file content. You MUST use this tool to look at video or audio files.",
133
+ parameters: {
134
+ type: "object",
135
+ properties: {
136
+ detailedInstructions: {
137
+ type: "string",
138
+ description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
139
+ },
140
+ file: {
141
+ type: "string",
142
+ description: "Optional: The file to analyze (from ListFileCollection or SearchFileCollection): can be the hash, the filename, the URL, or the GCS URL. You can find available files in the availableFiles section."
143
+ },
144
+ userMessage: {
145
+ type: "string",
146
+ description: "A user-friendly message that describes what you're doing with this tool"
147
+ }
148
+ },
149
+ required: ["detailedInstructions", "userMessage"]
150
+ }
151
+ }
152
+ }],
153
+
154
+ executePathway: async ({args, runAllPrompts, resolver}) => {
155
+ // Generate file message content and inject file if provided
156
+ if (args.file) {
157
+ const fileContent = await generateFileMessageContent(args.file, args.contextId, args.contextKey);
158
+ if (!fileContent) {
159
+ throw new Error(`File not found: "${args.file}". Use ListFileCollection or SearchFileCollection to find available files.`);
160
+ }
161
+ args.chatHistory = injectFileIntoChatHistory(args.chatHistory, fileContent);
162
+ }
163
+
164
+ if (args.detailedInstructions) {
165
+ args.chatHistory.push({role: "user", content: args.detailedInstructions});
166
+ }
167
+ const result = await runAllPrompts({ ...args });
168
+ resolver.tool = JSON.stringify({ toolUsed: "vision" });
169
+ return result;
170
+ }
171
+ }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { QueueServiceClient } from '@azure/storage-queue';
5
5
  import logger from '../../../../lib/logger.js';
6
+ import { resolveFileParameter } from '../../../../lib/fileUtils.js';
6
7
 
7
8
  const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
8
9
  let queueClient;
@@ -58,7 +59,7 @@ export default {
58
59
  },
59
60
  inputFiles: {
60
61
  type: "string",
61
- description: "A list of input files that the coding agent must use to complete the task. Each file should be the fully-qualified URL to the file. Omit this parameter if no input files are needed."
62
+ description: "A list of input files (from Available Files section or ListFileCollection or SearchFileCollection) that the coding agent must use to complete the task. Each file should be the hash or filename. Omit this parameter if no input files are needed."
62
63
  },
63
64
  userMessage: {
64
65
  type: "string",
@@ -76,12 +77,45 @@ export default {
76
77
 
77
78
  executePathway: async ({args, resolver}) => {
78
79
  try {
79
- const { codingTask, userMessage, inputFiles, codingTaskKeywords } = args;
80
- const { contextId } = args;
80
+ const { codingTask, userMessage, inputFiles, codingTaskKeywords, contextId, contextKey } = args;
81
81
 
82
82
  let taskSuffix = "";
83
83
  if (inputFiles) {
84
- taskSuffix = `You must use the following files as input to complete the task: ${inputFiles}.`
84
+ if (!contextId) {
85
+ throw new Error("contextId is required when using the 'inputFiles' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
86
+ }
87
+
88
+ // Resolve file parameters to URLs
89
+ // inputFiles can be a comma-separated, newline-separated, or space-separated list
90
+ const fileReferences = inputFiles
91
+ .split(/[,\n\r]+/)
92
+ .map(ref => ref.trim())
93
+ .filter(ref => ref.length > 0);
94
+
95
+ const resolvedUrls = [];
96
+ const failedFiles = [];
97
+
98
+ for (const fileRef of fileReferences) {
99
+ // Try to resolve each file reference
100
+ const resolvedUrl = await resolveFileParameter(fileRef, contextId, contextKey);
101
+ if (resolvedUrl) {
102
+ resolvedUrls.push(resolvedUrl);
103
+ } else {
104
+ failedFiles.push(fileRef);
105
+ }
106
+ }
107
+
108
+ // Fail early if any files couldn't be resolved
109
+ if (failedFiles.length > 0) {
110
+ const fileList = failedFiles.length === 1
111
+ ? `"${failedFiles[0]}"`
112
+ : failedFiles.map(f => `"${f}"`).join(', ');
113
+ throw new Error(`File(s) not found: ${fileList}. Use ListFileCollection or SearchFileCollection to find available files.`);
114
+ }
115
+
116
+ if (resolvedUrls.length > 0) {
117
+ taskSuffix = `You must use the following files as input to complete the task: ${resolvedUrls.join(', ')}.`
118
+ }
85
119
  }
86
120
 
87
121
 
@@ -0,0 +1,403 @@
1
+ // sys_tool_editfile.js
2
+ // Entity tool that modifies existing files by replacing line ranges or exact string matches
3
+ import logger from '../../../../lib/logger.js';
4
+ import { axios } from '../../../../lib/requestExecutor.js';
5
+ import { uploadFileToCloud, findFileInCollection, loadFileCollection, saveFileCollection, getMimeTypeFromFilename, resolveFileParameter, deleteFileByHash, modifyFileCollectionWithLock } from '../../../../lib/fileUtils.js';
6
+
7
+ export default {
8
+ prompt: [],
9
+ timeout: 120,
10
+ toolDefinition: [
11
+ {
12
+ type: "function",
13
+ icon: "✏️",
14
+ function: {
15
+ name: "EditFileByLine",
16
+ description: "Modify an existing file by replacing a range of lines. Use this for line-based edits where you know the exact line numbers to replace. The file must exist in your file collection and must be a text-type file (text, markdown, html, csv, etc.). After modification, the file is re-uploaded and the collection entry is updated.",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {
20
+ file: {
21
+ type: "string",
22
+ description: "The file to modify: can be the file ID, filename, URL, or hash from your file collection. You can find available files in the Available Files section or ListFileCollection or SearchFileCollection."
23
+ },
24
+ startLine: {
25
+ type: "number",
26
+ description: "Starting line number (1-indexed) to replace. The line range is inclusive (both startLine and endLine are replaced)."
27
+ },
28
+ endLine: {
29
+ type: "number",
30
+ description: "Ending line number (1-indexed) to replace. Must be >= startLine. The line range is inclusive (both startLine and endLine are replaced)."
31
+ },
32
+ content: {
33
+ type: "string",
34
+ description: "New content to replace the specified line range. This will replace lines startLine through endLine (inclusive)."
35
+ },
36
+ userMessage: {
37
+ type: "string",
38
+ description: "A user-friendly message that describes what you're doing with this tool"
39
+ }
40
+ },
41
+ required: ["file", "startLine", "endLine", "content", "userMessage"]
42
+ }
43
+ }
44
+ },
45
+ {
46
+ type: "function",
47
+ icon: "🔍",
48
+ function: {
49
+ name: "EditFileBySearchAndReplace",
50
+ description: "Search and replace exact string matches in a file. Use this when you know the exact text to find and replace. The file must exist in your file collection and must be a text-type file (text, markdown, html, csv, etc.). After modification, the old file version is deleted from cloud storage and the new version is uploaded. The collection entry is updated with the new URL and hash.",
51
+ parameters: {
52
+ type: "object",
53
+ properties: {
54
+ file: {
55
+ type: "string",
56
+ description: "The file to modify: can be the file ID, filename, URL, or hash from your file collection. You can find available files in the Available Files section or ListFileCollection or SearchFileCollection."
57
+ },
58
+ oldString: {
59
+ type: "string",
60
+ description: "Exact string to replace. Must match the exact text in the file (including whitespace and newlines). The search is case-sensitive and must match exactly."
61
+ },
62
+ newString: {
63
+ type: "string",
64
+ description: "New content to replace oldString with."
65
+ },
66
+ replaceAll: {
67
+ type: "boolean",
68
+ description: "Optional: If true, replace all occurrences of oldString. Default: false (replace only first occurrence)."
69
+ },
70
+ userMessage: {
71
+ type: "string",
72
+ description: "A user-friendly message that describes what you're doing with this tool"
73
+ }
74
+ },
75
+ required: ["file", "oldString", "newString", "userMessage"]
76
+ }
77
+ }
78
+ }
79
+ ],
80
+
81
+ executePathway: async ({args, runAllPrompts, resolver}) => {
82
+ const { file, startLine, endLine, content, oldString, newString, replaceAll = false, contextId, contextKey } = args;
83
+
84
+ // Determine which tool was called based on parameters
85
+ const isSearchReplace = oldString !== undefined && newString !== undefined;
86
+ const isEditByLine = startLine !== undefined && endLine !== undefined && content !== undefined;
87
+ const toolName = isSearchReplace ? "EditFileBySearchAndReplace" : "EditFileByLine";
88
+
89
+ // Validate basic inputs
90
+ if (!file || typeof file !== 'string') {
91
+ const errorResult = {
92
+ success: false,
93
+ error: "file parameter is required and must be a string"
94
+ };
95
+ resolver.tool = JSON.stringify({ toolUsed: toolName });
96
+ return JSON.stringify(errorResult);
97
+ }
98
+
99
+ if (!contextId) {
100
+ const errorResult = {
101
+ success: false,
102
+ error: "contextId is required for file modification"
103
+ };
104
+ resolver.tool = JSON.stringify({ toolUsed: toolName });
105
+ return JSON.stringify(errorResult);
106
+ }
107
+
108
+ // Validate that we have the right parameters for the tool being used
109
+ if (!isSearchReplace && !isEditByLine) {
110
+ const errorResult = {
111
+ success: false,
112
+ error: "Either use EditFileByLine (with startLine/endLine/content) or EditFileBySearchAndReplace (with oldString/newString)"
113
+ };
114
+ resolver.tool = JSON.stringify({ toolUsed: toolName });
115
+ return JSON.stringify(errorResult);
116
+ }
117
+
118
+ // Validate EditFileByLine parameters
119
+ if (isEditByLine) {
120
+ if (typeof startLine !== 'number' || startLine < 1) {
121
+ const errorResult = {
122
+ success: false,
123
+ error: "startLine must be a positive integer (1-indexed)"
124
+ };
125
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
126
+ return JSON.stringify(errorResult);
127
+ }
128
+
129
+ if (typeof endLine !== 'number' || endLine < 1) {
130
+ const errorResult = {
131
+ success: false,
132
+ error: "endLine must be a positive integer (1-indexed)"
133
+ };
134
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
135
+ return JSON.stringify(errorResult);
136
+ }
137
+
138
+ if (endLine < startLine) {
139
+ const errorResult = {
140
+ success: false,
141
+ error: "endLine must be >= startLine"
142
+ };
143
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
144
+ return JSON.stringify(errorResult);
145
+ }
146
+
147
+ if (typeof content !== 'string') {
148
+ const errorResult = {
149
+ success: false,
150
+ error: "content is required and must be a string"
151
+ };
152
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
153
+ return JSON.stringify(errorResult);
154
+ }
155
+ }
156
+
157
+ // Validate EditFileBySearchAndReplace parameters
158
+ if (isSearchReplace) {
159
+ if (typeof oldString !== 'string') {
160
+ const errorResult = {
161
+ success: false,
162
+ error: "oldString is required and must be a string"
163
+ };
164
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileBySearchAndReplace" });
165
+ return JSON.stringify(errorResult);
166
+ }
167
+
168
+ if (typeof newString !== 'string') {
169
+ const errorResult = {
170
+ success: false,
171
+ error: "newString is required and must be a string"
172
+ };
173
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileBySearchAndReplace" });
174
+ return JSON.stringify(errorResult);
175
+ }
176
+ }
177
+
178
+ try {
179
+ // Resolve the file parameter to a URL using the common utility
180
+ const fileUrl = await resolveFileParameter(file, contextId, contextKey);
181
+
182
+ if (!fileUrl) {
183
+ const errorResult = {
184
+ success: false,
185
+ error: `File not found: "${file}". Use ListFileCollection or SearchFileCollection to find available files.`
186
+ };
187
+ resolver.tool = JSON.stringify({ toolUsed: toolName });
188
+ return JSON.stringify(errorResult);
189
+ }
190
+
191
+ // Find the file in the collection to get metadata (for updating later)
192
+ // We'll load it again inside the lock, but need to verify it exists first
193
+ const collection = await loadFileCollection(contextId, contextKey, true);
194
+ const foundFile = findFileInCollection(file, collection);
195
+
196
+ if (!foundFile) {
197
+ const errorResult = {
198
+ success: false,
199
+ error: `File not found in collection: "${file}"`
200
+ };
201
+ resolver.tool = JSON.stringify({ toolUsed: toolName });
202
+ return JSON.stringify(errorResult);
203
+ }
204
+
205
+ // Store the file ID for updating inside the lock
206
+ const fileIdToUpdate = foundFile.id;
207
+
208
+ // Download the current file content
209
+ logger.info(`Downloading file for modification: ${fileUrl}`);
210
+ const downloadResponse = await axios.get(fileUrl, {
211
+ responseType: 'arraybuffer',
212
+ timeout: 60000,
213
+ validateStatus: (status) => status >= 200 && status < 400
214
+ });
215
+
216
+ if (downloadResponse.status !== 200 || !downloadResponse.data) {
217
+ throw new Error(`Failed to download file: ${downloadResponse.status}`);
218
+ }
219
+
220
+ // Explicitly decode as UTF-8 to prevent mojibake (encoding corruption)
221
+ const originalContent = Buffer.from(downloadResponse.data).toString('utf8');
222
+ let modifiedContent;
223
+ let modificationInfo = {};
224
+
225
+ if (isEditByLine) {
226
+ // Line-based replacement mode
227
+ const allLines = originalContent.split(/\r?\n/);
228
+ const totalLines = allLines.length;
229
+
230
+ // Validate line range
231
+ if (startLine > totalLines) {
232
+ const errorResult = {
233
+ success: false,
234
+ error: `startLine (${startLine}) exceeds file length (${totalLines} lines)`
235
+ };
236
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
237
+ return JSON.stringify(errorResult);
238
+ }
239
+
240
+ // Perform the line replacement
241
+ const startIndex = startLine - 1;
242
+ const endIndex = Math.min(endLine, totalLines);
243
+
244
+ // Split the replacement content into lines
245
+ const replacementLines = content.split(/\r?\n/);
246
+
247
+ // Build the modified content
248
+ const beforeLines = allLines.slice(0, startIndex);
249
+ const afterLines = allLines.slice(endIndex);
250
+ const modifiedLines = [...beforeLines, ...replacementLines, ...afterLines];
251
+ modifiedContent = modifiedLines.join('\n');
252
+
253
+ modificationInfo = {
254
+ mode: 'line-based',
255
+ originalLines: totalLines,
256
+ modifiedLines: modifiedLines.length,
257
+ replacedLines: endLine - startLine + 1,
258
+ insertedLines: replacementLines.length,
259
+ startLine: startLine,
260
+ endLine: endLine
261
+ };
262
+ } else if (isSearchReplace) {
263
+ // Search and replace mode
264
+ if (!originalContent.includes(oldString)) {
265
+ const errorResult = {
266
+ success: false,
267
+ error: `oldString not found in file. The exact string must match (including whitespace and newlines).`
268
+ };
269
+ resolver.tool = JSON.stringify({ toolUsed: "EditFileBySearchAndReplace" });
270
+ return JSON.stringify(errorResult);
271
+ }
272
+
273
+ // Count occurrences
274
+ const occurrences = (originalContent.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
275
+
276
+ if (replaceAll) {
277
+ modifiedContent = originalContent.split(oldString).join(newString);
278
+ modificationInfo = {
279
+ mode: 'string-based',
280
+ replaceAll: true,
281
+ occurrencesReplaced: occurrences
282
+ };
283
+ } else {
284
+ // Replace only first occurrence
285
+ modifiedContent = originalContent.replace(oldString, newString);
286
+ modificationInfo = {
287
+ mode: 'string-based',
288
+ replaceAll: false,
289
+ occurrencesReplaced: 1,
290
+ totalOccurrences: occurrences
291
+ };
292
+ }
293
+ }
294
+
295
+ // Determine MIME type from filename using utility function
296
+ const filename = foundFile.filename || 'modified.txt';
297
+ let mimeType = getMimeTypeFromFilename(filename, 'text/plain');
298
+
299
+ // Add charset=utf-8 for text-based MIME types
300
+ if (mimeType.startsWith('text/') || mimeType === 'application/json' ||
301
+ mimeType === 'application/javascript' || mimeType === 'application/typescript' ||
302
+ mimeType === 'application/xml') {
303
+ mimeType = `${mimeType}; charset=utf-8`;
304
+ }
305
+
306
+ // Delete the old file version before uploading the new one
307
+ const oldHash = foundFile.hash;
308
+ if (oldHash) {
309
+ logger.info(`Deleting old file version with hash ${oldHash} before uploading new version`);
310
+ const deleted = await deleteFileByHash(oldHash, resolver);
311
+ if (deleted) {
312
+ logger.info(`Successfully deleted old file version`);
313
+ } else {
314
+ logger.info(`Old file version not found or already deleted (hash: ${oldHash})`);
315
+ }
316
+ } else {
317
+ logger.info(`No hash found for old file, skipping deletion`);
318
+ }
319
+
320
+ // Upload the modified file
321
+ const fileBuffer = Buffer.from(modifiedContent, 'utf8');
322
+ const uploadResult = await uploadFileToCloud(
323
+ fileBuffer,
324
+ mimeType,
325
+ filename,
326
+ resolver
327
+ );
328
+
329
+ if (!uploadResult || !uploadResult.url) {
330
+ throw new Error('Failed to upload modified file to cloud storage');
331
+ }
332
+
333
+ // Update the file collection entry with new URL and hash using optimistic locking
334
+ const updatedCollection = await modifyFileCollectionWithLock(contextId, contextKey, (collection) => {
335
+ const fileToUpdate = collection.find(f => f.id === fileIdToUpdate);
336
+ if (!fileToUpdate) {
337
+ throw new Error(`File with ID "${fileIdToUpdate}" not found in collection during update`);
338
+ }
339
+
340
+ fileToUpdate.url = uploadResult.url;
341
+ if (uploadResult.gcs) {
342
+ fileToUpdate.gcs = uploadResult.gcs;
343
+ }
344
+ if (uploadResult.hash) {
345
+ fileToUpdate.hash = uploadResult.hash;
346
+ }
347
+ fileToUpdate.lastAccessed = new Date().toISOString();
348
+
349
+ return collection;
350
+ });
351
+
352
+ // Get the updated file info for the result
353
+ const updatedFile = updatedCollection.find(f => f.id === fileIdToUpdate);
354
+
355
+ // Build result message
356
+ let message;
357
+ if (isEditByLine) {
358
+ message = `File "${filename}" modified successfully. Replaced lines ${startLine}-${endLine} (${endLine - startLine + 1} lines) with ${modificationInfo.insertedLines} line(s).`;
359
+ } else if (isSearchReplace) {
360
+ if (replaceAll) {
361
+ message = `File "${filename}" modified successfully. Replaced all ${modificationInfo.occurrencesReplaced} occurrence(s) of the specified string.`;
362
+ } else {
363
+ message = `File "${filename}" modified successfully. Replaced first occurrence of the specified string${modificationInfo.totalOccurrences > 1 ? ` (${modificationInfo.totalOccurrences} total occurrences found)` : ''}.`;
364
+ }
365
+ }
366
+
367
+ const result = {
368
+ success: true,
369
+ filename: filename,
370
+ fileId: updatedFile.id,
371
+ url: uploadResult.url,
372
+ gcs: uploadResult.gcs || null,
373
+ hash: uploadResult.hash || null,
374
+ ...modificationInfo,
375
+ message: message
376
+ };
377
+
378
+ resolver.tool = JSON.stringify({ toolUsed: toolName });
379
+ return JSON.stringify(result);
380
+
381
+ } catch (error) {
382
+ let errorMsg;
383
+ if (error?.message) {
384
+ errorMsg = error.message;
385
+ } else if (error?.errors && Array.isArray(error.errors)) {
386
+ // Handle AggregateError
387
+ errorMsg = error.errors.map(e => e?.message || String(e)).join('; ');
388
+ } else {
389
+ errorMsg = String(error);
390
+ }
391
+ logger.error(`Error modifying file: ${errorMsg}`);
392
+
393
+ const errorResult = {
394
+ success: false,
395
+ error: errorMsg
396
+ };
397
+
398
+ resolver.tool = JSON.stringify({ toolUsed: toolName });
399
+ return JSON.stringify(errorResult);
400
+ }
401
+ }
402
+ };
403
+