@dexto/tools-filesystem 1.5.8 → 1.6.1
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/dist/directory-approval.cjs +98 -0
- package/dist/directory-approval.d.ts +24 -0
- package/dist/directory-approval.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.cjs +175 -390
- package/dist/directory-approval.integration.test.d.ts +14 -2
- package/dist/directory-approval.integration.test.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.js +178 -390
- package/dist/directory-approval.js +63 -0
- package/dist/edit-file-tool.cjs +109 -120
- package/dist/edit-file-tool.d.ts +22 -9
- package/dist/edit-file-tool.d.ts.map +1 -0
- package/dist/edit-file-tool.js +116 -110
- package/dist/edit-file-tool.test.cjs +109 -29
- package/dist/edit-file-tool.test.d.ts +7 -2
- package/dist/edit-file-tool.test.d.ts.map +1 -0
- package/dist/edit-file-tool.test.js +109 -29
- package/dist/error-codes.cjs +4 -0
- package/dist/error-codes.d.ts +6 -3
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +4 -0
- package/dist/errors.cjs +48 -0
- package/dist/errors.d.ts +20 -7
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/file-tool-types.d.ts +8 -40
- package/dist/file-tool-types.d.ts.map +1 -0
- package/dist/filesystem-service.cjs +325 -10
- package/dist/filesystem-service.d.ts +41 -12
- package/dist/filesystem-service.d.ts.map +1 -0
- package/dist/filesystem-service.js +326 -11
- package/dist/filesystem-service.test.cjs +10 -2
- package/dist/filesystem-service.test.d.ts +7 -2
- package/dist/filesystem-service.test.d.ts.map +1 -0
- package/dist/filesystem-service.test.js +10 -2
- package/dist/glob-files-tool.cjs +32 -46
- package/dist/glob-files-tool.d.ts +19 -9
- package/dist/glob-files-tool.d.ts.map +1 -0
- package/dist/glob-files-tool.js +33 -47
- package/dist/grep-content-tool.cjs +40 -45
- package/dist/grep-content-tool.d.ts +28 -9
- package/dist/grep-content-tool.d.ts.map +1 -0
- package/dist/grep-content-tool.js +41 -46
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +852 -14
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -2
- package/dist/path-validator.cjs +28 -2
- package/dist/path-validator.d.ts +20 -9
- package/dist/path-validator.d.ts.map +1 -0
- package/dist/path-validator.js +28 -2
- package/dist/path-validator.test.d.ts +7 -2
- package/dist/path-validator.test.d.ts.map +1 -0
- package/dist/read-file-tool.cjs +26 -59
- package/dist/read-file-tool.d.ts +19 -9
- package/dist/read-file-tool.d.ts.map +1 -0
- package/dist/read-file-tool.js +27 -50
- package/dist/tool-factory-config.cjs +61 -0
- package/dist/{tool-provider.d.ts → tool-factory-config.d.ts} +13 -30
- package/dist/tool-factory-config.d.ts.map +1 -0
- package/dist/tool-factory-config.js +36 -0
- package/dist/tool-factory.cjs +123 -0
- package/dist/tool-factory.d.ts +4 -0
- package/dist/tool-factory.d.ts.map +1 -0
- package/dist/tool-factory.js +102 -0
- package/dist/types.d.ts +82 -18
- package/dist/types.d.ts.map +1 -0
- package/dist/write-file-tool.cjs +93 -99
- package/dist/write-file-tool.d.ts +22 -9
- package/dist/write-file-tool.d.ts.map +1 -0
- package/dist/write-file-tool.js +97 -91
- package/dist/write-file-tool.test.cjs +139 -33
- package/dist/write-file-tool.test.d.ts +7 -2
- package/dist/write-file-tool.test.d.ts.map +1 -0
- package/dist/write-file-tool.test.js +139 -33
- package/package.json +5 -4
- package/dist/directory-approval.integration.test.d.cts +0 -2
- package/dist/edit-file-tool.d.cts +0 -17
- package/dist/edit-file-tool.test.d.cts +0 -2
- package/dist/error-codes.d.cts +0 -32
- package/dist/errors.d.cts +0 -112
- package/dist/file-tool-types.d.cts +0 -46
- package/dist/filesystem-service.d.cts +0 -112
- package/dist/filesystem-service.test.d.cts +0 -2
- package/dist/glob-files-tool.d.cts +0 -17
- package/dist/grep-content-tool.d.cts +0 -17
- package/dist/path-validator.d.cts +0 -97
- package/dist/path-validator.test.d.cts +0 -2
- package/dist/read-file-tool.d.cts +0 -17
- package/dist/tool-provider.cjs +0 -123
- package/dist/tool-provider.d.cts +0 -77
- package/dist/tool-provider.js +0 -99
- package/dist/types.d.cts +0 -178
- package/dist/write-file-tool.d.cts +0 -17
- package/dist/write-file-tool.test.d.cts +0 -2
|
@@ -41,16 +41,19 @@ var import_errors = require("./errors.js");
|
|
|
41
41
|
const DEFAULT_ENCODING = "utf-8";
|
|
42
42
|
const DEFAULT_MAX_RESULTS = 1e3;
|
|
43
43
|
const DEFAULT_MAX_SEARCH_RESULTS = 100;
|
|
44
|
+
const DEFAULT_MAX_LIST_RESULTS = 5e3;
|
|
45
|
+
const DEFAULT_LIST_CONCURRENCY = 16;
|
|
44
46
|
class FileSystemService {
|
|
45
47
|
config;
|
|
46
48
|
pathValidator;
|
|
47
49
|
initialized = false;
|
|
48
50
|
initPromise = null;
|
|
49
51
|
logger;
|
|
52
|
+
directoryApprovalChecker;
|
|
50
53
|
/**
|
|
51
54
|
* Create a new FileSystemService with validated configuration.
|
|
52
55
|
*
|
|
53
|
-
* @param config - Fully-validated configuration from
|
|
56
|
+
* @param config - Fully-validated configuration from the factory schema.
|
|
54
57
|
* All required fields have values, defaults already applied.
|
|
55
58
|
* @param logger - Logger instance for this service
|
|
56
59
|
*/
|
|
@@ -124,8 +127,24 @@ class FileSystemService {
|
|
|
124
127
|
* @param checker Function that returns true if path is in an approved directory
|
|
125
128
|
*/
|
|
126
129
|
setDirectoryApprovalChecker(checker) {
|
|
130
|
+
this.directoryApprovalChecker = checker;
|
|
127
131
|
this.pathValidator.setDirectoryApprovalChecker(checker);
|
|
128
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Update the working directory at runtime (e.g., when workspace changes).
|
|
135
|
+
* Rebuilds the PathValidator so allowed/blocked path roots are recalculated.
|
|
136
|
+
*/
|
|
137
|
+
setWorkingDirectory(workingDirectory) {
|
|
138
|
+
const normalized = workingDirectory?.trim();
|
|
139
|
+
if (!normalized) return;
|
|
140
|
+
if (this.config.workingDirectory === normalized) return;
|
|
141
|
+
this.config = { ...this.config, workingDirectory: normalized };
|
|
142
|
+
this.pathValidator = new import_path_validator.PathValidator(this.config, this.logger);
|
|
143
|
+
if (this.directoryApprovalChecker) {
|
|
144
|
+
this.pathValidator.setDirectoryApprovalChecker(this.directoryApprovalChecker);
|
|
145
|
+
}
|
|
146
|
+
this.logger.info(`FileSystemService working directory set to ${normalized}`);
|
|
147
|
+
}
|
|
129
148
|
/**
|
|
130
149
|
* Check if a file path is within the configured allowed paths (config only).
|
|
131
150
|
* This is used by file tools to determine if directory approval is needed.
|
|
@@ -136,16 +155,14 @@ class FileSystemService {
|
|
|
136
155
|
async isPathWithinConfigAllowed(filePath) {
|
|
137
156
|
return this.pathValidator.isPathWithinAllowed(filePath);
|
|
138
157
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*/
|
|
142
|
-
async readFile(filePath, options = {}) {
|
|
143
|
-
await this.ensureInitialized();
|
|
144
|
-
const validation = await this.pathValidator.validatePath(filePath);
|
|
158
|
+
async validateReadPath(filePath, mode) {
|
|
159
|
+
const validation = mode === "toolPreview" ? await this.pathValidator.validatePathForPreview(filePath) : await this.pathValidator.validatePath(filePath);
|
|
145
160
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
146
161
|
throw import_errors.FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
147
162
|
}
|
|
148
|
-
|
|
163
|
+
return validation.normalizedPath;
|
|
164
|
+
}
|
|
165
|
+
async readNormalizedFile(normalizedPath, options = {}) {
|
|
149
166
|
try {
|
|
150
167
|
const stats = await fs.stat(normalizedPath);
|
|
151
168
|
if (!stats.isFile()) {
|
|
@@ -159,12 +176,18 @@ class FileSystemService {
|
|
|
159
176
|
);
|
|
160
177
|
}
|
|
161
178
|
} catch (error) {
|
|
179
|
+
if (error instanceof import_core.DextoRuntimeError && error.scope === "filesystem") {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
162
182
|
if (error.code === "ENOENT") {
|
|
163
183
|
throw import_errors.FileSystemError.fileNotFound(normalizedPath);
|
|
164
184
|
}
|
|
165
185
|
if (error.code === "EACCES") {
|
|
166
186
|
throw import_errors.FileSystemError.permissionDenied(normalizedPath, "read");
|
|
167
187
|
}
|
|
188
|
+
if (error instanceof import_core.DextoRuntimeError) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
168
191
|
throw import_errors.FileSystemError.readFailed(
|
|
169
192
|
normalizedPath,
|
|
170
193
|
error instanceof Error ? error.message : String(error)
|
|
@@ -186,20 +209,44 @@ class FileSystemService {
|
|
|
186
209
|
} else {
|
|
187
210
|
selectedLines = lines;
|
|
188
211
|
}
|
|
212
|
+
const returnedContent = selectedLines.join("\n");
|
|
189
213
|
return {
|
|
190
|
-
content:
|
|
214
|
+
content: returnedContent,
|
|
191
215
|
lines: selectedLines.length,
|
|
192
216
|
encoding,
|
|
193
217
|
truncated,
|
|
194
|
-
size: Buffer.byteLength(
|
|
218
|
+
size: Buffer.byteLength(returnedContent, encoding)
|
|
195
219
|
};
|
|
196
220
|
} catch (error) {
|
|
221
|
+
if (error instanceof import_core.DextoRuntimeError && error.scope === "filesystem") {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
197
224
|
throw import_errors.FileSystemError.readFailed(
|
|
198
225
|
normalizedPath,
|
|
199
226
|
error instanceof Error ? error.message : String(error)
|
|
200
227
|
);
|
|
201
228
|
}
|
|
202
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Read a file with validation and size limits
|
|
232
|
+
*/
|
|
233
|
+
async readFile(filePath, options = {}) {
|
|
234
|
+
await this.ensureInitialized();
|
|
235
|
+
const normalizedPath = await this.validateReadPath(filePath, "execute");
|
|
236
|
+
return await this.readNormalizedFile(normalizedPath, options);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Preview-only file read that bypasses config-allowed roots.
|
|
240
|
+
*
|
|
241
|
+
* This is intended for UI previews (diffs, create previews) shown BEFORE a user
|
|
242
|
+
* confirms directory access for the tool call. The returned content is UI-only
|
|
243
|
+
* and should not be forwarded to the LLM.
|
|
244
|
+
*/
|
|
245
|
+
async readFileForToolPreview(filePath, options = {}) {
|
|
246
|
+
await this.ensureInitialized();
|
|
247
|
+
const normalizedPath = await this.validateReadPath(filePath, "toolPreview");
|
|
248
|
+
return await this.readNormalizedFile(normalizedPath, options);
|
|
249
|
+
}
|
|
203
250
|
/**
|
|
204
251
|
* Find files matching a glob pattern
|
|
205
252
|
*/
|
|
@@ -262,6 +309,274 @@ class FileSystemService {
|
|
|
262
309
|
);
|
|
263
310
|
}
|
|
264
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* List contents of a directory (non-recursive)
|
|
314
|
+
*/
|
|
315
|
+
async listDirectory(dirPath, options = {}) {
|
|
316
|
+
await this.ensureInitialized();
|
|
317
|
+
const validation = await this.pathValidator.validatePath(dirPath);
|
|
318
|
+
if (!validation.isValid || !validation.normalizedPath) {
|
|
319
|
+
throw import_errors.FileSystemError.invalidPath(dirPath, validation.error || "Unknown error");
|
|
320
|
+
}
|
|
321
|
+
const normalizedPath = validation.normalizedPath;
|
|
322
|
+
try {
|
|
323
|
+
const stats = await fs.stat(normalizedPath);
|
|
324
|
+
if (!stats.isDirectory()) {
|
|
325
|
+
throw import_errors.FileSystemError.invalidPath(normalizedPath, "Path is not a directory");
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error instanceof import_core.DextoRuntimeError && error.scope === "filesystem") {
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
if (error.code === "ENOENT") {
|
|
332
|
+
throw import_errors.FileSystemError.directoryNotFound(normalizedPath);
|
|
333
|
+
}
|
|
334
|
+
if (error.code === "EACCES") {
|
|
335
|
+
throw import_errors.FileSystemError.permissionDenied(normalizedPath, "read");
|
|
336
|
+
}
|
|
337
|
+
throw import_errors.FileSystemError.listFailed(
|
|
338
|
+
normalizedPath,
|
|
339
|
+
error instanceof Error ? error.message : String(error)
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const includeHidden = options.includeHidden ?? true;
|
|
343
|
+
const includeMetadata = options.includeMetadata !== false;
|
|
344
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_LIST_RESULTS;
|
|
345
|
+
try {
|
|
346
|
+
const dirEntries = await fs.readdir(normalizedPath, { withFileTypes: true });
|
|
347
|
+
const candidates = dirEntries.filter(
|
|
348
|
+
(entry) => includeHidden || !entry.name.startsWith(".")
|
|
349
|
+
);
|
|
350
|
+
const concurrency = DEFAULT_LIST_CONCURRENCY;
|
|
351
|
+
const validatedEntries = await this.mapWithConcurrency(
|
|
352
|
+
candidates,
|
|
353
|
+
concurrency,
|
|
354
|
+
async (entry) => {
|
|
355
|
+
const entryPath = path.join(normalizedPath, entry.name);
|
|
356
|
+
const entryValidation = await this.pathValidator.validatePath(entryPath);
|
|
357
|
+
if (!entryValidation.isValid || !entryValidation.normalizedPath) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
entry,
|
|
362
|
+
normalizedPath: entryValidation.normalizedPath
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
const validEntries = validatedEntries.filter(Boolean);
|
|
367
|
+
if (maxEntries <= 0) {
|
|
368
|
+
return {
|
|
369
|
+
path: normalizedPath,
|
|
370
|
+
entries: [],
|
|
371
|
+
truncated: validEntries.length > 0,
|
|
372
|
+
totalEntries: validEntries.length
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (!includeMetadata) {
|
|
376
|
+
const entries2 = validEntries.slice(0, maxEntries).map((entry) => ({
|
|
377
|
+
name: entry.entry.name,
|
|
378
|
+
path: entry.normalizedPath,
|
|
379
|
+
isDirectory: entry.entry.isDirectory(),
|
|
380
|
+
size: 0,
|
|
381
|
+
modified: /* @__PURE__ */ new Date()
|
|
382
|
+
}));
|
|
383
|
+
return {
|
|
384
|
+
path: normalizedPath,
|
|
385
|
+
entries: entries2,
|
|
386
|
+
truncated: validEntries.length > maxEntries,
|
|
387
|
+
totalEntries: validEntries.length
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const metadataEntries = await this.mapWithConcurrency(
|
|
391
|
+
validEntries,
|
|
392
|
+
concurrency,
|
|
393
|
+
async (entry) => {
|
|
394
|
+
try {
|
|
395
|
+
const stat = await fs.stat(entry.normalizedPath);
|
|
396
|
+
return {
|
|
397
|
+
name: entry.entry.name,
|
|
398
|
+
path: entry.normalizedPath,
|
|
399
|
+
isDirectory: entry.entry.isDirectory(),
|
|
400
|
+
size: stat.size,
|
|
401
|
+
modified: stat.mtime
|
|
402
|
+
};
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
const entries = [];
|
|
409
|
+
let successfulStats = 0;
|
|
410
|
+
let cutoffIndex = -1;
|
|
411
|
+
for (let index = 0; index < metadataEntries.length; index += 1) {
|
|
412
|
+
const entry = metadataEntries[index];
|
|
413
|
+
if (!entry) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
successfulStats += 1;
|
|
417
|
+
if (entries.length < maxEntries) {
|
|
418
|
+
entries.push(entry);
|
|
419
|
+
}
|
|
420
|
+
if (successfulStats === maxEntries) {
|
|
421
|
+
cutoffIndex = index;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const remainingSuccessful = cutoffIndex >= 0 ? metadataEntries.slice(cutoffIndex + 1).filter((entry) => entry !== null).length : 0;
|
|
425
|
+
const totalEntries = successfulStats < maxEntries ? successfulStats : maxEntries + remainingSuccessful;
|
|
426
|
+
return {
|
|
427
|
+
path: normalizedPath,
|
|
428
|
+
entries,
|
|
429
|
+
truncated: totalEntries > maxEntries,
|
|
430
|
+
totalEntries
|
|
431
|
+
};
|
|
432
|
+
} catch (error) {
|
|
433
|
+
throw import_errors.FileSystemError.listFailed(
|
|
434
|
+
normalizedPath,
|
|
435
|
+
error instanceof Error ? error.message : String(error)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async mapWithConcurrency(items, limit, mapper) {
|
|
440
|
+
if (items.length === 0) {
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
const results = new Array(items.length);
|
|
444
|
+
let nextIndex = 0;
|
|
445
|
+
const workerCount = Math.min(Math.max(1, limit), items.length);
|
|
446
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
447
|
+
while (true) {
|
|
448
|
+
const current = nextIndex++;
|
|
449
|
+
if (current >= items.length) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const item = items[current];
|
|
453
|
+
if (item === void 0) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
results[current] = await mapper(item, current);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
await Promise.all(workers);
|
|
460
|
+
return results;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Create a directory
|
|
464
|
+
*/
|
|
465
|
+
async createDirectory(dirPath, options = {}) {
|
|
466
|
+
await this.ensureInitialized();
|
|
467
|
+
const validation = await this.pathValidator.validatePath(dirPath);
|
|
468
|
+
if (!validation.isValid || !validation.normalizedPath) {
|
|
469
|
+
throw import_errors.FileSystemError.invalidPath(dirPath, validation.error || "Unknown error");
|
|
470
|
+
}
|
|
471
|
+
const normalizedPath = validation.normalizedPath;
|
|
472
|
+
const recursive = options.recursive ?? false;
|
|
473
|
+
try {
|
|
474
|
+
const firstCreated = await fs.mkdir(normalizedPath, { recursive });
|
|
475
|
+
const created = recursive ? typeof firstCreated === "string" : true;
|
|
476
|
+
return { path: normalizedPath, created };
|
|
477
|
+
} catch (error) {
|
|
478
|
+
const code = error.code;
|
|
479
|
+
if (code === "EEXIST") {
|
|
480
|
+
try {
|
|
481
|
+
const stat = await fs.stat(normalizedPath);
|
|
482
|
+
if (stat.isDirectory()) {
|
|
483
|
+
return { path: normalizedPath, created: false };
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
489
|
+
throw import_errors.FileSystemError.permissionDenied(normalizedPath, "create directory");
|
|
490
|
+
}
|
|
491
|
+
throw import_errors.FileSystemError.createDirFailed(
|
|
492
|
+
normalizedPath,
|
|
493
|
+
error instanceof Error ? error.message : String(error)
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Delete a file or directory
|
|
499
|
+
*/
|
|
500
|
+
async deletePath(targetPath, options = {}) {
|
|
501
|
+
await this.ensureInitialized();
|
|
502
|
+
const validation = await this.pathValidator.validatePath(targetPath);
|
|
503
|
+
if (!validation.isValid || !validation.normalizedPath) {
|
|
504
|
+
throw import_errors.FileSystemError.invalidPath(targetPath, validation.error || "Unknown error");
|
|
505
|
+
}
|
|
506
|
+
const normalizedPath = validation.normalizedPath;
|
|
507
|
+
try {
|
|
508
|
+
await fs.rm(normalizedPath, { recursive: options.recursive ?? false, force: false });
|
|
509
|
+
return { path: normalizedPath, deleted: true };
|
|
510
|
+
} catch (error) {
|
|
511
|
+
const code = error.code;
|
|
512
|
+
if (code === "ENOENT") {
|
|
513
|
+
throw import_errors.FileSystemError.fileNotFound(normalizedPath);
|
|
514
|
+
}
|
|
515
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
516
|
+
throw import_errors.FileSystemError.permissionDenied(normalizedPath, "delete");
|
|
517
|
+
}
|
|
518
|
+
throw import_errors.FileSystemError.deleteFailed(
|
|
519
|
+
normalizedPath,
|
|
520
|
+
error instanceof Error ? error.message : String(error)
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Rename or move a file or directory
|
|
526
|
+
*/
|
|
527
|
+
async renamePath(fromPath, toPath) {
|
|
528
|
+
await this.ensureInitialized();
|
|
529
|
+
const fromValidation = await this.pathValidator.validatePath(fromPath);
|
|
530
|
+
if (!fromValidation.isValid || !fromValidation.normalizedPath) {
|
|
531
|
+
throw import_errors.FileSystemError.invalidPath(fromPath, fromValidation.error || "Unknown error");
|
|
532
|
+
}
|
|
533
|
+
const toValidation = await this.pathValidator.validatePath(toPath);
|
|
534
|
+
if (!toValidation.isValid || !toValidation.normalizedPath) {
|
|
535
|
+
throw import_errors.FileSystemError.invalidPath(toPath, toValidation.error || "Unknown error");
|
|
536
|
+
}
|
|
537
|
+
const normalizedFrom = fromValidation.normalizedPath;
|
|
538
|
+
const normalizedTo = toValidation.normalizedPath;
|
|
539
|
+
if (normalizedFrom === normalizedTo) {
|
|
540
|
+
return { from: normalizedFrom, to: normalizedTo };
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
await fs.access(normalizedTo);
|
|
544
|
+
throw import_errors.FileSystemError.renameFailed(
|
|
545
|
+
normalizedFrom,
|
|
546
|
+
`Target already exists: ${normalizedTo}`
|
|
547
|
+
);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const code = error.code;
|
|
550
|
+
if (!code) {
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
if (code === "ENOENT") {
|
|
554
|
+
} else if (code === "EACCES" || code === "EPERM") {
|
|
555
|
+
throw import_errors.FileSystemError.permissionDenied(normalizedTo, "rename");
|
|
556
|
+
} else {
|
|
557
|
+
throw import_errors.FileSystemError.renameFailed(
|
|
558
|
+
normalizedFrom,
|
|
559
|
+
error instanceof Error ? error.message : String(error)
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
await fs.rename(normalizedFrom, normalizedTo);
|
|
565
|
+
return { from: normalizedFrom, to: normalizedTo };
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const code = error.code;
|
|
568
|
+
if (code === "ENOENT") {
|
|
569
|
+
throw import_errors.FileSystemError.fileNotFound(normalizedFrom);
|
|
570
|
+
}
|
|
571
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
572
|
+
throw import_errors.FileSystemError.permissionDenied(normalizedFrom, "rename");
|
|
573
|
+
}
|
|
574
|
+
throw import_errors.FileSystemError.renameFailed(
|
|
575
|
+
normalizedFrom,
|
|
576
|
+
error instanceof Error ? error.message : String(error)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
265
580
|
/**
|
|
266
581
|
* Search for content in files (grep-like functionality)
|
|
267
582
|
*/
|
|
@@ -1,36 +1,34 @@
|
|
|
1
|
-
import { IDextoLogger } from '@dexto/core';
|
|
2
|
-
import { FileSystemConfig, ReadFileOptions, FileContent, GlobOptions, GlobResult, GrepOptions, SearchResult, WriteFileOptions, WriteResult, EditOperation, EditFileOptions, EditResult } from './types.js';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* FileSystem Service
|
|
6
3
|
*
|
|
7
4
|
* Secure file system operations for Dexto internal tools
|
|
8
5
|
*/
|
|
9
|
-
|
|
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';
|
|
10
8
|
/**
|
|
11
9
|
* FileSystemService - Handles all file system operations with security checks
|
|
12
10
|
*
|
|
13
|
-
* This service receives fully-validated configuration from the FileSystem Tools
|
|
14
|
-
* All defaults have been applied by the
|
|
11
|
+
* This service receives fully-validated configuration from the FileSystem Tools Factory.
|
|
12
|
+
* All defaults have been applied by the factory's schema, so the service trusts the config
|
|
15
13
|
* and uses it as-is without any fallback logic.
|
|
16
14
|
*
|
|
17
|
-
* TODO: Add tests for this class
|
|
18
15
|
* TODO: instantiate only when internal file tools are enabled to avoid file dependencies which won't work in serverless
|
|
19
16
|
*/
|
|
20
|
-
declare class FileSystemService {
|
|
17
|
+
export declare class FileSystemService {
|
|
21
18
|
private config;
|
|
22
19
|
private pathValidator;
|
|
23
20
|
private initialized;
|
|
24
21
|
private initPromise;
|
|
25
22
|
private logger;
|
|
23
|
+
private directoryApprovalChecker?;
|
|
26
24
|
/**
|
|
27
25
|
* Create a new FileSystemService with validated configuration.
|
|
28
26
|
*
|
|
29
|
-
* @param config - Fully-validated configuration from
|
|
27
|
+
* @param config - Fully-validated configuration from the factory schema.
|
|
30
28
|
* All required fields have values, defaults already applied.
|
|
31
29
|
* @param logger - Logger instance for this service
|
|
32
30
|
*/
|
|
33
|
-
constructor(config: FileSystemConfig, logger:
|
|
31
|
+
constructor(config: FileSystemConfig, logger: Logger);
|
|
34
32
|
/**
|
|
35
33
|
* Get backup directory path (context-aware with optional override)
|
|
36
34
|
* TODO: Migrate to explicit configuration via CLI enrichment layer (per-agent paths)
|
|
@@ -63,6 +61,11 @@ declare class FileSystemService {
|
|
|
63
61
|
* @param checker Function that returns true if path is in an approved directory
|
|
64
62
|
*/
|
|
65
63
|
setDirectoryApprovalChecker(checker: (filePath: string) => boolean): void;
|
|
64
|
+
/**
|
|
65
|
+
* Update the working directory at runtime (e.g., when workspace changes).
|
|
66
|
+
* Rebuilds the PathValidator so allowed/blocked path roots are recalculated.
|
|
67
|
+
*/
|
|
68
|
+
setWorkingDirectory(workingDirectory: string): void;
|
|
66
69
|
/**
|
|
67
70
|
* Check if a file path is within the configured allowed paths (config only).
|
|
68
71
|
* This is used by file tools to determine if directory approval is needed.
|
|
@@ -71,14 +74,41 @@ declare class FileSystemService {
|
|
|
71
74
|
* @returns true if the path is within config-allowed paths, false otherwise
|
|
72
75
|
*/
|
|
73
76
|
isPathWithinConfigAllowed(filePath: string): Promise<boolean>;
|
|
77
|
+
private validateReadPath;
|
|
78
|
+
private readNormalizedFile;
|
|
74
79
|
/**
|
|
75
80
|
* Read a file with validation and size limits
|
|
76
81
|
*/
|
|
77
82
|
readFile(filePath: string, options?: ReadFileOptions): Promise<FileContent>;
|
|
83
|
+
/**
|
|
84
|
+
* Preview-only file read that bypasses config-allowed roots.
|
|
85
|
+
*
|
|
86
|
+
* This is intended for UI previews (diffs, create previews) shown BEFORE a user
|
|
87
|
+
* confirms directory access for the tool call. The returned content is UI-only
|
|
88
|
+
* and should not be forwarded to the LLM.
|
|
89
|
+
*/
|
|
90
|
+
readFileForToolPreview(filePath: string, options?: ReadFileOptions): Promise<FileContent>;
|
|
78
91
|
/**
|
|
79
92
|
* Find files matching a glob pattern
|
|
80
93
|
*/
|
|
81
94
|
globFiles(pattern: string, options?: GlobOptions): Promise<GlobResult>;
|
|
95
|
+
/**
|
|
96
|
+
* List contents of a directory (non-recursive)
|
|
97
|
+
*/
|
|
98
|
+
listDirectory(dirPath: string, options?: ListDirectoryOptions): Promise<ListDirectoryResult>;
|
|
99
|
+
private mapWithConcurrency;
|
|
100
|
+
/**
|
|
101
|
+
* Create a directory
|
|
102
|
+
*/
|
|
103
|
+
createDirectory(dirPath: string, options?: CreateDirectoryOptions): Promise<CreateDirectoryResult>;
|
|
104
|
+
/**
|
|
105
|
+
* Delete a file or directory
|
|
106
|
+
*/
|
|
107
|
+
deletePath(targetPath: string, options?: DeletePathOptions): Promise<DeletePathResult>;
|
|
108
|
+
/**
|
|
109
|
+
* Rename or move a file or directory
|
|
110
|
+
*/
|
|
111
|
+
renamePath(fromPath: string, toPath: string): Promise<RenamePathResult>;
|
|
82
112
|
/**
|
|
83
113
|
* Search for content in files (grep-like functionality)
|
|
84
114
|
*/
|
|
@@ -108,5 +138,4 @@ declare class FileSystemService {
|
|
|
108
138
|
*/
|
|
109
139
|
isPathAllowed(filePath: string): Promise<boolean>;
|
|
110
140
|
}
|
|
111
|
-
|
|
112
|
-
export { FileSystemService };
|
|
141
|
+
//# sourceMappingURL=filesystem-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesystem-service.d.ts","sourceRoot":"","sources":["../src/filesystem-service.ts"],"names":[],"mappings":"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"}
|