@dexto/tools-filesystem 1.6.20 → 1.6.22

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.
@@ -38,6 +38,7 @@ var import_safe_regex = __toESM(require("safe-regex"), 1);
38
38
  var import_core = require("@dexto/core");
39
39
  var import_path_validator = require("./path-validator.js");
40
40
  var import_errors = require("./errors.js");
41
+ var import_mime_utils = require("./mime-utils.js");
41
42
  const DEFAULT_ENCODING = "utf-8";
42
43
  const DEFAULT_MAX_RESULTS = 1e3;
43
44
  const DEFAULT_MAX_SEARCH_RESULTS = 100;
@@ -195,7 +196,17 @@ class FileSystemService {
195
196
  }
196
197
  try {
197
198
  const encoding = options.encoding || DEFAULT_ENCODING;
198
- const content = await fs.readFile(normalizedPath, encoding);
199
+ const rawContent = await fs.readFile(normalizedPath);
200
+ const mimeType = (0, import_mime_utils.detectMimeType)(normalizedPath, rawContent);
201
+ const binaryLike = (0, import_mime_utils.isLikelyBinary)(rawContent);
202
+ const canReadAsText = !binaryLike && ((0, import_mime_utils.isTextMimeType)(mimeType) || mimeType === "image/svg+xml");
203
+ if (!canReadAsText) {
204
+ throw import_errors.FileSystemError.readFailed(
205
+ normalizedPath,
206
+ `File is binary (${mimeType}). Use read_media_file instead.`
207
+ );
208
+ }
209
+ const content = rawContent.toString(encoding);
199
210
  const lines = content.split("\n");
200
211
  const limit = options.limit;
201
212
  const offset1 = options.offset;
@@ -214,6 +225,7 @@ class FileSystemService {
214
225
  content: returnedContent,
215
226
  lines: selectedLines.length,
216
227
  encoding,
228
+ mimeType,
217
229
  truncated,
218
230
  size: Buffer.byteLength(returnedContent, encoding)
219
231
  };
@@ -235,6 +247,55 @@ class FileSystemService {
235
247
  const normalizedPath = await this.validateReadPath(filePath, "execute");
236
248
  return await this.readNormalizedFile(normalizedPath, options);
237
249
  }
250
+ /**
251
+ * Read a media or binary file and return base64-encoded data with MIME metadata.
252
+ */
253
+ async readMediaFile(filePath) {
254
+ await this.ensureInitialized();
255
+ const normalizedPath = await this.validateReadPath(filePath, "execute");
256
+ try {
257
+ const stats = await fs.stat(normalizedPath);
258
+ if (!stats.isFile()) {
259
+ throw import_errors.FileSystemError.invalidPath(normalizedPath, "Path is not a file");
260
+ }
261
+ if (stats.size > this.config.maxFileSize) {
262
+ throw import_errors.FileSystemError.fileTooLarge(
263
+ normalizedPath,
264
+ stats.size,
265
+ this.config.maxFileSize
266
+ );
267
+ }
268
+ const rawContent = await fs.readFile(normalizedPath);
269
+ const mimeType = (0, import_mime_utils.detectMimeType)(normalizedPath, rawContent);
270
+ if ((0, import_mime_utils.isTextMimeType)(mimeType) && !(0, import_mime_utils.isLikelyBinary)(rawContent)) {
271
+ throw import_errors.FileSystemError.readFailed(
272
+ normalizedPath,
273
+ `File is text (${mimeType}). Use read_file instead.`
274
+ );
275
+ }
276
+ return {
277
+ data: rawContent.toString("base64"),
278
+ mimeType,
279
+ filename: path.basename(normalizedPath),
280
+ kind: (0, import_mime_utils.getMediaFileKind)(mimeType),
281
+ size: stats.size
282
+ };
283
+ } catch (error) {
284
+ if (error instanceof import_core.DextoRuntimeError && error.scope === "filesystem") {
285
+ throw error;
286
+ }
287
+ if (error.code === "ENOENT") {
288
+ throw import_errors.FileSystemError.fileNotFound(normalizedPath);
289
+ }
290
+ if (error.code === "EACCES") {
291
+ throw import_errors.FileSystemError.permissionDenied(normalizedPath, "read");
292
+ }
293
+ throw import_errors.FileSystemError.readFailed(
294
+ normalizedPath,
295
+ error instanceof Error ? error.message : String(error)
296
+ );
297
+ }
298
+ }
238
299
  /**
239
300
  * Preview-only file read that bypasses config-allowed roots.
240
301
  *
@@ -4,7 +4,7 @@
4
4
  * Secure file system operations for Dexto internal tools
5
5
  */
6
6
  import { Logger } from '@dexto/core';
7
- import { FileSystemConfig, FileContent, ReadFileOptions, GlobOptions, GlobResult, GrepOptions, SearchResult, WriteFileOptions, WriteResult, EditFileOptions, EditResult, EditOperation, ListDirectoryOptions, ListDirectoryResult, CreateDirectoryOptions, CreateDirectoryResult, DeletePathOptions, DeletePathResult, RenamePathResult } from './types.js';
7
+ import { FileSystemConfig, FileContent, MediaFileContent, ReadFileOptions, GlobOptions, GlobResult, GrepOptions, SearchResult, WriteFileOptions, WriteResult, EditFileOptions, EditResult, EditOperation, ListDirectoryOptions, ListDirectoryResult, CreateDirectoryOptions, CreateDirectoryResult, DeletePathOptions, DeletePathResult, RenamePathResult } from './types.js';
8
8
  /**
9
9
  * FileSystemService - Handles all file system operations with security checks
10
10
  *
@@ -80,6 +80,10 @@ export declare class FileSystemService {
80
80
  * Read a file with validation and size limits
81
81
  */
82
82
  readFile(filePath: string, options?: ReadFileOptions): Promise<FileContent>;
83
+ /**
84
+ * Read a media or binary file and return base64-encoded data with MIME metadata.
85
+ */
86
+ readMediaFile(filePath: string): Promise<MediaFileContent>;
83
87
  /**
84
88
  * Preview-only file read that bypasses config-allowed roots.
85
89
  *
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-service.d.ts","sourceRoot":"","sources":["../src/filesystem-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAmC,MAAM,EAAqB,MAAM,aAAa,CAAC;AACzF,OAAO,EACH,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,WAAW,EACX,UAAU,EACV,WAAW,EACX,YAAY,EAEZ,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EAGb,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAEnB,MAAM,YAAY,CAAC;AAUpB;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC1B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,wBAAwB,CAAC,CAAgC;IAEjE;;;;;;OAMG;gBACS,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM;IAQpD;;;OAGG;IACH,OAAO,CAAC,YAAY;IAKpB;;;OAGG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;;OAGG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B;;OAEG;YACW,YAAY;IAuB1B;;;;OAIG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOxC;;;;;OAKG;IACH,2BAA2B,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI;IAKzE;;;OAGG;IACH,mBAAmB,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI;IAanD;;;;;;OAMG;IACG,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAIrD,gBAAgB;YAchB,kBAAkB;IAgFhC;;OAEG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,WAAW,CAAC;IAOrF;;;;;;OAMG;IACG,sBAAsB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAoB,GAC9B,OAAO,CAAC,WAAW,CAAC;IAOvB;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAsEhF;;OAEG;IACG,aAAa,CACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACnC,OAAO,CAAC,mBAAmB,CAAC;YAuJjB,kBAAkB;IA+BhC;;OAEG;IACG,eAAe,CACjB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACrC,OAAO,CAAC,qBAAqB,CAAC;IAqCjC;;OAEG;IACG,UAAU,CACZ,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,iBAAsB,GAChC,OAAO,CAAC,gBAAgB,CAAC;IA4B5B;;OAEG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA6D7E;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,YAAY,CAAC;IAyItF;;OAEG;IACG,SAAS,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,gBAAqB,GAC/B,OAAO,CAAC,WAAW,CAAC;IAwDvB;;OAEG;IACG,QAAQ,CACV,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,aAAa,EACxB,OAAO,GAAE,eAAoB,GAC9B,OAAO,CAAC,UAAU,CAAC;IAqEtB;;OAEG;YACW,YAAY;IA0B1B;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IA6D1C;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,gBAAgB,CAAC;IAIvC;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI1D"}
1
+ {"version":3,"file":"filesystem-service.d.ts","sourceRoot":"","sources":["../src/filesystem-service.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,7 @@ import safeRegex from "safe-regex";
5
5
  import { DextoRuntimeError, getDextoPath, DextoLogComponent } from "@dexto/core";
6
6
  import { PathValidator } from "./path-validator.js";
7
7
  import { FileSystemError } from "./errors.js";
8
+ import { detectMimeType, getMediaFileKind, isLikelyBinary, isTextMimeType } from "./mime-utils.js";
8
9
  const DEFAULT_ENCODING = "utf-8";
9
10
  const DEFAULT_MAX_RESULTS = 1e3;
10
11
  const DEFAULT_MAX_SEARCH_RESULTS = 100;
@@ -162,7 +163,17 @@ class FileSystemService {
162
163
  }
163
164
  try {
164
165
  const encoding = options.encoding || DEFAULT_ENCODING;
165
- const content = await fs.readFile(normalizedPath, encoding);
166
+ const rawContent = await fs.readFile(normalizedPath);
167
+ const mimeType = detectMimeType(normalizedPath, rawContent);
168
+ const binaryLike = isLikelyBinary(rawContent);
169
+ const canReadAsText = !binaryLike && (isTextMimeType(mimeType) || mimeType === "image/svg+xml");
170
+ if (!canReadAsText) {
171
+ throw FileSystemError.readFailed(
172
+ normalizedPath,
173
+ `File is binary (${mimeType}). Use read_media_file instead.`
174
+ );
175
+ }
176
+ const content = rawContent.toString(encoding);
166
177
  const lines = content.split("\n");
167
178
  const limit = options.limit;
168
179
  const offset1 = options.offset;
@@ -181,6 +192,7 @@ class FileSystemService {
181
192
  content: returnedContent,
182
193
  lines: selectedLines.length,
183
194
  encoding,
195
+ mimeType,
184
196
  truncated,
185
197
  size: Buffer.byteLength(returnedContent, encoding)
186
198
  };
@@ -202,6 +214,55 @@ class FileSystemService {
202
214
  const normalizedPath = await this.validateReadPath(filePath, "execute");
203
215
  return await this.readNormalizedFile(normalizedPath, options);
204
216
  }
217
+ /**
218
+ * Read a media or binary file and return base64-encoded data with MIME metadata.
219
+ */
220
+ async readMediaFile(filePath) {
221
+ await this.ensureInitialized();
222
+ const normalizedPath = await this.validateReadPath(filePath, "execute");
223
+ try {
224
+ const stats = await fs.stat(normalizedPath);
225
+ if (!stats.isFile()) {
226
+ throw FileSystemError.invalidPath(normalizedPath, "Path is not a file");
227
+ }
228
+ if (stats.size > this.config.maxFileSize) {
229
+ throw FileSystemError.fileTooLarge(
230
+ normalizedPath,
231
+ stats.size,
232
+ this.config.maxFileSize
233
+ );
234
+ }
235
+ const rawContent = await fs.readFile(normalizedPath);
236
+ const mimeType = detectMimeType(normalizedPath, rawContent);
237
+ if (isTextMimeType(mimeType) && !isLikelyBinary(rawContent)) {
238
+ throw FileSystemError.readFailed(
239
+ normalizedPath,
240
+ `File is text (${mimeType}). Use read_file instead.`
241
+ );
242
+ }
243
+ return {
244
+ data: rawContent.toString("base64"),
245
+ mimeType,
246
+ filename: path.basename(normalizedPath),
247
+ kind: getMediaFileKind(mimeType),
248
+ size: stats.size
249
+ };
250
+ } catch (error) {
251
+ if (error instanceof DextoRuntimeError && error.scope === "filesystem") {
252
+ throw error;
253
+ }
254
+ if (error.code === "ENOENT") {
255
+ throw FileSystemError.fileNotFound(normalizedPath);
256
+ }
257
+ if (error.code === "EACCES") {
258
+ throw FileSystemError.permissionDenied(normalizedPath, "read");
259
+ }
260
+ throw FileSystemError.readFailed(
261
+ normalizedPath,
262
+ error instanceof Error ? error.message : String(error)
263
+ );
264
+ }
265
+ }
205
266
  /**
206
267
  * Preview-only file read that bypasses config-allowed roots.
207
268
  *
@@ -26,13 +26,23 @@ var path = __toESM(require("node:path"), 1);
26
26
  var fs = __toESM(require("node:fs/promises"), 1);
27
27
  var os = __toESM(require("node:os"), 1);
28
28
  var import_filesystem_service = require("./filesystem-service.js");
29
- const createMockLogger = () => ({
30
- debug: import_vitest.vi.fn(),
31
- info: import_vitest.vi.fn(),
32
- warn: import_vitest.vi.fn(),
33
- error: import_vitest.vi.fn(),
34
- createChild: import_vitest.vi.fn().mockReturnThis()
35
- });
29
+ const createMockLogger = () => {
30
+ const logger = {
31
+ debug: import_vitest.vi.fn(),
32
+ silly: import_vitest.vi.fn(),
33
+ info: import_vitest.vi.fn(),
34
+ warn: import_vitest.vi.fn(),
35
+ error: import_vitest.vi.fn(),
36
+ trackException: import_vitest.vi.fn(),
37
+ createChild: import_vitest.vi.fn(() => logger),
38
+ createFileOnlyChild: import_vitest.vi.fn(() => logger),
39
+ setLevel: import_vitest.vi.fn(),
40
+ getLevel: import_vitest.vi.fn(() => "debug"),
41
+ getLogFilePath: import_vitest.vi.fn(() => null),
42
+ destroy: import_vitest.vi.fn(async () => void 0)
43
+ };
44
+ return logger;
45
+ };
36
46
  (0, import_vitest.describe)("FileSystemService", () => {
37
47
  let mockLogger;
38
48
  let tempDir;
@@ -50,6 +60,112 @@ const createMockLogger = () => ({
50
60
  } catch {
51
61
  }
52
62
  });
63
+ (0, import_vitest.describe)("Read Modes", () => {
64
+ (0, import_vitest.it)("returns MIME metadata for text reads", async () => {
65
+ const fileSystemService = new import_filesystem_service.FileSystemService(
66
+ {
67
+ allowedPaths: [tempDir],
68
+ blockedPaths: [],
69
+ blockedExtensions: [],
70
+ maxFileSize: 10 * 1024 * 1024,
71
+ workingDirectory: tempDir,
72
+ enableBackups: false,
73
+ backupRetentionDays: 7
74
+ },
75
+ mockLogger
76
+ );
77
+ await fileSystemService.initialize();
78
+ const testFile = path.join(tempDir, "notes.md");
79
+ await fs.writeFile(testFile, "# hello\nworld");
80
+ const result = await fileSystemService.readFile(testFile);
81
+ (0, import_vitest.expect)(result.content).toBe("# hello\nworld");
82
+ (0, import_vitest.expect)(result.mimeType).toBe("text/markdown");
83
+ });
84
+ (0, import_vitest.it)("rejects binary files from readFile and points callers to readMediaFile", async () => {
85
+ const fileSystemService = new import_filesystem_service.FileSystemService(
86
+ {
87
+ allowedPaths: [tempDir],
88
+ blockedPaths: [],
89
+ blockedExtensions: [],
90
+ maxFileSize: 10 * 1024 * 1024,
91
+ workingDirectory: tempDir,
92
+ enableBackups: false,
93
+ backupRetentionDays: 7
94
+ },
95
+ mockLogger
96
+ );
97
+ await fileSystemService.initialize();
98
+ const testFile = path.join(tempDir, "image.png");
99
+ await fs.writeFile(testFile, Buffer.from([137, 80, 78, 71, 0, 1]));
100
+ await (0, import_vitest.expect)(fileSystemService.readFile(testFile)).rejects.toThrow(
101
+ /Use read_media_file instead/
102
+ );
103
+ });
104
+ (0, import_vitest.it)("rejects binary-like content even when the extension looks textual", async () => {
105
+ const fileSystemService = new import_filesystem_service.FileSystemService(
106
+ {
107
+ allowedPaths: [tempDir],
108
+ blockedPaths: [],
109
+ blockedExtensions: [],
110
+ maxFileSize: 10 * 1024 * 1024,
111
+ workingDirectory: tempDir,
112
+ enableBackups: false,
113
+ backupRetentionDays: 7
114
+ },
115
+ mockLogger
116
+ );
117
+ await fileSystemService.initialize();
118
+ const testFile = path.join(tempDir, "data.json");
119
+ await fs.writeFile(testFile, Buffer.from([123, 0, 34, 120, 34, 125]));
120
+ await (0, import_vitest.expect)(fileSystemService.readFile(testFile)).rejects.toThrow(
121
+ /Use read_media_file instead/
122
+ );
123
+ });
124
+ (0, import_vitest.it)("reads media files as base64 with MIME-aware kind metadata", async () => {
125
+ const fileSystemService = new import_filesystem_service.FileSystemService(
126
+ {
127
+ allowedPaths: [tempDir],
128
+ blockedPaths: [],
129
+ blockedExtensions: [],
130
+ maxFileSize: 10 * 1024 * 1024,
131
+ workingDirectory: tempDir,
132
+ enableBackups: false,
133
+ backupRetentionDays: 7
134
+ },
135
+ mockLogger
136
+ );
137
+ await fileSystemService.initialize();
138
+ const testFile = path.join(tempDir, "clip.mp4");
139
+ const buffer = Buffer.from([0, 0, 0, 24, 102, 116, 121, 112]);
140
+ await fs.writeFile(testFile, buffer);
141
+ const result = await fileSystemService.readMediaFile(testFile);
142
+ (0, import_vitest.expect)(result.data).toBe(buffer.toString("base64"));
143
+ (0, import_vitest.expect)(result.mimeType).toBe("video/mp4");
144
+ (0, import_vitest.expect)(result.kind).toBe("video");
145
+ (0, import_vitest.expect)(result.filename).toBe("clip.mp4");
146
+ (0, import_vitest.expect)(result.size).toBe(buffer.length);
147
+ });
148
+ (0, import_vitest.it)("rejects text files from readMediaFile and points callers to readFile", async () => {
149
+ const fileSystemService = new import_filesystem_service.FileSystemService(
150
+ {
151
+ allowedPaths: [tempDir],
152
+ blockedPaths: [],
153
+ blockedExtensions: [],
154
+ maxFileSize: 10 * 1024 * 1024,
155
+ workingDirectory: tempDir,
156
+ enableBackups: false,
157
+ backupRetentionDays: 7
158
+ },
159
+ mockLogger
160
+ );
161
+ await fileSystemService.initialize();
162
+ const testFile = path.join(tempDir, "plain.txt");
163
+ await fs.writeFile(testFile, "hello world");
164
+ await (0, import_vitest.expect)(fileSystemService.readMediaFile(testFile)).rejects.toThrow(
165
+ /Use read_file instead/
166
+ );
167
+ });
168
+ });
53
169
  (0, import_vitest.describe)("Backup Behavior", () => {
54
170
  (0, import_vitest.describe)("writeFile", () => {
55
171
  (0, import_vitest.it)("should NOT create backup when enableBackups is false (default)", async () => {
@@ -3,13 +3,23 @@ import * as path from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as os from "node:os";
5
5
  import { FileSystemService } from "./filesystem-service.js";
6
- const createMockLogger = () => ({
7
- debug: vi.fn(),
8
- info: vi.fn(),
9
- warn: vi.fn(),
10
- error: vi.fn(),
11
- createChild: vi.fn().mockReturnThis()
12
- });
6
+ const createMockLogger = () => {
7
+ const logger = {
8
+ debug: vi.fn(),
9
+ silly: vi.fn(),
10
+ info: vi.fn(),
11
+ warn: vi.fn(),
12
+ error: vi.fn(),
13
+ trackException: vi.fn(),
14
+ createChild: vi.fn(() => logger),
15
+ createFileOnlyChild: vi.fn(() => logger),
16
+ setLevel: vi.fn(),
17
+ getLevel: vi.fn(() => "debug"),
18
+ getLogFilePath: vi.fn(() => null),
19
+ destroy: vi.fn(async () => void 0)
20
+ };
21
+ return logger;
22
+ };
13
23
  describe("FileSystemService", () => {
14
24
  let mockLogger;
15
25
  let tempDir;
@@ -27,6 +37,112 @@ describe("FileSystemService", () => {
27
37
  } catch {
28
38
  }
29
39
  });
40
+ describe("Read Modes", () => {
41
+ it("returns MIME metadata for text reads", async () => {
42
+ const fileSystemService = new FileSystemService(
43
+ {
44
+ allowedPaths: [tempDir],
45
+ blockedPaths: [],
46
+ blockedExtensions: [],
47
+ maxFileSize: 10 * 1024 * 1024,
48
+ workingDirectory: tempDir,
49
+ enableBackups: false,
50
+ backupRetentionDays: 7
51
+ },
52
+ mockLogger
53
+ );
54
+ await fileSystemService.initialize();
55
+ const testFile = path.join(tempDir, "notes.md");
56
+ await fs.writeFile(testFile, "# hello\nworld");
57
+ const result = await fileSystemService.readFile(testFile);
58
+ expect(result.content).toBe("# hello\nworld");
59
+ expect(result.mimeType).toBe("text/markdown");
60
+ });
61
+ it("rejects binary files from readFile and points callers to readMediaFile", async () => {
62
+ const fileSystemService = new FileSystemService(
63
+ {
64
+ allowedPaths: [tempDir],
65
+ blockedPaths: [],
66
+ blockedExtensions: [],
67
+ maxFileSize: 10 * 1024 * 1024,
68
+ workingDirectory: tempDir,
69
+ enableBackups: false,
70
+ backupRetentionDays: 7
71
+ },
72
+ mockLogger
73
+ );
74
+ await fileSystemService.initialize();
75
+ const testFile = path.join(tempDir, "image.png");
76
+ await fs.writeFile(testFile, Buffer.from([137, 80, 78, 71, 0, 1]));
77
+ await expect(fileSystemService.readFile(testFile)).rejects.toThrow(
78
+ /Use read_media_file instead/
79
+ );
80
+ });
81
+ it("rejects binary-like content even when the extension looks textual", async () => {
82
+ const fileSystemService = new FileSystemService(
83
+ {
84
+ allowedPaths: [tempDir],
85
+ blockedPaths: [],
86
+ blockedExtensions: [],
87
+ maxFileSize: 10 * 1024 * 1024,
88
+ workingDirectory: tempDir,
89
+ enableBackups: false,
90
+ backupRetentionDays: 7
91
+ },
92
+ mockLogger
93
+ );
94
+ await fileSystemService.initialize();
95
+ const testFile = path.join(tempDir, "data.json");
96
+ await fs.writeFile(testFile, Buffer.from([123, 0, 34, 120, 34, 125]));
97
+ await expect(fileSystemService.readFile(testFile)).rejects.toThrow(
98
+ /Use read_media_file instead/
99
+ );
100
+ });
101
+ it("reads media files as base64 with MIME-aware kind metadata", async () => {
102
+ const fileSystemService = new FileSystemService(
103
+ {
104
+ allowedPaths: [tempDir],
105
+ blockedPaths: [],
106
+ blockedExtensions: [],
107
+ maxFileSize: 10 * 1024 * 1024,
108
+ workingDirectory: tempDir,
109
+ enableBackups: false,
110
+ backupRetentionDays: 7
111
+ },
112
+ mockLogger
113
+ );
114
+ await fileSystemService.initialize();
115
+ const testFile = path.join(tempDir, "clip.mp4");
116
+ const buffer = Buffer.from([0, 0, 0, 24, 102, 116, 121, 112]);
117
+ await fs.writeFile(testFile, buffer);
118
+ const result = await fileSystemService.readMediaFile(testFile);
119
+ expect(result.data).toBe(buffer.toString("base64"));
120
+ expect(result.mimeType).toBe("video/mp4");
121
+ expect(result.kind).toBe("video");
122
+ expect(result.filename).toBe("clip.mp4");
123
+ expect(result.size).toBe(buffer.length);
124
+ });
125
+ it("rejects text files from readMediaFile and points callers to readFile", async () => {
126
+ const fileSystemService = new FileSystemService(
127
+ {
128
+ allowedPaths: [tempDir],
129
+ blockedPaths: [],
130
+ blockedExtensions: [],
131
+ maxFileSize: 10 * 1024 * 1024,
132
+ workingDirectory: tempDir,
133
+ enableBackups: false,
134
+ backupRetentionDays: 7
135
+ },
136
+ mockLogger
137
+ );
138
+ await fileSystemService.initialize();
139
+ const testFile = path.join(tempDir, "plain.txt");
140
+ await fs.writeFile(testFile, "hello world");
141
+ await expect(fileSystemService.readMediaFile(testFile)).rejects.toThrow(
142
+ /Use read_file instead/
143
+ );
144
+ });
145
+ });
30
146
  describe("Backup Behavior", () => {
31
147
  describe("writeFile", () => {
32
148
  it("should NOT create backup when enableBackups is false (default)", async () => {
package/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  createGlobFilesTool: () => import_glob_files_tool.createGlobFilesTool,
28
28
  createGrepContentTool: () => import_grep_content_tool.createGrepContentTool,
29
29
  createReadFileTool: () => import_read_file_tool.createReadFileTool,
30
+ createReadMediaFileTool: () => import_read_media_file_tool.createReadMediaFileTool,
30
31
  createWriteFileTool: () => import_write_file_tool.createWriteFileTool,
31
32
  fileSystemToolsFactory: () => import_tool_factory.fileSystemToolsFactory
32
33
  });
@@ -38,6 +39,7 @@ var import_path_validator = require("./path-validator.js");
38
39
  var import_errors = require("./errors.js");
39
40
  var import_error_codes = require("./error-codes.js");
40
41
  var import_read_file_tool = require("./read-file-tool.js");
42
+ var import_read_media_file_tool = require("./read-media-file-tool.js");
41
43
  var import_write_file_tool = require("./write-file-tool.js");
42
44
  var import_edit_file_tool = require("./edit-file-tool.js");
43
45
  var import_glob_files_tool = require("./glob-files-tool.js");
@@ -53,6 +55,7 @@ var import_grep_content_tool = require("./grep-content-tool.js");
53
55
  createGlobFilesTool,
54
56
  createGrepContentTool,
55
57
  createReadFileTool,
58
+ createReadMediaFileTool,
56
59
  createWriteFileTool,
57
60
  fileSystemToolsFactory
58
61
  });
package/dist/index.d.cts CHANGED
@@ -32,7 +32,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
32
32
  enableBackups: z.ZodDefault<z.ZodBoolean>;
33
33
  backupPath: z.ZodOptional<z.ZodString>;
34
34
  backupRetentionDays: z.ZodDefault<z.ZodNumber>;
35
- enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
35
+ enabledTools: z.ZodOptional<z.ZodArray<z.ZodEnum<["read_file", "read_media_file", "write_file", "edit_file", "glob_files", "grep_content"]>, "many">>;
36
36
  }, "strict", z.ZodTypeAny, {
37
37
  allowedPaths: string[];
38
38
  blockedExtensions: string[];
@@ -43,7 +43,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
43
43
  type: "filesystem-tools";
44
44
  backupPath?: string | undefined;
45
45
  workingDirectory?: string | undefined;
46
- enabledTools?: ("read_file" | "write_file" | "edit_file" | "glob_files" | "grep_content")[] | undefined;
46
+ enabledTools?: ("read_file" | "read_media_file" | "write_file" | "edit_file" | "glob_files" | "grep_content")[] | undefined;
47
47
  }, {
48
48
  type: "filesystem-tools";
49
49
  allowedPaths?: string[] | undefined;
@@ -54,7 +54,7 @@ declare const FileSystemToolsConfigSchema: z.ZodObject<{
54
54
  enableBackups?: boolean | undefined;
55
55
  backupRetentionDays?: number | undefined;
56
56
  workingDirectory?: string | undefined;
57
- enabledTools?: ("read_file" | "write_file" | "edit_file" | "glob_files" | "grep_content")[] | undefined;
57
+ enabledTools?: ("read_file" | "read_media_file" | "write_file" | "edit_file" | "glob_files" | "grep_content")[] | undefined;
58
58
  }>;
59
59
  type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
60
60
 
@@ -78,6 +78,16 @@ interface FileContent {
78
78
  truncated: boolean;
79
79
  size: number;
80
80
  }
81
+ /**
82
+ * Media or binary file content with metadata.
83
+ */
84
+ interface MediaFileContent {
85
+ data: string;
86
+ mimeType: string;
87
+ filename: string;
88
+ kind: 'image' | 'audio' | 'video' | 'file';
89
+ size: number;
90
+ }
81
91
  /**
82
92
  * Options for reading files
83
93
  */
@@ -383,6 +393,10 @@ declare class FileSystemService {
383
393
  * Read a file with validation and size limits
384
394
  */
385
395
  readFile(filePath: string, options?: ReadFileOptions): Promise<FileContent>;
396
+ /**
397
+ * Read a media or binary file and return base64-encoded data with MIME metadata.
398
+ */
399
+ readMediaFile(filePath: string): Promise<MediaFileContent>;
386
400
  /**
387
401
  * Preview-only file read that bypasses config-allowed roots.
388
402
  *
@@ -738,6 +752,21 @@ declare const ReadFileInputSchema: z.ZodObject<{
738
752
  */
739
753
  declare function createReadFileTool(getFileSystemService: FileSystemServiceGetter): Tool<typeof ReadFileInputSchema>;
740
754
 
755
+ /**
756
+ * Read Media File Tool
757
+ *
758
+ * Internal tool for reading media or binary files as base64 with MIME metadata.
759
+ */
760
+
761
+ declare const ReadMediaFileInputSchema: z.ZodObject<{
762
+ file_path: z.ZodString;
763
+ }, "strict", z.ZodTypeAny, {
764
+ file_path: string;
765
+ }, {
766
+ file_path: string;
767
+ }>;
768
+ declare function createReadMediaFileTool(getFileSystemService: FileSystemServiceGetter): Tool<typeof ReadMediaFileInputSchema>;
769
+
741
770
  /**
742
771
  * Write File Tool
743
772
  *
@@ -849,4 +878,4 @@ declare const GrepContentInputSchema: z.ZodObject<{
849
878
  */
850
879
  declare function createGrepContentTool(getFileSystemService: FileSystemServiceGetter): Tool<typeof GrepContentInputSchema>;
851
880
 
852
- export { type BufferEncoding, type CreateDirectoryOptions, type CreateDirectoryResult, type DeletePathOptions, type DeletePathResult, type DirectoryEntry, type EditFileOptions, type EditOperation, type EditResult, type FileContent, type FileMetadata, type FileSystemConfig, FileSystemError, FileSystemErrorCode, FileSystemService, type FileSystemServiceGetter, type FileSystemToolsConfig, FileSystemToolsConfigSchema, type GlobOptions, type GlobResult, type GrepOptions, type ListDirectoryOptions, type ListDirectoryResult, type PathValidation, PathValidator, type ReadFileOptions, type RenamePathResult, type SearchMatch, type SearchResult, type WriteFileOptions, type WriteResult, createEditFileTool, createGlobFilesTool, createGrepContentTool, createReadFileTool, createWriteFileTool, fileSystemToolsFactory };
881
+ export { type BufferEncoding, type CreateDirectoryOptions, type CreateDirectoryResult, type DeletePathOptions, type DeletePathResult, type DirectoryEntry, type EditFileOptions, type EditOperation, type EditResult, type FileContent, type FileMetadata, type FileSystemConfig, FileSystemError, FileSystemErrorCode, FileSystemService, type FileSystemServiceGetter, type FileSystemToolsConfig, FileSystemToolsConfigSchema, type GlobOptions, type GlobResult, type GrepOptions, type ListDirectoryOptions, type ListDirectoryResult, type MediaFileContent, type PathValidation, PathValidator, type ReadFileOptions, type RenamePathResult, type SearchMatch, type SearchResult, type WriteFileOptions, type WriteResult, createEditFileTool, createGlobFilesTool, createGrepContentTool, createReadFileTool, createReadMediaFileTool, createWriteFileTool, fileSystemToolsFactory };
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @dexto/tools-filesystem
3
3
  *
4
4
  * FileSystem tools factory for Dexto agents.
5
- * Provides file operation tools: read, write, edit, glob, grep.
5
+ * Provides file operation tools: read text, read media, write, edit, glob, grep.
6
6
  */
7
7
  export { fileSystemToolsFactory } from './tool-factory.js';
8
8
  export type { FileSystemServiceGetter } from './file-tool-types.js';
@@ -11,8 +11,9 @@ export { FileSystemService } from './filesystem-service.js';
11
11
  export { PathValidator } from './path-validator.js';
12
12
  export { FileSystemError } from './errors.js';
13
13
  export { FileSystemErrorCode } from './error-codes.js';
14
- export type { FileSystemConfig, FileContent, ReadFileOptions, GlobOptions, GlobResult, GrepOptions, SearchResult, SearchMatch, WriteFileOptions, WriteResult, EditFileOptions, EditResult, EditOperation, FileMetadata, DirectoryEntry, ListDirectoryOptions, ListDirectoryResult, CreateDirectoryOptions, CreateDirectoryResult, DeletePathOptions, DeletePathResult, RenamePathResult, PathValidation, BufferEncoding, } from './types.js';
14
+ export type { FileSystemConfig, FileContent, MediaFileContent, ReadFileOptions, GlobOptions, GlobResult, GrepOptions, SearchResult, SearchMatch, WriteFileOptions, WriteResult, EditFileOptions, EditResult, EditOperation, FileMetadata, DirectoryEntry, ListDirectoryOptions, ListDirectoryResult, CreateDirectoryOptions, CreateDirectoryResult, DeletePathOptions, DeletePathResult, RenamePathResult, PathValidation, BufferEncoding, } from './types.js';
15
15
  export { createReadFileTool } from './read-file-tool.js';
16
+ export { createReadMediaFileTool } from './read-media-file-tool.js';
16
17
  export { createWriteFileTool } from './write-file-tool.js';
17
18
  export { createEditFileTool } from './edit-file-tool.js';
18
19
  export { createGlobFilesTool } from './glob-files-tool.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAGnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGvD,YAAY,EACR,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,WAAW,EACX,UAAU,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAGnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGvD,YAAY,EACR,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,UAAU,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { PathValidator } from "./path-validator.js";
5
5
  import { FileSystemError } from "./errors.js";
6
6
  import { FileSystemErrorCode } from "./error-codes.js";
7
7
  import { createReadFileTool } from "./read-file-tool.js";
8
+ import { createReadMediaFileTool } from "./read-media-file-tool.js";
8
9
  import { createWriteFileTool } from "./write-file-tool.js";
9
10
  import { createEditFileTool } from "./edit-file-tool.js";
10
11
  import { createGlobFilesTool } from "./glob-files-tool.js";
@@ -19,6 +20,7 @@ export {
19
20
  createGlobFilesTool,
20
21
  createGrepContentTool,
21
22
  createReadFileTool,
23
+ createReadMediaFileTool,
22
24
  createWriteFileTool,
23
25
  fileSystemToolsFactory
24
26
  };