@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.
- package/dist/directory-approval.cjs +6 -4
- package/dist/directory-approval.d.ts.map +1 -1
- package/dist/directory-approval.integration.test.cjs +235 -6
- package/dist/directory-approval.integration.test.js +236 -6
- package/dist/directory-approval.js +6 -4
- package/dist/filesystem-service.cjs +6 -2
- package/dist/filesystem-service.d.ts.map +1 -1
- package/dist/filesystem-service.js +6 -2
- package/dist/glob-files-tool.cjs +3 -13
- package/dist/glob-files-tool.d.ts.map +1 -1
- package/dist/glob-files-tool.js +3 -3
- package/dist/glob-files-tool.test.cjs +115 -0
- package/dist/glob-files-tool.test.d.ts +2 -0
- package/dist/glob-files-tool.test.d.ts.map +1 -0
- package/dist/glob-files-tool.test.js +92 -0
- package/dist/grep-content-tool.cjs +3 -13
- package/dist/grep-content-tool.d.ts.map +1 -1
- package/dist/grep-content-tool.js +3 -3
- package/dist/grep-content-tool.test.cjs +115 -0
- package/dist/grep-content-tool.test.d.ts +2 -0
- package/dist/grep-content-tool.test.d.ts.map +1 -0
- package/dist/grep-content-tool.test.js +92 -0
- package/dist/path-utils.cjs +55 -0
- package/dist/path-utils.d.ts +11 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +20 -0
- package/dist/path-utils.test.cjs +52 -0
- package/dist/path-utils.test.d.ts +2 -0
- package/dist/path-utils.test.d.ts.map +1 -0
- package/dist/path-utils.test.js +29 -0
- package/dist/path-validator.cjs +23 -9
- package/dist/path-validator.d.ts.map +1 -1
- package/dist/path-validator.js +23 -9
- package/dist/path-validator.test.cjs +41 -0
- package/dist/path-validator.test.js +19 -0
- package/dist/tool-factory-config.d.ts +2 -2
- package/dist/tool-factory.cjs +29 -50
- package/dist/tool-factory.d.ts.map +1 -1
- package/dist/tool-factory.js +29 -50
- package/dist/write-file-tool.test.cjs +33 -0
- package/dist/write-file-tool.test.js +33 -0
- 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 @@
|
|
|
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
|
+
});
|
package/dist/path-validator.cjs
CHANGED
|
@@ -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(
|
|
48
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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;
|
|
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"}
|
package/dist/path-validator.js
CHANGED
|
@@ -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(
|
|
15
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
203
|
+
const workingDir = resolveUserPath(
|
|
204
|
+
process.cwd(),
|
|
205
|
+
this.config.workingDirectory || process.cwd()
|
|
206
|
+
);
|
|
193
207
|
let normalizedPath;
|
|
194
208
|
try {
|
|
195
|
-
normalizedPath =
|
|
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" | "
|
|
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" | "
|
|
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
|
package/dist/tool-factory.cjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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;
|
|
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"}
|