@aj-archipelago/cortex 1.4.7 → 1.4.8

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.
package/lib/fileUtils.js CHANGED
@@ -983,10 +983,7 @@ async function generateFileMessageContent(fileParam, contextId, contextKey = nul
983
983
  if (!contextId) {
984
984
  // Without contextId, we can't look up in collection
985
985
  // Return a basic content object from the URL
986
- return {
987
- type: 'file',
988
- url: fileParam
989
- };
986
+ return null;
990
987
  }
991
988
 
992
989
  // Load file collection
@@ -1000,31 +997,14 @@ async function generateFileMessageContent(fileParam, contextId, contextKey = nul
1000
997
  return null;
1001
998
  }
1002
999
 
1003
- // Determine file type based on filename extension or existing type
1004
- const filename = foundFile.filename || '';
1005
- const extension = filename.split('.').pop()?.toLowerCase() || '';
1006
- const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension);
1007
- const fileType = foundFile.type || (isImage ? 'image_url' : 'file');
1008
-
1009
- // Create content object in the proper format for plugins (url and gcs)
1010
- if (fileType === 'image_url') {
1011
- return {
1012
- type: 'image_url',
1013
- image_url: { url: foundFile.url },
1014
- url: foundFile.url,
1015
- gcs: foundFile.gcs || null,
1016
- originalFilename: foundFile.filename || null,
1017
- hash: foundFile.hash || null
1018
- };
1019
- } else {
1020
- return {
1021
- type: 'file',
1022
- url: foundFile.url,
1023
- gcs: foundFile.gcs || null,
1024
- originalFilename: foundFile.filename || null,
1025
- hash: foundFile.hash || null
1026
- };
1027
- }
1000
+ return {
1001
+ type: 'image_url',
1002
+ url: foundFile.url,
1003
+ gcs: foundFile.gcs || null,
1004
+ originalFilename: foundFile.filename || null,
1005
+ hash: foundFile.hash || null
1006
+ };
1007
+
1028
1008
  }
1029
1009
 
