@dexto/tools-filesystem 1.6.25 → 1.6.27

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 (42) hide show
  1. package/dist/directory-approval.cjs +6 -4
  2. package/dist/directory-approval.d.ts.map +1 -1
  3. package/dist/directory-approval.integration.test.cjs +235 -6
  4. package/dist/directory-approval.integration.test.js +236 -6
  5. package/dist/directory-approval.js +6 -4
  6. package/dist/filesystem-service.cjs +6 -2
  7. package/dist/filesystem-service.d.ts.map +1 -1
  8. package/dist/filesystem-service.js +6 -2
  9. package/dist/glob-files-tool.cjs +3 -13
  10. package/dist/glob-files-tool.d.ts.map +1 -1
  11. package/dist/glob-files-tool.js +3 -3
  12. package/dist/glob-files-tool.test.cjs +115 -0
  13. package/dist/glob-files-tool.test.d.ts +2 -0
  14. package/dist/glob-files-tool.test.d.ts.map +1 -0
  15. package/dist/glob-files-tool.test.js +92 -0
  16. package/dist/grep-content-tool.cjs +3 -13
  17. package/dist/grep-content-tool.d.ts.map +1 -1
  18. package/dist/grep-content-tool.js +3 -3
  19. package/dist/grep-content-tool.test.cjs +115 -0
  20. package/dist/grep-content-tool.test.d.ts +2 -0
  21. package/dist/grep-content-tool.test.d.ts.map +1 -0
  22. package/dist/grep-content-tool.test.js +92 -0
  23. package/dist/path-utils.cjs +55 -0
  24. package/dist/path-utils.d.ts +11 -0
  25. package/dist/path-utils.d.ts.map +1 -0
  26. package/dist/path-utils.js +20 -0
  27. package/dist/path-utils.test.cjs +52 -0
  28. package/dist/path-utils.test.d.ts +2 -0
  29. package/dist/path-utils.test.d.ts.map +1 -0
  30. package/dist/path-utils.test.js +29 -0
  31. package/dist/path-validator.cjs +23 -9
  32. package/dist/path-validator.d.ts.map +1 -1
  33. package/dist/path-validator.js +23 -9
  34. package/dist/path-validator.test.cjs +41 -0
  35. package/dist/path-validator.test.js +19 -0
  36. package/dist/tool-factory-config.d.ts +2 -2
  37. package/dist/tool-factory.cjs +29 -50
  38. package/dist/tool-factory.d.ts.map +1 -1
  39. package/dist/tool-factory.js +29 -50
  40. package/dist/write-file-tool.test.cjs +33 -0
  41. package/dist/write-file-tool.test.js +33 -0
  42. package/package.json +3 -3
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import * as path from "node:path";
3
+ import * as fs from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import { FileSystemService } from "./filesystem-service.js";
6
+ import { createGrepContentTool } from "./grep-content-tool.js";
7
+ const createMockLogger = () => {
8
+ const logger = {
9
+ debug: vi.fn(),
10
+ silly: vi.fn(),
11
+ info: vi.fn(),
12
+ warn: vi.fn(),
13
+ error: vi.fn(),
14
+ trackException: vi.fn(),
15
+ createChild: vi.fn(() => logger),
16
+ createFileOnlyChild: vi.fn(() => logger),
17
+ setLevel: vi.fn(),
18
+ getLevel: vi.fn(() => "debug"),
19
+ getLogFilePath: vi.fn(() => null),
20
+ destroy: vi.fn(async () => void 0)
21
+ };
22
+ return logger;
23
+ };
24
+ function createToolContext(logger) {
25
+ return { logger };
26
+ }
27
+ describe("grep_content tool", () => {
28
+ let mockLogger;
29
+ let tempDir;
30
+ let fileSystemService;
31
+ beforeEach(async () => {
32
+ mockLogger = createMockLogger();
33
+ const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-grep-test-"));
34
+ tempDir = await fs.realpath(rawTempDir);
35
+ fileSystemService = new FileSystemService(
36
+ {
37
+ allowedPaths: [tempDir],
38
+ blockedPaths: [],
39
+ blockedExtensions: [],
40
+ maxFileSize: 10 * 1024 * 1024,
41
+ workingDirectory: tempDir,
42
+ enableBackups: false,
43
+ backupRetentionDays: 7
44
+ },
45
+ mockLogger
46
+ );
47
+ await fileSystemService.initialize();
48
+ });
49
+ afterEach(async () => {
50
+ vi.restoreAllMocks();
51
+ try {
52
+ await fs.rm(tempDir, { recursive: true, force: true });
53
+ } catch {
54
+ }
55
+ });
56
+ it("expands home-directory shorthand for search paths", async () => {
57
+ const homeTempDir = await fs.mkdtemp(path.join(os.homedir(), ".dexto-grep-home-test-"));
58
+ const fileSystemServiceForHome = new FileSystemService(
59
+ {
60
+ allowedPaths: [homeTempDir],
61
+ blockedPaths: [],
62
+ blockedExtensions: [],
63
+ maxFileSize: 10 * 1024 * 1024,
64
+ workingDirectory: tempDir,
65
+ enableBackups: false,
66
+ backupRetentionDays: 7
67
+ },
68
+ mockLogger
69
+ );
70
+ await fileSystemServiceForHome.initialize();
71
+ try {
72
+ const filePath = path.join(homeTempDir, "notes.txt");
73
+ await fs.writeFile(filePath, "alpha\nneedle\nomega\n");
74
+ const tool = createGrepContentTool(async () => fileSystemServiceForHome);
75
+ const parsedInput = tool.inputSchema.parse({
76
+ pattern: "needle",
77
+ path: `~/${path.basename(homeTempDir)}`
78
+ });
79
+ const result = await tool.execute(parsedInput, createToolContext(mockLogger));
80
+ expect(result.files_searched).toBe(1);
81
+ expect(result.matches).toEqual([
82
+ {
83
+ file: filePath,
84
+ line_number: 2,
85
+ line: "needle"
86
+ }
87
+ ]);
88
+ } finally {
89
+ await fs.rm(homeTempDir, { recursive: true, force: true });
90
+ }
91
+ });
92
+ });
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var path_utils_exports = {};
30
+ __export(path_utils_exports, {
31
+ expandHomeShorthand: () => expandHomeShorthand,
32
+ resolveUserPath: () => resolveUserPath
33
+ });
34
+ module.exports = __toCommonJS(path_utils_exports);
35
+ var os = __toESM(require("node:os"), 1);
36
+ var path = __toESM(require("node:path"), 1);
37
+ function expandHomeShorthand(inputPath) {
38
+ if (!/^~(?=$|[\\/])/.test(inputPath)) {
39
+ return inputPath;
40
+ }
41
+ if (inputPath === "~") {
42
+ return os.homedir();
43
+ }
44
+ return path.join(os.homedir(), inputPath.slice(2));
45
+ }
46
+ function resolveUserPath(workingDirectory, inputPath) {
47
+ const resolvedWorkingDirectory = path.resolve(expandHomeShorthand(workingDirectory));
48
+ const expandedPath = expandHomeShorthand(inputPath);
49
+ return path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(resolvedWorkingDirectory, expandedPath);
50
+ }
51
+ // Annotate the CommonJS export names for ESM import in node:
52
+ 0 && (module.exports = {
53
+ expandHomeShorthand,
54
+ resolveUserPath
55
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Expand a leading home-directory shorthand (`~`) into an absolute path.
3
+ * Leaves other path shapes untouched, including `~user/...`.
4
+ */
5
+ export declare function expandHomeShorthand(inputPath: string): string;
6
+ /**
7
+ * Resolve a user-supplied path against a working directory after expanding
8
+ * supported home-directory shorthand.
9
+ */
10
+ export declare function resolveUserPath(workingDirectory: string, inputPath: string): string;
11
+ //# sourceMappingURL=path-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAU7D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMnF"}
@@ -0,0 +1,20 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ function expandHomeShorthand(inputPath) {
4
+ if (!/^~(?=$|[\\/])/.test(inputPath)) {
5
+ return inputPath;
6
+ }
7
+ if (inputPath === "~") {
8
+ return os.homedir();
9
+ }
10
+ return path.join(os.homedir(), inputPath.slice(2));
11
+ }
12
+ function resolveUserPath(workingDirectory, inputPath) {
13
+ const resolvedWorkingDirectory = path.resolve(expandHomeShorthand(workingDirectory));
14
+ const expandedPath = expandHomeShorthand(inputPath);
15
+ return path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(resolvedWorkingDirectory, expandedPath);
16
+ }
17
+ export {
18
+ expandHomeShorthand,
19
+ resolveUserPath
20
+ };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ var import_vitest = require("vitest");
25
+ var os = __toESM(require("node:os"), 1);
26
+ var path = __toESM(require("node:path"), 1);
27
+ var import_path_utils = require("./path-utils.js");
28
+ (0, import_vitest.describe)("path-utils", () => {
29
+ (0, import_vitest.it)("expands bare home shorthand", () => {
30
+ (0, import_vitest.expect)((0, import_path_utils.expandHomeShorthand)("~")).toBe(os.homedir());
31
+ });
32
+ (0, import_vitest.it)("expands home shorthand with nested path segments", () => {
33
+ (0, import_vitest.expect)((0, import_path_utils.expandHomeShorthand)("~/Projects/test.md")).toBe(
34
+ path.join(os.homedir(), "Projects", "test.md")
35
+ );
36
+ (0, import_vitest.expect)((0, import_path_utils.expandHomeShorthand)("~\\Projects\\test.md")).toBe(
37
+ path.join(os.homedir(), "Projects\\test.md")
38
+ );
39
+ });
40
+ (0, import_vitest.it)("does not expand unsupported user-home syntax", () => {
41
+ (0, import_vitest.expect)((0, import_path_utils.expandHomeShorthand)("~other-user/file.txt")).toBe("~other-user/file.txt");
42
+ });
43
+ (0, import_vitest.it)("resolves home shorthand before relative resolution", () => {
44
+ (0, import_vitest.expect)((0, import_path_utils.resolveUserPath)("/workspace", "~/Projects/test.md")).toBe(
45
+ path.join(os.homedir(), "Projects", "test.md")
46
+ );
47
+ (0, import_vitest.expect)((0, import_path_utils.resolveUserPath)("/workspace", "notes/today.md")).toBe("/workspace/notes/today.md");
48
+ (0, import_vitest.expect)((0, import_path_utils.resolveUserPath)("~", "notes/today.md")).toBe(
49
+ path.join(os.homedir(), "notes/today.md")
50
+ );
51
+ });
52
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=path-utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-utils.test.d.ts","sourceRoot":"","sources":["../src/path-utils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { expandHomeShorthand, resolveUserPath } from "./path-utils.js";
5
+ describe("path-utils", () => {
6
+ it("expands bare home shorthand", () => {
7
+ expect(expandHomeShorthand("~")).toBe(os.homedir());
8
+ });
9
+ it("expands home shorthand with nested path segments", () => {
10
+ expect(expandHomeShorthand("~/Projects/test.md")).toBe(
11
+ path.join(os.homedir(), "Projects", "test.md")
12
+ );
13
+ expect(expandHomeShorthand("~\\Projects\\test.md")).toBe(
14
+ path.join(os.homedir(), "Projects\\test.md")
15
+ );
16
+ });
17
+ it("does not expand unsupported user-home syntax", () => {
18
+ expect(expandHomeShorthand("~other-user/file.txt")).toBe("~other-user/file.txt");
19
+ });
20
+ it("resolves home shorthand before relative resolution", () => {
21
+ expect(resolveUserPath("/workspace", "~/Projects/test.md")).toBe(
22
+ path.join(os.homedir(), "Projects", "test.md")
23
+ );
24
+ expect(resolveUserPath("/workspace", "notes/today.md")).toBe("/workspace/notes/today.md");
25
+ expect(resolveUserPath("~", "notes/today.md")).toBe(
26
+ path.join(os.homedir(), "notes/today.md")
27
+ );
28
+ });
29
+ });
@@ -33,6 +33,7 @@ __export(path_validator_exports, {
33
33
  module.exports = __toCommonJS(path_validator_exports);
34
34
  var path = __toESM(require("node:path"), 1);
35
35
  var fs = __toESM(require("node:fs/promises"), 1);
36
+ var import_path_utils = require("./path-utils.js");
36
37
  class PathValidator {
37
38
  config;
38
39
  normalizedAllowedPaths;
@@ -43,9 +44,13 @@ class PathValidator {
43
44
  constructor(config, logger) {
44
45
  this.config = config;
45
46
  this.logger = logger;
46
- const workingDir = config.workingDirectory || process.cwd();
47
- this.normalizedAllowedPaths = config.allowedPaths.map((p) => path.resolve(workingDir, p));
48
- this.normalizedBlockedPaths = config.blockedPaths.map((p) => path.normalize(p));
47
+ const workingDir = (0, import_path_utils.resolveUserPath)(process.cwd(), config.workingDirectory || process.cwd());
48
+ this.normalizedAllowedPaths = config.allowedPaths.map(
49
+ (p) => (0, import_path_utils.resolveUserPath)(workingDir, p)
50
+ );
51
+ this.normalizedBlockedPaths = config.blockedPaths.map(
52
+ (p) => path.normalize((0, import_path_utils.expandHomeShorthand)(p))
53
+ );
49
54
  this.normalizedBlockedExtensions = (config.blockedExtensions || []).map((ext) => {
50
55
  const e = ext.startsWith(".") ? ext : `.${ext}`;
51
56
  return e.toLowerCase();
@@ -92,11 +97,14 @@ class PathValidator {
92
97
  error: "Path cannot be empty"
93
98
  };
94
99
  }
95
- const workingDir = this.config.workingDirectory || process.cwd();
100
+ const workingDir = (0, import_path_utils.resolveUserPath)(
101
+ process.cwd(),
102
+ this.config.workingDirectory || process.cwd()
103
+ );
96
104
  let resolvedPath;
97
105
  let normalizedPath;
98
106
  try {
99
- resolvedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
107
+ resolvedPath = (0, import_path_utils.resolveUserPath)(workingDir, filePath);
100
108
  normalizedPath = resolvedPath;
101
109
  try {
102
110
  normalizedPath = await fs.realpath(normalizedPath);
@@ -150,7 +158,10 @@ class PathValidator {
150
158
  */
151
159
  hasPathTraversal(originalPath, normalizedPath) {
152
160
  if (originalPath.includes("../") || originalPath.includes("..\\")) {
153
- const workingDir = this.config.workingDirectory || process.cwd();
161
+ const workingDir = (0, import_path_utils.resolveUserPath)(
162
+ process.cwd(),
163
+ this.config.workingDirectory || process.cwd()
164
+ );
154
165
  const relative = path.relative(workingDir, normalizedPath);
155
166
  if (relative.startsWith("..")) {
156
167
  return true;
@@ -170,7 +181,7 @@ class PathValidator {
170
181
  * Check if path matches blocked patterns (blacklist check)
171
182
  */
172
183
  isPathBlocked(normalizedPath) {
173
- const roots = this.normalizedAllowedPaths.length > 0 ? this.normalizedAllowedPaths : [this.config.workingDirectory || process.cwd()];
184
+ const roots = this.normalizedAllowedPaths.length > 0 ? this.normalizedAllowedPaths : [(0, import_path_utils.resolveUserPath)(process.cwd(), this.config.workingDirectory || process.cwd())];
174
185
  for (const blocked of this.normalizedBlockedPaths) {
175
186
  for (const root of roots) {
176
187
  const blockedFull = path.isAbsolute(blocked) ? path.normalize(blocked) : path.resolve(root, blocked);
@@ -222,10 +233,13 @@ class PathValidator {
222
233
  if (!filePath || filePath.trim() === "") {
223
234
  return false;
224
235
  }
225
- const workingDir = this.config.workingDirectory || process.cwd();
236
+ const workingDir = (0, import_path_utils.resolveUserPath)(
237
+ process.cwd(),
238
+ this.config.workingDirectory || process.cwd()
239
+ );
226
240
  let normalizedPath;
227
241
  try {
228
- normalizedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
242
+ normalizedPath = (0, import_path_utils.resolveUserPath)(workingDir, filePath);
229
243
  try {
230
244
  normalizedPath = await fs.realpath(normalizedPath);
231
245
  } catch {
@@ -1 +1 @@
1
- {"version":3,"file":"path-validator.d.ts","sourceRoot":"","sources":["../src/path-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAErE;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,sBAAsB,CAAW;IACzC,OAAO,CAAC,sBAAsB,CAAW;IACzC,OAAO,CAAC,2BAA2B,CAAW;IAC9C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,wBAAwB,CAAuC;gBAE3D,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM;IAsBpD;;;;;OAKG;IACH,2BAA2B,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI;IAKpE;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAI7D;;;;;;;;;;;OAWG;IACG,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;YAIzD,oBAAoB;IAyFlC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;;OAGG;IACH,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAInD;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;;;;;;;;OASG;IACG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8B7D;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;OAEG;IACH,eAAe,IAAI,MAAM,EAAE;IAI3B;;OAEG;IACH,eAAe,IAAI,MAAM,EAAE;CAG9B"}
1
+ {"version":3,"file":"path-validator.d.ts","sourceRoot":"","sources":["../src/path-validator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;AAErE;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,sBAAsB,CAAW;IACzC,OAAO,CAAC,sBAAsB,CAAW;IACzC,OAAO,CAAC,2BAA2B,CAAW;IAC9C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,wBAAwB,CAAuC;gBAE3D,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM;IA0BpD;;;;;OAKG;IACH,2BAA2B,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI;IAKpE;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAI7D;;;;;;;;;;;OAWG;IACG,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;YAIzD,oBAAoB;IA0FlC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAgBxB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;;OAGG;IACH,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAInD;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;;;;;;;;OASG;IACG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA+B7D;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;OAEG;IACH,eAAe,IAAI,MAAM,EAAE;IAI3B;;OAEG;IACH,eAAe,IAAI,MAAM,EAAE;CAG9B"}
@@ -1,5 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs/promises";
3
+ import { expandHomeShorthand, resolveUserPath } from "./path-utils.js";
3
4
  class PathValidator {
4
5
  config;
5
6
  normalizedAllowedPaths;
@@ -10,9 +11,13 @@ class PathValidator {
10
11
  constructor(config, logger) {
11
12
  this.config = config;
12
13
  this.logger = logger;
13
- const workingDir = config.workingDirectory || process.cwd();
14
- this.normalizedAllowedPaths = config.allowedPaths.map((p) => path.resolve(workingDir, p));
15
- this.normalizedBlockedPaths = config.blockedPaths.map((p) => path.normalize(p));
14
+ const workingDir = resolveUserPath(process.cwd(), config.workingDirectory || process.cwd());
15
+ this.normalizedAllowedPaths = config.allowedPaths.map(
16
+ (p) => resolveUserPath(workingDir, p)
17
+ );
18
+ this.normalizedBlockedPaths = config.blockedPaths.map(
19
+ (p) => path.normalize(expandHomeShorthand(p))
20
+ );
16
21
  this.normalizedBlockedExtensions = (config.blockedExtensions || []).map((ext) => {
17
22
  const e = ext.startsWith(".") ? ext : `.${ext}`;
18
23
  return e.toLowerCase();
@@ -59,11 +64,14 @@ class PathValidator {
59
64
  error: "Path cannot be empty"
60
65
  };
61
66
  }
62
- const workingDir = this.config.workingDirectory || process.cwd();
67
+ const workingDir = resolveUserPath(
68
+ process.cwd(),
69
+ this.config.workingDirectory || process.cwd()
70
+ );
63
71
  let resolvedPath;
64
72
  let normalizedPath;
65
73
  try {
66
- resolvedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
74
+ resolvedPath = resolveUserPath(workingDir, filePath);
67
75
  normalizedPath = resolvedPath;
68
76
  try {
69
77
  normalizedPath = await fs.realpath(normalizedPath);
@@ -117,7 +125,10 @@ class PathValidator {
117
125
  */
118
126
  hasPathTraversal(originalPath, normalizedPath) {
119
127
  if (originalPath.includes("../") || originalPath.includes("..\\")) {
120
- const workingDir = this.config.workingDirectory || process.cwd();
128
+ const workingDir = resolveUserPath(
129
+ process.cwd(),
130
+ this.config.workingDirectory || process.cwd()
131
+ );
121
132
  const relative = path.relative(workingDir, normalizedPath);
122
133
  if (relative.startsWith("..")) {
123
134
  return true;
@@ -137,7 +148,7 @@ class PathValidator {
137
148
  * Check if path matches blocked patterns (blacklist check)
138
149
  */
139
150
  isPathBlocked(normalizedPath) {
140
- const roots = this.normalizedAllowedPaths.length > 0 ? this.normalizedAllowedPaths : [this.config.workingDirectory || process.cwd()];
151
+ const roots = this.normalizedAllowedPaths.length > 0 ? this.normalizedAllowedPaths : [resolveUserPath(process.cwd(), this.config.workingDirectory || process.cwd())];
141
152
  for (const blocked of this.normalizedBlockedPaths) {
142
153
  for (const root of roots) {
143
154
  const blockedFull = path.isAbsolute(blocked) ? path.normalize(blocked) : path.resolve(root, blocked);
@@ -189,10 +200,13 @@ class PathValidator {
189
200
  if (!filePath || filePath.trim() === "") {
190
201
  return false;
191
202
  }
192
- const workingDir = this.config.workingDirectory || process.cwd();
203
+ const workingDir = resolveUserPath(
204
+ process.cwd(),
205
+ this.config.workingDirectory || process.cwd()
206
+ );
193
207
  let normalizedPath;
194
208
  try {
195
- normalizedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
209
+ normalizedPath = resolveUserPath(workingDir, filePath);
196
210
  try {
197
211
  normalizedPath = await fs.realpath(normalizedPath);
198
212
  } catch {
@@ -1,5 +1,29 @@
1
1
  "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
2
24
  var import_vitest = require("vitest");
25
+ var os = __toESM(require("node:os"), 1);
26
+ var path = __toESM(require("node:path"), 1);
3
27
  var import_path_validator = require("./path-validator.js");
4
28
  const createMockLogger = () => ({
5
29
  debug: import_vitest.vi.fn(),
@@ -85,6 +109,23 @@ const createMockLogger = () => ({
85
109
  const result = await validator.validatePath("src/file.ts");
86
110
  (0, import_vitest.expect)(result.isValid).toBe(true);
87
111
  });
112
+ (0, import_vitest.it)("should expand home-directory shorthand before validation", async () => {
113
+ const validator = new import_path_validator.PathValidator(
114
+ {
115
+ allowedPaths: ["~"],
116
+ blockedPaths: [],
117
+ blockedExtensions: [],
118
+ maxFileSize: 10 * 1024 * 1024,
119
+ enableBackups: false,
120
+ backupRetentionDays: 7,
121
+ workingDirectory: "/workspace/project"
122
+ },
123
+ mockLogger
124
+ );
125
+ const result = await validator.validatePath("~/notes/file.ts");
126
+ (0, import_vitest.expect)(result.isValid).toBe(true);
127
+ (0, import_vitest.expect)(result.normalizedPath).toBe(path.join(os.homedir(), "notes", "file.ts"));
128
+ });
88
129
  (0, import_vitest.it)("should reject paths outside allowed directories", async () => {
89
130
  const validator = new import_path_validator.PathValidator(
90
131
  {
@@ -1,4 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
2
4
  import { PathValidator } from "./path-validator.js";
3
5
  const createMockLogger = () => ({
4
6
  debug: vi.fn(),
@@ -84,6 +86,23 @@ describe("PathValidator", () => {
84
86
  const result = await validator.validatePath("src/file.ts");
85
87
  expect(result.isValid).toBe(true);
86
88
  });
89
+ it("should expand home-directory shorthand before validation", async () => {
90
+ const validator = new PathValidator(
91
+ {
92
+ allowedPaths: ["~"],
93
+ blockedPaths: [],
94
+ blockedExtensions: [],
95
+ maxFileSize: 10 * 1024 * 1024,
96
+ enableBackups: false,
97
+ backupRetentionDays: 7,
98
+ workingDirectory: "/workspace/project"
99
+ },
100
+ mockLogger
101
+ );
102
+ const result = await validator.validatePath("~/notes/file.ts");
103
+ expect(result.isValid).toBe(true);
104
+ expect(result.normalizedPath).toBe(path.join(os.homedir(), "notes", "file.ts"));
105
+ });
87
106
  it("should reject paths outside allowed directories", async () => {
88
107
  const validator = new PathValidator(
89
108
  {
@@ -43,7 +43,7 @@ export 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" | "read_media_file")[] | undefined;
46
+ enabledTools?: ("read_file" | "write_file" | "edit_file" | "read_media_file" | "glob_files" | "grep_content")[] | undefined;
47
47
  }, {
48
48
  type: "filesystem-tools";
49
49
  allowedPaths?: string[] | undefined;
@@ -54,7 +54,7 @@ export 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" | "read_media_file")[] | undefined;
57
+ enabledTools?: ("read_file" | "write_file" | "edit_file" | "read_media_file" | "glob_files" | "grep_content")[] | undefined;
58
58
  }>;
59
59
  export type FileSystemToolsConfig = z.output<typeof FileSystemToolsConfigSchema>;
60
60
  //# sourceMappingURL=tool-factory-config.d.ts.map
@@ -48,64 +48,43 @@ const fileSystemToolsFactory = {
48
48
  backupPath: config.backupPath,
49
49
  backupRetentionDays: config.backupRetentionDays
50
50
  };
51
- let fileSystemService;
52
51
  const resolveWorkingDirectory = (context) => context.workspace?.path ?? fileSystemConfig.workingDirectory ?? process.cwd();
53
- const applyWorkspace = (context, service) => {
54
- const workingDirectory = resolveWorkingDirectory(context);
55
- service.setWorkingDirectory(workingDirectory);
56
- };
57
- const resolveInjectedService = (context) => {
58
- const candidate = context.services?.filesystemService;
59
- if (!candidate) return null;
60
- if (candidate instanceof import_filesystem_service.FileSystemService) return candidate;
61
- const hasMethods = typeof candidate.readFile === "function" && typeof candidate.writeFile === "function" && typeof candidate.setWorkingDirectory === "function" && typeof candidate.setDirectoryApprovalChecker === "function";
62
- return hasMethods ? candidate : null;
63
- };
64
- const getFileSystemService = async (context) => {
65
- const injectedService = resolveInjectedService(context);
66
- if (injectedService) {
67
- const approvalManager2 = context.services?.approval;
68
- if (!approvalManager2) {
69
- throw import_core.ToolError.configInvalid(
70
- "filesystem-tools requires ToolExecutionContext.services.approval"
71
- );
72
- }
73
- injectedService.setDirectoryApprovalChecker(
74
- (filePath) => approvalManager2.isDirectoryApproved(filePath)
75
- );
76
- applyWorkspace(context, injectedService);
77
- return injectedService;
78
- }
79
- if (fileSystemService) {
80
- const approvalManager2 = context.services?.approval;
81
- if (!approvalManager2) {
82
- throw import_core.ToolError.configInvalid(
83
- "filesystem-tools requires ToolExecutionContext.services.approval"
84
- );
85
- }
86
- fileSystemService.setDirectoryApprovalChecker(
87
- (filePath) => approvalManager2.isDirectoryApproved(filePath)
88
- );
89
- applyWorkspace(context, fileSystemService);
90
- return fileSystemService;
91
- }
92
- const logger = context.logger;
93
- fileSystemService = new import_filesystem_service.FileSystemService(fileSystemConfig, logger);
52
+ const createScopedFileSystemService = (context, baseConfig) => {
94
53
  const approvalManager = context.services?.approval;
95
54
  if (!approvalManager) {
96
55
  throw import_core.ToolError.configInvalid(
97
56
  "filesystem-tools requires ToolExecutionContext.services.approval"
98
57
  );
99
58
  }
100
- fileSystemService.setDirectoryApprovalChecker(
101
- (filePath) => approvalManager.isDirectoryApproved(filePath)
59
+ const service = new import_filesystem_service.FileSystemService(
60
+ {
61
+ ...baseConfig,
62
+ workingDirectory: resolveWorkingDirectory(context)
63
+ },
64
+ context.logger
65
+ );
66
+ service.setDirectoryApprovalChecker(
67
+ (filePath) => approvalManager.isDirectoryApproved(filePath, context.sessionId)
68
+ );
69
+ return service;
70
+ };
71
+ const resolveInjectedServiceConfig = (context) => {
72
+ const candidate = context.services?.filesystemService;
73
+ if (!candidate) return null;
74
+ if (candidate instanceof import_filesystem_service.FileSystemService) return candidate.getConfig();
75
+ const getConfig = candidate.getConfig;
76
+ if (typeof getConfig === "function") {
77
+ return getConfig.call(candidate);
78
+ }
79
+ return null;
80
+ };
81
+ const getFileSystemService = async (context) => {
82
+ const scopedFileSystemService = createScopedFileSystemService(
83
+ context,
84
+ resolveInjectedServiceConfig(context) ?? fileSystemConfig
102
85
  );
103
- applyWorkspace(context, fileSystemService);
104
- fileSystemService.initialize().catch((error) => {
105
- const message = error instanceof Error ? error.message : String(error);
106
- logger.error(`Failed to initialize FileSystemService: ${message}`);
107
- });
108
- return fileSystemService;
86
+ await scopedFileSystemService.initialize();
87
+ return scopedFileSystemService;
109
88
  };
110
89
  const toolCreators = {
111
90
  read_file: () => (0, import_read_file_tool.createReadFileTool)(getFileSystemService),
@@ -1 +1 @@
1
- {"version":3,"file":"tool-factory.d.ts","sourceRoot":"","sources":["../src/tool-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAUvD,OAAO,EAGH,KAAK,qBAAqB,EAC7B,MAAM,0BAA0B,CAAC;AAKlC,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,qBAAqB,CA+GrE,CAAC"}
1
+ {"version":3,"file":"tool-factory.d.ts","sourceRoot":"","sources":["../src/tool-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAWvD,OAAO,EAGH,KAAK,qBAAqB,EAC7B,MAAM,0BAA0B,CAAC;AAKlC,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,qBAAqB,CAqFrE,CAAC"}