1030
1010
  /**
@@ -1448,6 +1428,35 @@ function getMimeTypeFromExtension(extension, defaultMimeType = 'application/octe
1448
1428
  return mimeType || defaultMimeType;
1449
1429
  }
1450
1430
 
1431
+ /**
1432
+ * Check if a MIME type represents a text-based file that can be read as text
1433
+ * @param {string} mimeType - MIME type to check
1434
+ * @returns {boolean} - Returns true if it's a text-based MIME type
1435
+ */
1436
+ function isTextMimeType(mimeType) {
1437
+ if (!mimeType || typeof mimeType !== 'string') {
1438
+ return false;
1439
+ }
1440
+
1441
+ const mimeTypeLower = mimeType.toLowerCase();
1442
+
1443
+ // All text/* types
1444
+ if (mimeTypeLower.startsWith('text/')) {
1445
+ return true;
1446
+ }
1447
+
1448
+ // Text-based application types (consistent with sys_tool_writefile and sys_tool_editfile)
1449
+ const textApplicationTypes = [
1450
+ 'application/json',
1451
+ 'application/javascript',
1452
+ 'application/x-javascript',
1453
+ 'application/typescript',
1454
+ 'application/xml',
1455
+ ];
1456
+
1457
+ return textApplicationTypes.includes(mimeTypeLower);
1458
+ }
1459
+
1451
1460
  export {
1452
1461
  computeFileHash,
1453
1462
  computeBufferHash,
@@ -1476,6 +1485,7 @@ export {
1476
1485
  uploadImageToCloud,
1477
1486
  resolveFileHashesToContent,
1478
1487
  getMimeTypeFromFilename,
1479
- getMimeTypeFromExtension
1488
+ getMimeTypeFromExtension,
1489
+ isTextMimeType
1480
1490
  };
1481
1491
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -2,7 +2,7 @@
2
2
  // Entity tool that modifies existing files by replacing line ranges or exact string matches
3
3
  import logger from '../../../../lib/logger.js';
4
4
  import { axios } from '../../../../lib/requestExecutor.js';
5
- import { uploadFileToCloud, findFileInCollection, loadFileCollection, saveFileCollection, getMimeTypeFromFilename, resolveFileParameter, deleteFileByHash, modifyFileCollectionWithLock } from '../../../../lib/fileUtils.js';
5
+ import { uploadFileToCloud, findFileInCollection, loadFileCollection, saveFileCollection, getMimeTypeFromFilename, resolveFileParameter, deleteFileByHash, modifyFileCollectionWithLock, isTextMimeType } from '../../../../lib/fileUtils.js';
6
6
 
7
7
  export default {
8
8
  prompt: [],
@@ -297,9 +297,7 @@ export default {
297
297
  let mimeType = getMimeTypeFromFilename(filename, 'text/plain');
298
298
 
299
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') {
300
+ if (isTextMimeType(mimeType)) {
303
301
  mimeType = `${mimeType}; charset=utf-8`;
304
302
  }
305
303
 
@@ -2,7 +2,32 @@
2
2
  // Tool pathway that reads text files with line number support
3
3
  import logger from '../../../../lib/logger.js';
4
4
  import { axios } from '../../../../lib/requestExecutor.js';
5
- import { resolveFileParameter } from '../../../../lib/fileUtils.js';
5
+ import { resolveFileParameter, getMimeTypeFromFilename, isTextMimeType } from '../../../../lib/fileUtils.js';
6
+
7
+ /**
8
+ * Check if a file is a text file type that can be read
9
+ * @param {string} url - File URL or path
10
+ * @returns {boolean} - Returns true if it's a text file, false otherwise
11
+ */
12
+ function isTextFile(url) {
13
+ if (!url || typeof url !== 'string') {
14
+ return false;
15
+ }
16
+
17
+ // Extract filename from URL (remove query string and fragment)
18
+ const urlPath = url.split('?')[0].split('#')[0];
19
+
20
+ // Use existing MIME utility to get MIME type
21
+ const mimeType = getMimeTypeFromFilename(urlPath);
22
+
23
+ if (!mimeType || mimeType === 'application/octet-stream') {
24
+ // Unknown MIME type, reject to be safe
25
+ return false;
26
+ }
27
+
28
+ // Use shared utility function for consistency with other tools
29
+ return isTextMimeType(mimeType);
30
+ }
6
31
 
7
32
  export default {
8
33
  prompt: [],
@@ -11,8 +36,8 @@ export default {
11
36
  type: "function",
12
37
  icon: "📖",
13
38
  function: {
14
- name: "ReadFile",
15
- description: "Read text content from a file. Can read the entire file or specific line ranges. Use this to access and analyze text files from your file collection. Supports text files, markdown files, html, csv, and other document formats that can be converted to text, but not images, videos, or audio files or pdfs.",
39
+ name: "ReadTextFile",
40
+ description: "Read text content from a file. Can read the entire file or specific line ranges. Use this to access and analyze text files from your file collection. Supports text files, markdown files, html, csv, and other document formats that can be converted to text. DOES NOT support binary files, images, videos, or audio files or pdfs.",
16
41
  parameters: {
17
42
  type: "object",
18
43
  properties: {
@@ -77,6 +102,19 @@ export default {
77
102
  return JSON.stringify(errorResult);
78
103
  }
79
104
 
105
+ // Check if file is a text type before attempting to read
106
+ if (!isTextFile(cloudUrl)) {
107
+ const mimeType = getMimeTypeFromFilename(cloudUrl.split('?')[0].split('#')[0]);
108
+ const detectedType = mimeType || 'unknown type';
109
+
110
+ const errorResult = {
111
+ success: false,
112
+ error: `This tool only supports text files. The file appears to be a non-text file (MIME type: ${detectedType}). For images, PDFs, videos, or other non-text files, please use the AnalyzeImage, AnalyzePDF, or AnalyzeVideo tools instead.`
113
+ };
114
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
115
+ return JSON.stringify(errorResult);
116
+ }
117
+
80
118
  if (startLine !== undefined) {
81
119
  if (typeof startLine !== 'number' || !Number.isInteger(startLine) || startLine < 1) {
82
120
  const errorResult = {
@@ -130,6 +168,17 @@ export default {
130
168
  throw new Error(`Failed to download file content: ${response.status}`);
131
169
  }
132
170
 
171
+ // Secondary check: verify content-type header if available
172
+ const contentType = response.headers['content-type'] || response.headers['Content-Type'];
173
+ if (contentType && !isTextMimeType(contentType)) {
174
+ const errorResult = {
175
+ success: false,
176
+ error: `This tool only supports text files. The file appears to be a non-text file (Content-Type: ${contentType}). For images, PDFs, videos, or other non-text files, please use the AnalyzeImage, AnalyzePDF, or AnalyzeVideo tools instead.`
177
+ };
178
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
179
+ return JSON.stringify(errorResult);
180
+ }
181
+
133
182
  // Explicitly decode as UTF-8 to prevent mojibake (encoding corruption)
134
183
  const textContent = Buffer.from(response.data).toString('utf8');
135
184
  const allLines = textContent.split(/\r?\n/);
@@ -1,7 +1,7 @@
1
1
  // sys_tool_writefile.js
2
2
  // Entity tool that writes content to a file and uploads it to cloud storage
3
3
  import logger from '../../../../lib/logger.js';
4
- import { uploadFileToCloud, addFileToCollection, getMimeTypeFromFilename } from '../../../../lib/fileUtils.js';
4
+ import { uploadFileToCloud, addFileToCollection, getMimeTypeFromFilename, isTextMimeType } from '../../../../lib/fileUtils.js';
5
5
 
6
6
  // Helper function to format file size
7
7
  function formatFileSize(bytes) {
@@ -129,9 +129,7 @@ export default {
129
129
  let mimeType = getMimeTypeFromFilename(filename, 'text/plain');
130
130
 
131
131
  // Add charset=utf-8 for text-based MIME types to ensure proper encoding
132
- if (mimeType.startsWith('text/') || mimeType === 'application/json' ||
133
- mimeType === 'application/javascript' || mimeType === 'application/typescript' ||
134
- mimeType === 'application/xml') {
132
+ if (isTextMimeType(mimeType)) {
135
133
  mimeType = `${mimeType}; charset=utf-8`;
136
134
  }
137
135
 
@@ -518,7 +518,7 @@ test('generateFileMessageContent should find file by ID', async t => {
518
518
  const result = await generateFileMessageContent(fileId, contextId);
519
519
 
520
520
  t.truthy(result);
521
- t.is(result.type, 'file');
521
+ t.is(result.type, 'image_url');
522
522
  t.is(result.url, 'https://example.com/test.pdf');
523
523
  t.is(result.gcs, 'gs://bucket/test.pdf');
524
524
  t.is(result.originalFilename, 'test.pdf');
@@ -607,8 +607,7 @@ test('generateFileMessageContent should detect image type', async t => {
607
607
 
608
608
  t.truthy(result);
609
609
  t.is(result.type, 'image_url');
610
- t.truthy(result.image_url);
611
- t.is(result.image_url.url, 'https://example.com/image.jpg');
610
+ t.is(result.url, 'https://example.com/image.jpg');
612
611
  } finally {
613
612
  await cleanup(contextId);
614
613
  }
@@ -239,12 +239,10 @@ test('generateFileMessageContent should return null for invalid input', async t
239
239
  t.is(await generateFileMessageContent(123, 'context-1'), null);
240
240
  });
241
241
 
242
- test('generateFileMessageContent should return basic object when no contextId', async t => {
242
+ test('generateFileMessageContent should return null when no contextId', async t => {
243
243
  const result = await generateFileMessageContent('https://example.com/file.pdf', null);
244
244
 
245
- t.truthy(result);
246
- t.is(result.type, 'file');
247
- t.is(result.url, 'https://example.com/file.pdf');
245
+ t.is(result, null);
248
246
  });
249
247
 
250
248
  test('generateFileMessageContent should return null for file not in collection', async t => {